diff --git a/.gitattributes b/.gitattributes
index cd6be071488041d9713c05cbcd87cd2590dc4426..0195febc1afb06c207c447060ec4079a37055bf4 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -2,3 +2,4 @@
 *.png filter=lfs diff=lfs merge=lfs -text
 *.wav filter=lfs diff=lfs merge=lfs -text
 *.ttf filter=lfs diff=lfs merge=lfs -text
+*.apack filter=lfs diff=lfs merge=lfs -text
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c13b95f1d7f3f6d8943ee16ace1873e696c558c9..0867f1bd693312f180c44299939675f09317500c 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -52,30 +52,30 @@ build-linux:
   only:
     - trunk
 
-build-arm64:
-  image: "r.lcr.gr/microhacks/bevy-builder:arm64"
-  tags:
-    - arm64
-  stage: build
-  before_script:
-    - export CARGO_HOME="${CI_PROJECT_DIR}/.cargo"
-    - export PATH="${CI_PROJECT_DIR}/.cargo/bin:$PATH"
-  cache:
-    key: build-cache-arm64
-    paths:
-      - .cargo/registry/cache
-      - .cargo/registry/index
-      - .cargo/git/db
-      - .cargo/bin/
-      - target/
-  script:
-    - cargo build --release -p ${BINARY_FOLDER} --target aarch64-unknown-linux-gnu
-  artifacts:
-    expire_in: 1 day
-    paths:
-      - target/aarch64-unknown-linux-gnu/release/game_core
-  only:
-    - trunk
+#build-arm64:
+#  image: "r.lcr.gr/microhacks/bevy-builder:arm64"
+#  tags:
+#    - arm64
+#  stage: build
+#  before_script:
+#    - export CARGO_HOME="${CI_PROJECT_DIR}/.cargo"
+#    - export PATH="${CI_PROJECT_DIR}/.cargo/bin:$PATH"
+#  cache:
+#    key: build-cache-arm64
+#    paths:
+#      - .cargo/registry/cache
+#      - .cargo/registry/index
+#      - .cargo/git/db
+#      - .cargo/bin/
+#      - target/
+#  script:
+#    - cargo build --release -p ${BINARY_FOLDER} --target aarch64-unknown-linux-gnu
+#  artifacts:
+#    expire_in: 1 day
+#    paths:
+#      - target/aarch64-unknown-linux-gnu/release/game_core
+#  only:
+#    - trunk
 
 build-web:
   stage: build
@@ -112,16 +112,16 @@ package-all:
     - cp -r assets dist/assets
     - cp target/x86_64-unknown-linux-gnu/release/${BINARY_NAME} "dist/${BINARY_NAME}"
     - cp target/x86_64-pc-windows-gnu/release/${BINARY_NAME}.exe "dist/${BINARY_NAME}.exe"
-    - cp target/aarch64-unknown-linux-gnu/release/${BINARY_NAME} "dist/${BINARY_NAME}.arm64"
+#    - cp target/aarch64-unknown-linux-gnu/release/${BINARY_NAME} "dist/${BINARY_NAME}.arm64"
     - cd "${CI_PROJECT_DIR}/dist" && zip -r "windows.zip" "./${BINARY_NAME}.exe" ./assets
     - cd "${CI_PROJECT_DIR}/dist" && zip -r "linux.x86.zip" "./${BINARY_NAME}" ./assets
-    - cd "${CI_PROJECT_DIR}/dist" && zip -r "linux.arm64.zip" "./${BINARY_NAME}.arm64" ./assets
+#    - cd "${CI_PROJECT_DIR}/dist" && zip -r "linux.arm64.zip" "./${BINARY_NAME}.arm64" ./assets
     - cd "${CI_PROJECT_DIR}/${BINARY_FOLDER}/dist" && zip -r "web.zip" ./*
     - cd "${CI_PROJECT_DIR}" && mv "${CI_PROJECT_DIR}/game_core/dist/web.zip" "${CI_PROJECT_DIR}/dist/web.zip"
   dependencies:
     - build-windows
     - build-linux
-    - build-arm64
+#    - build-arm64
     - build-web
   artifacts:
     expire_in: 7 days
@@ -129,7 +129,7 @@ package-all:
       - dist/web.zip
       - dist/windows.zip
       - dist/linux.x86.zip
-      - dist/linux.arm64.zip
+#      - dist/linux.arm64.zip
   only:
     - trunk
 
diff --git a/CREDITS.toml b/CREDITS.toml
new file mode 100644
index 0000000000000000000000000000000000000000..c43b422d4cea1d907c8cf6e5a334319f42710b12
--- /dev/null
+++ b/CREDITS.toml
@@ -0,0 +1,5 @@
+[[font]]
+name = "compass_pro.ttf"
+author = "Eeve Somepx"
+website = "https://somepx.itch.io/humble-fonts-free"
+usage = "Main game font"
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index 2fc492e4d4e279a28300936cfae5e5155a20f15f..c94aed1c5b0af214129004fb50fd2737cf7971d7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -734,6 +734,16 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "bevy_tweening"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d38b2abf5518df10c3c32ee57a54b9ba7067f3bd1c137b912e286d573962145"
+dependencies = [
+ "bevy",
+ "interpolation",
+]
+
 [[package]]
 name = "bevy_ui"
 version = "0.9.1"
@@ -1431,14 +1441,17 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "bevy",
+ "bevy_tweening",
  "fastrand",
  "iyes_loopless",
  "log",
+ "micro_asset_io",
  "micro_banimate",
  "micro_musicbox",
  "serde",
  "serde_json",
  "thiserror",
+ "toml",
  "web-sys",
 ]
 
@@ -1699,6 +1712,12 @@ dependencies = [
  "web-sys",
 ]
 
+[[package]]
+name = "interpolation"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3b7357d2bbc5ee92f8e899ab645233e43d21407573cceb37fed8bc3dede2c02"
+
 [[package]]
 name = "itoa"
 version = "1.0.4"
@@ -1845,6 +1864,17 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "lzma-sys"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+]
+
 [[package]]
 name = "mach"
 version = "0.3.2"
@@ -1910,6 +1940,23 @@ dependencies = [
  "objc",
 ]
 
+[[package]]
+name = "micro_asset_io"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "bevy_app",
+ "bevy_asset",
+ "bevy_ecs",
+ "bevy_reflect",
+ "bevy_tasks",
+ "flate2",
+ "futures-lite",
+ "log",
+ "tar",
+ "xz2",
+]
+
 [[package]]
 name = "micro_banimate"
 version = "0.2.1"
@@ -2801,6 +2848,17 @@ dependencies = [
  "typenum",
 ]
 
+[[package]]
+name = "tar"
+version = "0.4.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6"
+dependencies = [
+ "filetime",
+ "libc",
+ "xattr",
+]
+
 [[package]]
 name = "termcolor"
 version = "1.1.3"
@@ -3461,6 +3519,15 @@ dependencies = [
  "pkg-config",
 ]
 
+[[package]]
+name = "xattr"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "xcursor"
 version = "0.3.4"
@@ -3481,3 +3548,12 @@ name = "xml-rs"
 version = "0.8.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
+
+[[package]]
+name = "xz2"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2"
+dependencies = [
+ "lzma-sys",
+]
diff --git a/Cargo.toml b/Cargo.toml
index a1651997580825f71a14895ee1f3d613f49b352c..1ae6fca738a1d73d133d7756e3b707c5adaceb7e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,6 +4,7 @@ resolver = "2"
 
 members = [
     "game_core",
+    "micro_asset_io",
 ]
 
 [workspace.dependencies]
@@ -18,6 +19,9 @@ iyes_loopless = "0.9.1"
 micro_musicbox = { version = "0.5.0", features = ["mp3"] }
 micro_banimate = "0.2.1"
 
+kayak_ui = { rev = "220694d12a5aeffe680fdaf2a8e5ac3ed9d81ae7", git = "https://github.com/StarArawn/kayak_ui" }
+kayak_font = { rev = "220694d12a5aeffe680fdaf2a8e5ac3ed9d81ae7", git = "https://github.com/StarArawn/kayak_ui.git" }
+
 [workspace.dependencies.bevy]
 version = "0.9.0"
 default-features = false
diff --git a/Makefile b/Makefile
index 2dc4e111f5fabb10dc0a0a26fea2a3e955504ff9..3d8c38951e82535b0e1a6890b832a3297a239fb8 100644
--- a/Makefile
+++ b/Makefile
@@ -30,6 +30,9 @@ run-web:
 check:
 	cargo check --release --features "bevy/dynamic" -p game_core
 
+pak:
+	cd raw_assets && tar -cJf ../assets/resources.apack ./
+
 build-windows: clean_dist top_tail
 	docker run --rm --name "${PROJECT_NAME}-build-windows" -v "$(CURRENT_DIRECTORY):/app" -w /app --user $(shell id -u):$(shell id -g) r.lcr.gr/microhacks/bevy-builder \
 		cargo build --release -p game_core --target x86_64-pc-windows-gnu
diff --git a/assets/maps/overworld.ldtk b/assets/maps/overworld.ldtk
new file mode 100644
index 0000000000000000000000000000000000000000..deca56318dc78664be698711b94a41cd1b7cafd0
--- /dev/null
+++ b/assets/maps/overworld.ldtk
@@ -0,0 +1,63 @@
+{
+	"__header__": {
+		"fileType": "LDtk Project JSON",
+		"app": "LDtk",
+		"doc": "https://ldtk.io/json",
+		"schema": "https://ldtk.io/files/JSON_SCHEMA.json",
+		"appAuthor": "Sebastien 'deepnight' Benard",
+		"appVersion": "1.1.3",
+		"url": "https://ldtk.io"
+	},
+	"jsonVersion": "1.1.3",
+	"appBuildId": 458364,
+	"nextUid": 1,
+	"identifierStyle": "Capitalize",
+	"worldLayout": "Free",
+	"worldGridWidth": 256,
+	"worldGridHeight": 256,
+	"defaultLevelWidth": 256,
+	"defaultLevelHeight": 256,
+	"defaultPivotX": 0,
+	"defaultPivotY": 0,
+	"defaultGridSize": 16,
+	"bgColor": "#40465B",
+	"defaultLevelBgColor": "#696A79",
+	"minifyJson": false,
+	"externalLevels": false,
+	"exportTiled": false,
+	"simplifiedExport": false,
+	"imageExportMode": "None",
+	"pngFilePattern": null,
+	"backupOnSave": false,
+	"backupLimit": 10,
+	"levelNamePattern": "Level_%idx",
+	"tutorialDesc": null,
+	"flags": [],
+	"defs": { "layers": [], "entities": [], "tilesets": [], "enums": [], "externalEnums": [], "levelFields": [] },
+	"levels": [
+		{
+			"identifier": "Level_0",
+			"iid": "bb560c20-5110-11ed-89b7-477fe10d896e",
+			"uid": 0,
+			"worldX": 0,
+			"worldY": 0,
+			"worldDepth": 0,
+			"pxWid": 256,
+			"pxHei": 256,
+			"__bgColor": "#696A79",
+			"bgColor": null,
+			"useAutoIdentifier": true,
+			"bgRelPath": null,
+			"bgPos": null,
+			"bgPivotX": 0.5,
+			"bgPivotY": 0.5,
+			"__smartColor": "#ADADB5",
+			"__bgPos": null,
+			"externalRelPath": null,
+			"fieldInstances": [],
+			"layerInstances": [],
+			"__neighbours": []
+		}
+	],
+	"worlds": []
+}
\ No newline at end of file
diff --git a/assets/resources.apack b/assets/resources.apack
new file mode 100644
index 0000000000000000000000000000000000000000..b3121861cf1be79f9eaebfdcd0074d351e85a440
--- /dev/null
+++ b/assets/resources.apack
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:14b68833d9afa61d753201afec2aa717ed8266344799c5a46bdb03892288331a
+size 44828
diff --git a/game_core/Cargo.toml b/game_core/Cargo.toml
index e29da139023946621c2256a673f544644dd2be83..af78c8b55d9699089c7d2dd1150694471dc23aa3 100644
--- a/game_core/Cargo.toml
+++ b/game_core/Cargo.toml
@@ -19,6 +19,11 @@ iyes_loopless.workspace = true
 micro_banimate.workspace = true
 micro_musicbox.workspace = true
 
+micro_asset_io = { path = "../micro_asset_io" }
+
+bevy_tweening = "0.6.0"
+toml = "0.5.9"
+
 #remote_events = { git = "https://lab.lcr.gr/microhacks/micro-bevy-remote-events.git", rev = "be0c6b43a73e4c5e7ece20797e3d6f59340147b4"}
 
 [target.'cfg(target_arch = "wasm32")'.dependencies]
diff --git a/game_core/src/assets/apack_handler.rs b/game_core/src/assets/apack_handler.rs
new file mode 100644
index 0000000000000000000000000000000000000000..ea3e271e2ac612e24c7003c90ff3f171c37dbc24
--- /dev/null
+++ b/game_core/src/assets/apack_handler.rs
@@ -0,0 +1,226 @@
+/// This module is a cut down  version of the currently WIP APack loader plugin that will
+/// accompany micro_asset_io.
+///
+/// As such, it is missing many features and has been put together by cutting bits from various
+/// files. Have a peek for curiosity, but try not to learn from this particular file
+///
+use std::ffi::OsStr;
+use std::path::PathBuf;
+
+use bevy::asset::Asset;
+use bevy::ecs::system::{SystemParam, SystemParamFetch, SystemState};
+use bevy::math::vec2;
+use bevy::prelude::*;
+use bevy::render::texture::{CompressedImageFormats, ImageType};
+use micro_asset_io::{APack, APackProcessingComplete};
+use serde::{Deserialize, Serialize};
+
+use crate::assets::AssetHandles;
+
+fn file_prefix(path: &PathBuf) -> Option<String> {
+	path.file_name().and_then(|ss: &OsStr| {
+		let lossy = ss.to_string_lossy();
+		let mut parts = lossy.split(".");
+		parts.next().map(String::from)
+	})
+}
+
+trait BuildType {
+	fn build_from_bytes(bytes: &Vec<u8>) -> Self;
+}
+
+impl BuildType for Image {
+	fn build_from_bytes(bytes: &Vec<u8>) -> Self {
+		Image::from_buffer(
+			bytes.as_slice(),
+			ImageType::Extension("png"),
+			CompressedImageFormats::all(),
+			true,
+		)
+		.expect("Invalid PNG")
+	}
+}
+
+#[derive(Deserialize, Serialize, Debug)]
+pub struct ManifestGenericAsset {
+	pub name: String,
+	pub path: String,
+}
+
+#[derive(Deserialize, Serialize, Debug)]
+pub struct ManifestImage {
+	pub name: String,
+	pub path: String,
+	pub format: String,
+}
+
+#[derive(Deserialize, Serialize, Debug)]
+pub struct ManifestSpriteSheetDescriptor {
+	pub size: usize,
+	pub rows: usize,
+	pub columns: usize,
+}
+
+#[derive(Deserialize, Serialize, Debug)]
+pub struct ManifestSpriteSheet {
+	pub name: String,
+	pub path: String,
+	pub tiles: ManifestSpriteSheetDescriptor,
+}
+
+#[derive(Deserialize, Serialize, Debug)]
+pub struct APackManifest {
+	#[serde(default = "Vec::new")]
+	pub images: Vec<ManifestImage>,
+	#[serde(default = "Vec::new")]
+	pub spritesheets: Vec<ManifestSpriteSheet>,
+	#[serde(default = "Vec::new")]
+	pub fonts: Vec<ManifestGenericAsset>,
+}
+
+fn in_world<Param: SystemParam + 'static>(
+	world: &mut World,
+	cb: impl FnOnce(&mut <Param::Fetch as SystemParamFetch>::Item),
+) {
+	let mut state: SystemState<Param> = SystemState::new(world);
+	let mut params = state.get_mut(world);
+	cb(&mut params);
+}
+
+fn create_asset_type<Type: Asset + BuildType>(
+	world: &mut World,
+	bytes: &Vec<u8>,
+	cb: impl FnOnce(Handle<Type>, &mut ResMut<AssetHandles>),
+) {
+	let mut state: SystemState<(ResMut<Assets<Type>>, ResMut<AssetHandles>)> =
+		SystemState::new(world);
+	let (mut assets, mut handles) = state.get_mut(world);
+
+	let value = Type::build_from_bytes(bytes);
+	let handle = assets.add(value);
+
+	cb(handle, &mut handles);
+}
+
+pub fn handle_apack_process_events(world: &mut World) {
+	let events: Vec<APackProcessingComplete> = {
+		let mut state: SystemState<ResMut<Events<APackProcessingComplete>>> =
+			SystemState::new(world);
+		let mut events = state.get_mut(world);
+
+		events.drain().collect()
+	};
+
+	for event in events {
+		let pack = {
+			let mut state: SystemState<ResMut<Assets<APack>>> = SystemState::new(world);
+			let mut packs = state.get_mut(world);
+			if let Some(pack) = packs.get_mut(&event.0) {
+				if pack.processed {
+					std::mem::take(&mut pack.vfs)
+				} else {
+					continue;
+				}
+			} else {
+				continue;
+			}
+		};
+
+		if let Some(raw) = pack.get("./manifest.toml") {
+			let manifest: APackManifest =
+				toml::from_slice(raw.as_slice()).expect("Badly formatted apack manifest");
+
+			for image in &manifest.images {
+				let asset = pack
+					.get(&format!("./{}", &image.path))
+					.expect("Missing asset");
+
+				if let Ok(image_data) = Image::from_buffer(
+					asset.as_slice(),
+					ImageType::Extension(&image.format),
+					CompressedImageFormats::all(),
+					true,
+				) {
+					in_world::<ParamSet<(ResMut<Assets<Image>>, ResMut<AssetHandles>)>>(
+						world,
+						move |params: &mut ParamSet<(
+							ResMut<Assets<Image>>,
+							ResMut<AssetHandles>,
+						)>| {
+							let handle = params.p0().add(image_data);
+							params.p1().images.insert(image.name.clone(), handle);
+						},
+					);
+				} else {
+					log::warn!("Malformed image {}", &image.path);
+				}
+			}
+
+			for font in &manifest.fonts {
+				let asset = pack
+					.get(&format!("./{}", &font.path))
+					.expect("Missing asset");
+
+				if let Ok(font_data) = Font::try_from_bytes(asset.to_vec()) {
+					in_world::<ParamSet<(ResMut<Assets<Font>>, ResMut<AssetHandles>)>>(
+						world,
+						move |params: &mut ParamSet<(
+							ResMut<Assets<Font>>,
+							ResMut<AssetHandles>,
+						)>| {
+							let handle = params.p0().add(font_data);
+							params.p1().fonts.insert(font.name.clone(), handle);
+						},
+					);
+				} else {
+					log::warn!("Malformed font {}", font.path);
+				}
+			}
+
+			for spritesheet in &manifest.spritesheets {
+				let asset = pack
+					.get(&format!("./{}", &spritesheet.path))
+					.expect("Missing asset");
+
+				if let Ok(image_data) = Image::from_buffer(
+					asset.as_slice(),
+					ImageType::Extension("png"),
+					CompressedImageFormats::all(),
+					true,
+				) {
+					in_world::<
+						ParamSet<(
+							ResMut<Assets<Image>>,
+							ResMut<Assets<TextureAtlas>>,
+							ResMut<AssetHandles>,
+						)>,
+					>(
+						world,
+						move |params: &mut ParamSet<(
+							ResMut<Assets<Image>>,
+							ResMut<Assets<TextureAtlas>>,
+							ResMut<AssetHandles>,
+						)>| {
+							let image_handle = params.p0().add(image_data);
+							let texture_atlas = TextureAtlas::from_grid(
+								image_handle.clone_weak(),
+								Vec2::splat(spritesheet.tiles.size as f32),
+								spritesheet.tiles.columns,
+								spritesheet.tiles.rows,
+								None,
+								None,
+							);
+
+							let atlas_handle = params.p1().add(texture_atlas);
+							let mut assets = params.p2();
+							assets.images.insert(spritesheet.name.clone(), image_handle);
+							assets.atlas.insert(spritesheet.name.clone(), atlas_handle);
+						},
+					);
+				} else {
+					log::warn!("Malformed spritesheet {}", &spritesheet.path);
+				}
+			}
+		}
+	}
+}
diff --git a/game_core/src/assets/loader.rs b/game_core/src/assets/loader.rs
index a66028a4ec6e26ca17a5f6e7530751c1aba16cb1..e725fbbb70a446d7c4c8ac3a5bcf9f6ace166c8d 100644
--- a/game_core/src/assets/loader.rs
+++ b/game_core/src/assets/loader.rs
@@ -4,6 +4,7 @@ use bevy::asset::LoadState;
 use bevy::ecs::system::SystemParam;
 use bevy::prelude::*;
 use bevy::reflect::TypeUuid;
+use micro_asset_io::APack;
 use micro_musicbox::prelude::AudioSource;
 
 use crate::assets::{AssetHandles, FixedAssetNameMapping, SpriteSheetConfig};
@@ -55,6 +56,7 @@ impl<'w, 's> AssetTypeLoader<'w, 's> {
 	load_basic_type!(load_images, Image => images);
 	load_basic_type!(load_audio, AudioSource => sounds);
 	load_basic_type!(load_font, Font => fonts);
+	load_basic_type!(load_apack, APack => apacks);
 
 	pub fn load_spritesheet(
 		&mut self,
diff --git a/game_core/src/assets/mod.rs b/game_core/src/assets/mod.rs
index 5fc030efb503352aec0631efe091a2e49b9a2124..37ce6f9abbe8fee11a395aedeaf126468ec8f68b 100644
--- a/game_core/src/assets/mod.rs
+++ b/game_core/src/assets/mod.rs
@@ -1,3 +1,4 @@
+mod apack_handler;
 mod loader;
 mod resources;
 mod startup;
@@ -8,6 +9,7 @@ use iyes_loopless::prelude::AppLooplessStateExt;
 pub use loader::AssetTypeLoader;
 pub use resources::{AssetHandles, AssetNameMapping, FixedAssetNameMapping, SpriteSheetConfig};
 
+use crate::assets::apack_handler::handle_apack_process_events;
 use crate::system::flow::AppState;
 
 pub struct AssetsPlugin;
@@ -16,6 +18,13 @@ impl Plugin for AssetsPlugin {
 		app.init_resource::<AssetHandles>()
 			.add_enter_system(AppState::Preload, startup::start_preload_resources)
 			.add_enter_system(AppState::Preload, startup::start_load_resources)
+			.add_system(handle_apack_process_events)
+			.add_enter_system(
+				AppState::Menu,
+				|assets: bevy::prelude::Res<AssetHandles>| {
+					log::info!("{:?}", assets.images);
+				},
+			)
 			.add_system_set(
 				ConditionSet::new()
 					.run_in_state(AppState::Setup)
diff --git a/game_core/src/assets/resources.rs b/game_core/src/assets/resources.rs
index 0800d966e3cc927da713f7956904cd95297e6eb3..3b317e0475cd6e75bf4334f37db84a36a12772e6 100644
--- a/game_core/src/assets/resources.rs
+++ b/game_core/src/assets/resources.rs
@@ -1,5 +1,6 @@
 use bevy::prelude::*;
 use bevy::utils::HashMap;
+use micro_asset_io::APack;
 use micro_musicbox::prelude::AudioSource;
 use micro_musicbox::utilities::{SuppliesAudio, TrackType};
 
@@ -37,6 +38,7 @@ pub struct AssetHandles {
 	pub atlas: HashMap<String, Handle<TextureAtlas>>,
 	pub sounds: HashMap<String, Handle<AudioSource>>,
 	pub fonts: HashMap<String, Handle<Font>>,
+	pub apacks: HashMap<String, Handle<APack>>,
 }
 
 macro_rules! fetch_wrapper {
@@ -66,6 +68,7 @@ impl AssetHandles {
 	fetch_wrapper!(atlas, TextureAtlas => atlas);
 	fetch_wrapper!(sound, AudioSource => sounds);
 	fetch_wrapper!(font, Font => fonts);
+	fetch_wrapper!(apack, APack => apacks);
 }
 
 impl SuppliesAudio for AssetHandles {
diff --git a/game_core/src/assets/startup.rs b/game_core/src/assets/startup.rs
index cb6d71ed2de4f8479c254b106a5effea69d0b012..18641088458aee209dc9716d4f0420b1b0b0df9f 100644
--- a/game_core/src/assets/startup.rs
+++ b/game_core/src/assets/startup.rs
@@ -14,6 +14,7 @@ pub fn start_preload_resources(mut commands: Commands) {
 pub fn start_load_resources(mut loader: AssetTypeLoader) {
 	loader.load_images(&[("splash.png", "splash")]);
 	loader.load_audio(&[("splash_sting.mp3", "splash_sting")]);
+	loader.load_apack(&[("resources.apack", "resources")]);
 }
 
 pub fn check_load_resources(mut commands: Commands, loader: AssetTypeLoader) {
diff --git a/game_core/src/lib.rs b/game_core/src/lib.rs
index e0acb2fac262674fb983e9880091120b5ad2a646..cd6d8dcfd7d64c2e9d4066f4b458ed62d3343301 100644
--- a/game_core/src/lib.rs
+++ b/game_core/src/lib.rs
@@ -1,4 +1,7 @@
+extern crate core;
+
 pub mod assets;
 pub mod multiplayer;
 pub mod splash_screen;
+pub mod states;
 pub mod system;
diff --git a/game_core/src/main.rs b/game_core/src/main.rs
index 1eda1ba63ea6e8a0e5938db49a1bd2b4321b7369..ba118436d60bf522ed2e16438c5a429e1ff02229 100644
--- a/game_core/src/main.rs
+++ b/game_core/src/main.rs
@@ -4,7 +4,6 @@ use game_core::system::flow::AppState;
 use game_core::system::resources::InitAppPlugins;
 use iyes_loopless::prelude::AppLooplessStateExt;
 use micro_musicbox::CombinedAudioPlugins;
-// use remote_events::RemoteEventPlugin;
 
 fn main() {
 	App::new()
@@ -14,9 +13,8 @@ fn main() {
 		.add_plugins(CombinedAudioPlugins::<AssetHandles>::new())
 		.add_plugin(game_core::splash_screen::SplashScreenPlugin)
 		.add_plugin(game_core::system::camera::CameraManagementPlugin)
-		// .add_plugin(RemoteEventPlugin::<
-		// 	game_core::multiplayer::OutgoingEvent,
-		// 	game_core::multiplayer::IncomingEvent,
-		// >::new())
+		.add_plugin(game_core::states::StatesPlugin)
+		.add_plugin(micro_asset_io::MicroAssetIOPlugin)
+		.add_plugin(bevy_tweening::TweeningPlugin)
 		.run();
 }
diff --git a/game_core/src/states/menu_state.rs b/game_core/src/states/menu_state.rs
new file mode 100644
index 0000000000000000000000000000000000000000..e0638347a92677d9bbc5b4df9598f7b8e2c5b7cf
--- /dev/null
+++ b/game_core/src/states/menu_state.rs
@@ -0,0 +1,84 @@
+use std::time::Duration;
+
+use bevy::prelude::*;
+use bevy_tweening::lens::TextColorLens;
+use bevy_tweening::{Animator, EaseFunction, RepeatCount, RepeatStrategy, Tween};
+
+use crate::assets::AssetHandles;
+
+#[derive(Component)]
+pub struct MenuStateEntity;
+
+pub fn spawn_menu_entities(mut commands: Commands, assets: Res<AssetHandles>) {
+	commands.spawn((
+		SpriteBundle {
+			texture: assets.image("menu_background"),
+			..Default::default()
+		},
+		MenuStateEntity,
+	));
+
+	commands
+		.spawn(NodeBundle {
+			style: Style {
+				size: Size::new(Val::Percent(100.0), Val::Percent(100.0)),
+				flex_direction: FlexDirection::Column,
+				align_items: AlignItems::Center,
+				..Default::default()
+			},
+			..Default::default()
+		})
+		.with_children(|commands| {
+			commands.spawn(TextBundle {
+				text: Text::from_section(
+					"Trader Tales",
+					TextStyle {
+						font_size: 72.0,
+						font: assets.font("default"),
+						color: Color::ANTIQUE_WHITE,
+					},
+				),
+				style: Style {
+					margin: UiRect::top(Val::Percent(20.0)),
+					..Default::default()
+				},
+				..Default::default()
+			});
+			commands.spawn((
+				TextBundle {
+					text: Text::from_section(
+						"> Get Trading <",
+						TextStyle {
+							font_size: 48.0,
+							font: assets.font("default"),
+							color: Color::ANTIQUE_WHITE,
+						},
+					),
+					style: Style {
+						margin: UiRect::top(Val::Px(50.0)),
+						..Default::default()
+					},
+					..Default::default()
+				},
+				Animator::new(
+					Tween::new(
+						EaseFunction::QuadraticInOut,
+						Duration::from_secs(1),
+						TextColorLens {
+							start: Color::ANTIQUE_WHITE,
+							end: *Color::ANTIQUE_WHITE.set_a(0.0),
+							section: 0,
+						},
+					)
+					.with_repeat_count(RepeatCount::Infinite)
+					.with_repeat_strategy(RepeatStrategy::MirroredRepeat),
+				),
+			));
+		});
+}
+
+pub fn despawn_menu_entities(mut commands: Commands, query: Query<Entity, With<MenuStateEntity>>) {
+	for entity in &query {
+		commands.entity(entity).despawn_recursive();
+	}
+}
diff --git a/game_core/src/states/mod.rs b/game_core/src/states/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..20d5cbde4e5d2574926702faf92e26d62847f9d0
--- /dev/null
+++ b/game_core/src/states/mod.rs
@@ -0,0 +1,14 @@
+use bevy::app::{App, Plugin};
+use iyes_loopless::prelude::AppLooplessStateExt;
+
+use crate::system::flow::AppState;
+
+mod menu_state;
+
+pub struct StatesPlugin;
+impl Plugin for StatesPlugin {
+	fn build(&self, app: &mut App) {
+		app.add_enter_system(AppState::Menu, menu_state::spawn_menu_entities)
+			.add_exit_system(AppState::Menu, menu_state::despawn_menu_entities);
+	}
+}
diff --git a/game_core/src/system/load_config.rs b/game_core/src/system/load_config.rs
index fa15cb9108edc2b0c32fbd78aa290d8c293c2ae5..a989503b5a093d3b463381b4d121f5efc29be91f 100644
--- a/game_core/src/system/load_config.rs
+++ b/game_core/src/system/load_config.rs
@@ -13,7 +13,7 @@ mod setup {
 		(1280.0, 720.0)
 	}
 	pub fn virtual_size() -> (f32, f32) {
-		(1280.0, 720.0)
+		(640.0, 360.0)
 	}
 }
 
@@ -23,7 +23,7 @@ mod setup {
 		String::from("assets")
 	}
 	pub fn virtual_size() -> (f32, f32) {
-		(1280.0, 720.0)
+		(640.0, 360.0)
 	}
 	#[cfg(feature = "no_aspect")]
 	pub fn initial_size() -> (f32, f32) {
diff --git a/micro_asset_io/Cargo.toml b/micro_asset_io/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..49f7081c5160e5aed6d9c3476035b065cc8a83bf
--- /dev/null
+++ b/micro_asset_io/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "micro_asset_io"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+bevy_asset = "0.9.1"
+bevy_reflect = "0.9.1"
+bevy_ecs = "0.9.1"
+bevy_tasks = "0.9.1"
+bevy_app = "0.9.1"
+
+anyhow = "1.0.66"
+flate2 = { version = "1.0.25", features = ["rust_backend"] }
+tar = "0.4.38"
+log = "0.4.17"
+futures-lite = "1.12.0"
+xz2 = "0.1.7"
\ No newline at end of file
diff --git a/micro_asset_io/src/apack_loader.rs b/micro_asset_io/src/apack_loader.rs
new file mode 100644
index 0000000000000000000000000000000000000000..2417fc45c3e15d9a4b579f81394589b2cab170ef
--- /dev/null
+++ b/micro_asset_io/src/apack_loader.rs
@@ -0,0 +1,248 @@
+use std::collections::HashMap;
+use std::fmt::{Debug, Formatter};
+use std::io::Read;
+use std::ops::{Deref, DerefMut, Index, IndexMut};
+
+use bevy_asset::{AssetEvent, AssetLoader, Assets, BoxedFuture, Handle, LoadContext, LoadedAsset};
+use bevy_ecs::component::Component;
+use bevy_ecs::entity::Entity;
+use bevy_ecs::event::{EventReader, EventWriter};
+use bevy_ecs::system::{Commands, Query, Res, ResMut};
+use bevy_reflect::TypeUuid;
+use bevy_tasks::{AsyncComputeTaskPool, Task};
+use futures_lite::future;
+use xz2::read::XzDecoder;
+// use bevy::prelude::*;
+
+#[derive(Default)]
+pub struct APackLoader;
+
+#[derive(TypeUuid)]
+#[uuid = "bce5dd7a-726c-11ed-b53f-071b386b4896"]
+pub struct APack {
+	pub processed: bool,
+	pub compressed_bytes: Vec<u8>,
+	pub vfs: HashMap<String, Vec<u8>>,
+}
+
+struct DebugVfs<'a>(&'a HashMap<String, Vec<u8>>);
+impl<'a> Debug for DebugVfs<'a> {
+	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+		f.debug_map()
+			.entries(self.0.iter().map(|(key, value)| {
+				let key = key.clone();
+				let value = format!("len({})", value.len());
+
+				(key, value)
+			}))
+			.finish()
+	}
+}
+
+impl Debug for APack {
+	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+		f.debug_struct("APack")
+			.field("processed", &self.processed)
+			.field(
+				"compressed_bytes",
+				&format!("len({})", self.compressed_bytes.len()),
+			)
+			.field("vfs", &DebugVfs(&self.vfs))
+			.finish()
+	}
+}
+
+pub struct ClonableAPack(pub APack);
+impl Deref for ClonableAPack {
+	type Target = APack;
+
+	fn deref(&self) -> &Self::Target {
+		&self.0
+	}
+}
+impl DerefMut for ClonableAPack {
+	fn deref_mut(&mut self) -> &mut Self::Target {
+		&mut self.0
+	}
+}
+impl Clone for ClonableAPack {
+	fn clone(&self) -> Self {
+		ClonableAPack(APack {
+			processed: self.processed,
+			compressed_bytes: self.compressed_bytes.clone(),
+			vfs: self.vfs.clone(),
+		})
+	}
+}
+impl Into<APack> for ClonableAPack {
+	fn into(self) -> APack {
+		self.0
+	}
+}
+
+impl APack {
+	pub fn new(bytes: Vec<u8>) -> APack {
+		APack {
+			processed: false,
+			compressed_bytes: bytes,
+			vfs: Default::default(),
+		}
+	}
+
+	/// Cloning an APack can be _very_ expensive, depending on the
+	/// contents. To clone an APack, you've got to jump through some hoops
+	pub fn into_clone(self) -> ClonableAPack {
+		ClonableAPack(self)
+	}
+
+	pub fn clone_ref(apack: &APack) -> APack {
+		APack {
+			processed: apack.processed,
+			compressed_bytes: apack.compressed_bytes.clone(),
+			vfs: apack.vfs.clone(),
+		}
+	}
+
+	/// An APack doesn't need to hold onto the raw bytes after it has been processed.
+	/// This method clears the raw buffer
+	pub fn drop_raw_bytes(&mut self) {
+		self.compressed_bytes = Vec::new();
+	}
+
+	pub fn set(&mut self, key: impl ToString, value: Vec<u8>) {
+		self.vfs.insert(key.to_string(), value);
+	}
+}
+
+impl Index<&String> for APack {
+	type Output = Vec<u8>;
+
+	fn index(&self, index: &String) -> &Self::Output {
+		&self.vfs[index]
+	}
+}
+impl IndexMut<&String> for APack {
+	fn index_mut(&mut self, index: &String) -> &mut Self::Output {
+		self.vfs
+			.get_mut(index)
+			.expect(&*format!("Missing key {}", index))
+	}
+}
+
+impl AssetLoader for APackLoader {
+	fn load<'a>(
+		&'a self,
+		bytes: &'a [u8],
+		load_context: &'a mut LoadContext,
+	) -> BoxedFuture<'a, anyhow::Result<(), anyhow::Error>> {
+		Box::pin(async move {
+			load_context.set_default_asset(LoadedAsset::new(APack::new(bytes.to_vec())));
+			Ok(())
+		})
+	}
+
+	fn extensions(&self) -> &[&str] {
+		&["apack"]
+	}
+}
+
+type TaskOutput = (APack, Handle<APack>);
+#[derive(Component)]
+pub struct ProcessAPack(Task<TaskOutput>);
+
+fn create_task_body(
+	mut pack: APack,
+	handle_ref: Handle<APack>,
+) -> impl futures_lite::Future<Output = TaskOutput> {
+	async move {
+		let mut pack = pack;
+
+		let raw_bytes = std::mem::take(&mut pack.compressed_bytes);
+		let decoder = XzDecoder::new(raw_bytes.as_slice());
+		let mut tarboy = tar::Archive::new(decoder);
+
+		if let Ok(entries) = tarboy.entries() {
+			for entry in entries.flatten() {
+				let path = &entry
+					.path()
+					.ok()
+					.and_then(|v| v.to_str().map(String::from))
+					.expect("Malformed path");
+
+				let file = entry.bytes().flatten().collect::<Vec<u8>>();
+				pack.set(path, file);
+			}
+		}
+
+		pack.processed = true;
+		pack.drop_raw_bytes();
+		(pack, handle_ref)
+	}
+}
+
+pub fn process_loaded_apacks(
+	mut commands: Commands,
+	mut events: EventReader<AssetEvent<APack>>,
+	packs: Res<Assets<APack>>,
+) {
+	if !events.is_empty() {
+		let pool = AsyncComputeTaskPool::get();
+
+		for event in events.iter() {
+			match event {
+				AssetEvent::Created { handle } => {
+					if let Some(pack) = packs.get(handle) {
+						let copy = APack::clone_ref(pack);
+						let handle_ref = handle.clone_weak();
+						let task = pool.spawn(create_task_body(copy, handle_ref));
+
+						commands.spawn(ProcessAPack(task));
+					} else {
+						log::warn!("Received a 'created' event for an apack that is not part of the Assets<APack> resource");
+					}
+				}
+				AssetEvent::Modified { handle } => {
+					if let Some(pack) = packs.get(handle) {
+						if !pack.processed {
+							log::debug!(
+								"Handling hot-reload of APack by spawning new processing task"
+							);
+
+							let copy = APack::clone_ref(pack);
+							let handle_ref = handle.clone_weak();
+							let task = pool.spawn(create_task_body(copy, handle_ref));
+
+							commands.spawn(ProcessAPack(task));
+						} else {
+							log::debug!("APack was changed, but will not notify because it has already been processed")
+						}
+					} else {
+						log::warn!("Received a 'modified' event for an apack that is not part of the Assets<APack> resource");
+					}
+				}
+				AssetEvent::Removed { .. } => {
+					log::debug!("Removed an APack. Handles already extracted from this APack may not be removed");
+				}
+			}
+		}
+	}
+}
+
+pub struct APackProcessingComplete(pub Handle<APack>);
+
+pub fn poll_apack_loading_tasks(
+	mut commands: Commands,
+	mut query: Query<(Entity, &mut ProcessAPack)>,
+	mut packs: ResMut<Assets<APack>>,
+	mut events: EventWriter<APackProcessingComplete>,
+) {
+	for (entity, mut task) in &mut query {
+		if let Some((pack, handle)) = future::block_on(future::poll_once(&mut task.0)) {
+			if let Some(existing) = packs.get_mut(&handle) {
+				*existing = pack;
+			}
+			events.send(APackProcessingComplete(handle.clone_weak()));
+			commands.entity(entity).despawn();
+		}
+	}
+}
diff --git a/micro_asset_io/src/lib.rs b/micro_asset_io/src/lib.rs
new file mode 100644
index 0000000000000000000000000000000000000000..33b995857d8c68433e10ea3fdf410d7c6abf731d
--- /dev/null
+++ b/micro_asset_io/src/lib.rs
@@ -0,0 +1,20 @@
+mod apack_loader;
+
+mod micro_asset_io {
+	use bevy_app::{App, Plugin};
+	use bevy_asset::AddAsset;
+
+	pub struct MicroAssetIOPlugin;
+	impl Plugin for MicroAssetIOPlugin {
+		fn build(&self, app: &mut App) {
+			app.add_event::<super::apack_loader::APackProcessingComplete>()
+				.add_asset::<super::apack_loader::APack>()
+				.add_asset_loader(super::apack_loader::APackLoader)
+				.add_system(super::apack_loader::process_loaded_apacks)
+				.add_system(super::apack_loader::poll_apack_loading_tasks);
+		}
+	}
+}
+
+pub use apack_loader::{APack, APackLoader, APackProcessingComplete, ClonableAPack, ProcessAPack};
+pub use micro_asset_io::MicroAssetIOPlugin;
diff --git a/raw_assets/.gitignore b/raw_assets/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..fbeaca80e3ce81fa39959a7c162c6f3e2bccd7ff
--- /dev/null
+++ b/raw_assets/.gitignore
@@ -0,0 +1,6 @@
+backgrounds/
+fonts/
+sprites/
+
+!.gitignore
+!manifest.toml
\ No newline at end of file
diff --git a/raw_assets/manifest.toml b/raw_assets/manifest.toml
new file mode 100644
index 0000000000000000000000000000000000000000..c6536724a339df51bbfcb7d77dcea613885082c0
--- /dev/null
+++ b/raw_assets/manifest.toml
@@ -0,0 +1,13 @@
+[[spritesheets]]
+path = "sprites/overworld.png"
+name = "overworld"
+tiles = { size = 4, columns = 32, rows = 64 }
+
+[[images]]
+path = "backgrounds/main_menu.png"
+name = "menu_background"
+format = "png"
+
+[[fonts]]
+path = "fonts/CompassPro.ttf"
+name = "default"
\ No newline at end of file