diff --git a/Cargo.lock b/Cargo.lock
index 1c95b736ee6c90c505d3cad2ef52ea762398986b..2d2ed425816ac02d5e84902e15fb3bce4f775449 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -197,6 +197,24 @@ dependencies = [
  "bevy_internal",
 ]
 
+[[package]]
+name = "bevy_animation"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d43b8073f299eb60ce9e1d60fa293b348590dd57aca8321d6859d9e7aa57d2da"
+dependencies = [
+ "bevy_app",
+ "bevy_asset",
+ "bevy_core",
+ "bevy_ecs",
+ "bevy_hierarchy",
+ "bevy_math",
+ "bevy_reflect",
+ "bevy_time",
+ "bevy_transform",
+ "bevy_utils",
+]
+
 [[package]]
 name = "bevy_app"
 version = "0.9.1"
@@ -232,7 +250,7 @@ dependencies = [
  "js-sys",
  "ndk-glue",
  "notify",
- "parking_lot",
+ "parking_lot 0.12.1",
  "serde",
  "thiserror",
  "wasm-bindgen",
@@ -240,6 +258,22 @@ dependencies = [
  "web-sys",
 ]
 
+[[package]]
+name = "bevy_audio"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29a05efc6c23bef37520e44029943c65b7e8a4fe4f5e54cb3f96e63ce0b3d361"
+dependencies = [
+ "anyhow",
+ "bevy_app",
+ "bevy_asset",
+ "bevy_ecs",
+ "bevy_reflect",
+ "bevy_utils",
+ "parking_lot 0.12.1",
+ "rodio",
+]
+
 [[package]]
 name = "bevy_core"
 version = "0.9.1"
@@ -364,6 +398,19 @@ dependencies = [
  "encase_derive_impl",
 ]
 
+[[package]]
+name = "bevy_gilrs"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4af552dad82f854b2fae24f36a389fd8ee99d65fe86ae876e854e70d53ff16d9"
+dependencies = [
+ "bevy_app",
+ "bevy_ecs",
+ "bevy_input",
+ "bevy_utils",
+ "gilrs",
+]
+
 [[package]]
 name = "bevy_gltf"
 version = "0.9.1"
@@ -372,6 +419,7 @@ checksum = "e853e346ba412354e02292c7aa5b9a9dccdfa748e273b1b7ebf8f6a172f89712"
 dependencies = [
  "anyhow",
  "base64",
+ "bevy_animation",
  "bevy_app",
  "bevy_asset",
  "bevy_core",
@@ -428,13 +476,16 @@ version = "0.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4c46014b7e885b1311de06b6039e448454a4db55b8d35464798ba88faa186e11"
 dependencies = [
+ "bevy_animation",
  "bevy_app",
  "bevy_asset",
+ "bevy_audio",
  "bevy_core",
  "bevy_core_pipeline",
  "bevy_derive",
  "bevy_diagnostic",
  "bevy_ecs",
+ "bevy_gilrs",
  "bevy_gltf",
  "bevy_hierarchy",
  "bevy_input",
@@ -444,6 +495,7 @@ dependencies = [
  "bevy_ptr",
  "bevy_reflect",
  "bevy_render",
+ "bevy_scene",
  "bevy_sprite",
  "bevy_tasks",
  "bevy_text",
@@ -465,7 +517,7 @@ dependencies = [
  "anyhow",
  "bevy",
  "kira",
- "parking_lot",
+ "parking_lot 0.12.1",
  "thiserror",
 ]
 
@@ -557,7 +609,7 @@ dependencies = [
  "erased-serde",
  "glam",
  "once_cell",
- "parking_lot",
+ "parking_lot 0.12.1",
  "serde",
  "smallvec",
  "thiserror",
@@ -610,7 +662,7 @@ dependencies = [
  "image",
  "naga",
  "once_cell",
- "parking_lot",
+ "parking_lot 0.12.1",
  "regex",
  "serde",
  "smallvec",
@@ -868,6 +920,12 @@ version = "0.6.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
 
+[[package]]
+name = "bit_field"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcb6dd1c2376d2e096796e234a70e17e94cc2d5d54ff8ce42b28cef1d0d359a4"
+
 [[package]]
 name = "bitflags"
 version = "1.3.2"
@@ -1152,12 +1210,12 @@ dependencies = [
  "ndk-context",
  "oboe",
  "once_cell",
- "parking_lot",
+ "parking_lot 0.12.1",
  "stdweb",
  "thiserror",
  "wasm-bindgen",
  "web-sys",
- "windows",
+ "windows 0.37.0",
 ]
 
 [[package]]
@@ -1179,6 +1237,30 @@ dependencies = [
  "crossbeam-utils",
 ]
 
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc"
+dependencies = [
+ "cfg-if",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "crossbeam-utils",
+ "memoffset 0.7.1",
+ "scopeguard",
+]
+
 [[package]]
 name = "crossbeam-utils"
 version = "0.8.14"
@@ -1188,6 +1270,12 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "crunchy"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
+
 [[package]]
 name = "cty"
 version = "0.2.2"
@@ -1240,6 +1328,19 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "dashmap"
+version = "5.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc"
+dependencies = [
+ "cfg-if",
+ "hashbrown",
+ "lock_api",
+ "once_cell",
+ "parking_lot_core 0.9.5",
+]
+
 [[package]]
 name = "discard"
 version = "1.0.4"
@@ -1267,6 +1368,12 @@ version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650"
 
+[[package]]
+name = "either"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
+
 [[package]]
 name = "encase"
 version = "0.4.0"
@@ -1342,6 +1449,21 @@ version = "2.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
 
+[[package]]
+name = "exr"
+version = "1.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eb5f255b5980bb0c8cf676b675d1a99be40f316881444f44e0462eaf5df5ded"
+dependencies = [
+ "bit_field",
+ "flume",
+ "half",
+ "lebe",
+ "miniz_oxide",
+ "smallvec",
+ "threadpool",
+]
+
 [[package]]
 name = "fastrand"
 version = "1.8.0"
@@ -1363,6 +1485,15 @@ dependencies = [
  "windows-sys 0.42.0",
 ]
 
+[[package]]
+name = "find-crate"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2"
+dependencies = [
+ "toml",
+]
+
 [[package]]
 name = "fixedbitset"
 version = "0.4.2"
@@ -1379,6 +1510,19 @@ dependencies = [
  "miniz_oxide",
 ]
 
+[[package]]
+name = "flume"
+version = "0.10.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+ "nanorand",
+ "pin-project",
+ "spin",
+]
+
 [[package]]
 name = "fnv"
 version = "1.0.7"
@@ -1436,6 +1580,12 @@ dependencies = [
  "waker-fn",
 ]
 
+[[package]]
+name = "futures-sink"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9"
+
 [[package]]
 name = "fxhash"
 version = "0.2.1"
@@ -1455,11 +1605,15 @@ dependencies = [
  "bevy_tweening",
  "fastrand",
  "iyes_loopless",
+ "kayak_font",
+ "kayak_ui",
+ "ldtk_rust",
  "log",
  "micro_asset_io",
  "micro_banimate",
  "micro_musicbox",
  "noise",
+ "num-traits",
  "serde",
  "serde_json",
  "thiserror",
@@ -1491,6 +1645,49 @@ dependencies = [
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "gif"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06"
+dependencies = [
+ "color_quant",
+ "weezl",
+]
+
+[[package]]
+name = "gilrs"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d0342acdc7b591d171212e17c9350ca02383b86d5f9af33c6e3598e03a6c57e"
+dependencies = [
+ "fnv",
+ "gilrs-core",
+ "log",
+ "uuid",
+ "vec_map",
+]
+
+[[package]]
+name = "gilrs-core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e4e9fae0d270709ce2c8aacb34f2b7293d4828ce748360ec364ccb7c1585e53"
+dependencies = [
+ "core-foundation",
+ "io-kit-sys",
+ "js-sys",
+ "libc",
+ "libudev-sys",
+ "log",
+ "nix 0.25.0",
+ "uuid",
+ "vec_map",
+ "wasm-bindgen",
+ "web-sys",
+ "windows 0.43.0",
+]
+
 [[package]]
 name = "glam"
 version = "0.22.0"
@@ -1614,6 +1811,15 @@ dependencies = [
  "svg_fmt",
 ]
 
+[[package]]
+name = "half"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad6a9459c9c30b177b925162351f97e7d967c7ea8bab3b8352805327daf45554"
+dependencies = [
+ "crunchy",
+]
+
 [[package]]
 name = "hash32"
 version = "0.2.1"
@@ -1644,6 +1850,15 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "hex"
 version = "0.4.3"
@@ -1681,10 +1896,14 @@ dependencies = [
  "bytemuck",
  "byteorder",
  "color_quant",
+ "exr",
+ "gif",
+ "jpeg-decoder",
  "num-rational",
  "num-traits",
  "png",
  "scoped_threadpool",
+ "tiff",
 ]
 
 [[package]]
@@ -1741,6 +1960,16 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d3b7357d2bbc5ee92f8e899ab645233e43d21407573cceb37fed8bc3dede2c02"
 
+[[package]]
+name = "io-kit-sys"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7789f7f3c9686f96164f5109d69152de759e76e284f736bd57661c6df5091919"
+dependencies = [
+ "core-foundation-sys",
+ "mach",
+]
+
 [[package]]
 name = "itoa"
 version = "1.0.4"
@@ -1788,6 +2017,15 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "jpeg-decoder"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e"
+dependencies = [
+ "rayon",
+]
+
 [[package]]
 name = "js-sys"
 version = "0.3.60"
@@ -1797,6 +2035,54 @@ dependencies = [
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "kayak_font"
+version = "0.1.0"
+source = "git+https://github.com/StarArawn/kayak_ui.git?rev=220694d12a5aeffe680fdaf2a8e5ac3ed9d81ae7#220694d12a5aeffe680fdaf2a8e5ac3ed9d81ae7"
+dependencies = [
+ "anyhow",
+ "bevy",
+ "image",
+ "nanoserde",
+ "num",
+ "num-derive",
+ "num-traits",
+ "ttf-parser",
+ "unicode-segmentation",
+ "xi-unicode",
+]
+
+[[package]]
+name = "kayak_ui"
+version = "0.1.0"
+source = "git+https://github.com/StarArawn/kayak_ui.git?rev=220694d12a5aeffe680fdaf2a8e5ac3ed9d81ae7#220694d12a5aeffe680fdaf2a8e5ac3ed9d81ae7"
+dependencies = [
+ "bevy",
+ "bitflags",
+ "bytemuck",
+ "dashmap",
+ "indexmap",
+ "kayak_font",
+ "kayak_ui_macros",
+ "log",
+ "morphorm",
+ "reorder",
+ "resources",
+]
+
+[[package]]
+name = "kayak_ui_macros"
+version = "0.1.0"
+source = "git+https://github.com/StarArawn/kayak_ui.git?rev=220694d12a5aeffe680fdaf2a8e5ac3ed9d81ae7#220694d12a5aeffe680fdaf2a8e5ac3ed9d81ae7"
+dependencies = [
+ "find-crate",
+ "proc-macro-crate",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "khronos-egl"
 version = "4.1.0"
@@ -1852,6 +2138,33 @@ version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
 
+[[package]]
+name = "ldtk_rust"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e6ec9d145e9e4265fc8d473114353c07ab84a21ea1e1ad7c86ba7fc6337aebe"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "lebe"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
+
+[[package]]
+name = "lewton"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030"
+dependencies = [
+ "byteorder",
+ "ogg",
+ "tinyvec",
+]
+
 [[package]]
 name = "libc"
 version = "0.2.137"
@@ -1868,6 +2181,16 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "libudev-sys"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324"
+dependencies = [
+ "libc",
+ "pkg-config",
+]
+
 [[package]]
 name = "lock_api"
 version = "0.4.9"
@@ -1949,6 +2272,15 @@ dependencies = [
  "autocfg",
 ]
 
+[[package]]
+name = "memoffset"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4"
+dependencies = [
+ "autocfg",
+]
+
 [[package]]
 name = "metal"
 version = "0.24.0"
@@ -2030,6 +2362,16 @@ dependencies = [
  "windows-sys 0.42.0",
 ]
 
+[[package]]
+name = "morphorm"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43f33fad4a772050ad1cac8d7add5f1e73ac2a3d581120bbdbe472fbc0c31163"
+dependencies = [
+ "bitflags",
+ "smallvec",
+]
+
 [[package]]
 name = "naga"
 version = "0.10.0"
@@ -2052,6 +2394,30 @@ dependencies = [
  "unicode-xid",
 ]
 
+[[package]]
+name = "nanorand"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
+dependencies = [
+ "getrandom 0.2.8",
+]
+
+[[package]]
+name = "nanoserde"
+version = "0.1.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "755e7965536bc54d7c9fba2df5ada5bf835b0443fd613f0a53fa199a301839d3"
+dependencies = [
+ "nanoserde-derive",
+]
+
+[[package]]
+name = "nanoserde-derive"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed7a94da6c6181c35d043fc61c43ac96d3a5d739e7b8027f77650ba41504d6ab"
+
 [[package]]
 name = "ndk"
 version = "0.6.0"
@@ -2099,7 +2465,7 @@ dependencies = [
  "ndk-macro",
  "ndk-sys 0.4.1+23.1.7779620",
  "once_cell",
- "parking_lot",
+ "parking_lot 0.12.1",
 ]
 
 [[package]]
@@ -2143,7 +2509,7 @@ dependencies = [
  "cc",
  "cfg-if",
  "libc",
- "memoffset",
+ "memoffset 0.6.5",
 ]
 
 [[package]]
@@ -2155,7 +2521,7 @@ dependencies = [
  "bitflags",
  "cfg-if",
  "libc",
- "memoffset",
+ "memoffset 0.6.5",
 ]
 
 [[package]]
@@ -2168,7 +2534,7 @@ dependencies = [
  "bitflags",
  "cfg-if",
  "libc",
- "memoffset",
+ "memoffset 0.6.5",
 ]
 
 [[package]]
@@ -2220,6 +2586,40 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "num"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606"
+dependencies = [
+ "num-bigint",
+ "num-complex",
+ "num-integer",
+ "num-iter",
+ "num-rational",
+ "num-traits",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-complex"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ae39348c8bc5fbd7f40c727a9925f03517afd2ab27d46702108b6a7e5414c19"
+dependencies = [
+ "num-traits",
+]
+
 [[package]]
 name = "num-derive"
 version = "0.3.3"
@@ -2241,6 +2641,17 @@ dependencies = [
  "num-traits",
 ]
 
+[[package]]
+name = "num-iter"
+version = "0.1.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
 [[package]]
 name = "num-rational"
 version = "0.4.1"
@@ -2248,6 +2659,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
 dependencies = [
  "autocfg",
+ "num-bigint",
  "num-integer",
  "num-traits",
 ]
@@ -2261,6 +2673,16 @@ dependencies = [
  "autocfg",
 ]
 
+[[package]]
+name = "num_cpus"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
 [[package]]
 name = "num_enum"
 version = "0.5.7"
@@ -2324,6 +2746,15 @@ dependencies = [
  "cc",
 ]
 
+[[package]]
+name = "ogg"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e"
+dependencies = [
+ "byteorder",
+]
+
 [[package]]
 name = "once_cell"
 version = "1.16.0"
@@ -2351,6 +2782,17 @@ version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72"
 
+[[package]]
+name = "parking_lot"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
+dependencies = [
+ "instant",
+ "lock_api",
+ "parking_lot_core 0.8.5",
+]
+
 [[package]]
 name = "parking_lot"
 version = "0.12.1"
@@ -2358,7 +2800,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
 dependencies = [
  "lock_api",
- "parking_lot_core",
+ "parking_lot_core 0.9.5",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
+dependencies = [
+ "cfg-if",
+ "instant",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "winapi",
 ]
 
 [[package]]
@@ -2396,6 +2852,26 @@ dependencies = [
  "indexmap",
 ]
 
+[[package]]
+name = "pin-project"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "pin-project-lite"
 version = "0.2.9"
@@ -2446,6 +2922,30 @@ dependencies = [
  "toml",
 ]
 
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
 [[package]]
 name = "proc-macro2"
 version = "1.0.47"
@@ -2550,6 +3050,29 @@ dependencies = [
  "cty",
 ]
 
+[[package]]
+name = "rayon"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e060280438193c554f654141c9ea9417886713b7acd75974c85b18a69a88e0b"
+dependencies = [
+ "crossbeam-deque",
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cac410af5d00ab6884528b4ab69d1e8e146e8d471201800fa1b4524126de6ad3"
+dependencies = [
+ "crossbeam-channel",
+ "crossbeam-deque",
+ "crossbeam-utils",
+ "num_cpus",
+]
+
 [[package]]
 name = "rectangle-pack"
 version = "0.4.2"
@@ -2597,6 +3120,23 @@ version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f1382d1f0a252c4bf97dc20d979a2fdd05b024acd7c2ed0f7595d7817666a157"
 
+[[package]]
+name = "reorder"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd6672031e07dc5b839bd8be4d950eec4575e29996b538c0e96043c46bc5b56d"
+
+[[package]]
+name = "resources"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42070ea13709eb92d2977b48c7d3bd44866fa328e14248f8d1f00d6ea14d5066"
+dependencies = [
+ "downcast-rs",
+ "fxhash",
+ "parking_lot 0.11.2",
+]
+
 [[package]]
 name = "ringbuf"
 version = "0.3.1"
@@ -2606,6 +3146,16 @@ dependencies = [
  "crossbeam-utils",
 ]
 
+[[package]]
+name = "rodio"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb10b653d5ec0e9411a2e7d46e2c7f4046fd87d35b9955bd73ba4108d69072b5"
+dependencies = [
+ "cpal",
+ "lewton",
+]
+
 [[package]]
 name = "ron"
 version = "0.8.0"
@@ -2787,6 +3337,15 @@ dependencies = [
  "wayland-protocols",
 ]
 
+[[package]]
+name = "spin"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09"
+dependencies = [
+ "lock_api",
+]
+
 [[package]]
 name = "spirv"
 version = "0.2.0+1.5.4"
@@ -2987,6 +3546,41 @@ dependencies = [
  "once_cell",
 ]
 
+[[package]]
+name = "threadpool"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa"
+dependencies = [
+ "num_cpus",
+]
+
+[[package]]
+name = "tiff"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f17def29300a156c19ae30814710d9c63cd50288a49c6fd3a10ccfbe4cf886fd"
+dependencies = [
+ "flate2",
+ "jpeg-decoder",
+ "weezl",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+
 [[package]]
 name = "toml"
 version = "0.5.9"
@@ -3087,6 +3681,12 @@ version = "1.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
 
+[[package]]
+name = "unicode-segmentation"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a"
+
 [[package]]
 name = "unicode-width"
 version = "0.1.10"
@@ -3304,6 +3904,12 @@ dependencies = [
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "weezl"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb"
+
 [[package]]
 name = "wgpu"
 version = "0.14.2"
@@ -3314,7 +3920,7 @@ dependencies = [
  "js-sys",
  "log",
  "naga",
- "parking_lot",
+ "parking_lot 0.12.1",
  "raw-window-handle 0.5.0",
  "smallvec",
  "static_assertions",
@@ -3340,7 +3946,7 @@ dependencies = [
  "fxhash",
  "log",
  "naga",
- "parking_lot",
+ "parking_lot 0.12.1",
  "profiling",
  "raw-window-handle 0.5.0",
  "smallvec",
@@ -3376,7 +3982,7 @@ dependencies = [
  "metal",
  "naga",
  "objc",
- "parking_lot",
+ "parking_lot 0.12.1",
  "profiling",
  "range-alloc",
  "raw-window-handle 0.5.0",
@@ -3442,6 +4048,21 @@ dependencies = [
  "windows_x86_64_msvc 0.37.0",
 ]
 
+[[package]]
+name = "windows"
+version = "0.43.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc 0.42.0",
+ "windows_i686_gnu 0.42.0",
+ "windows_i686_msvc 0.42.0",
+ "windows_x86_64_gnu 0.42.0",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc 0.42.0",
+]
+
 [[package]]
 name = "windows-sys"
 version = "0.36.1"
@@ -3591,7 +4212,7 @@ dependencies = [
  "ndk-glue",
  "objc",
  "once_cell",
- "parking_lot",
+ "parking_lot 0.12.1",
  "percent-encoding",
  "raw-window-handle 0.4.3",
  "raw-window-handle 0.5.0",
diff --git a/Makefile b/Makefile
index 3d8c38951e82535b0e1a6890b832a3297a239fb8..d6226624a5fd57e7b658cdb15e92f518dd766931 100644
--- a/Makefile
+++ b/Makefile
@@ -33,6 +33,9 @@ check:
 pak:
 	cd raw_assets && tar -cJf ../assets/resources.apack ./
 
+msdf:
+	msdf-atlas-gen -font raw_assets/fonts/CompassPro.ttf -type msdf -minsize 18 -format png -imageout raw_assets/fonts/CompassPro.png -json raw_assets/fonts/CompassPro.kayak_font
+
 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/resources.apack b/assets/resources.apack
index 1f14276a8d0e7aeeac57a6708122b23946fdcd1c..b5f38dd40c9edcb6b709447ec6c4d9140f8ccf35 100644
--- a/assets/resources.apack
+++ b/assets/resources.apack
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:e279a1551f7c02cfba8b16f358e4bf8ae62065e97d511f935984f5548a20fbf5
-size 49928
+oid sha256:dcb87c1d8969f1007611545f2b08107140ca49189883bc51fc1ac7d4b3e8d73a
+size 105084
diff --git a/game_core/Cargo.toml b/game_core/Cargo.toml
index 59880345d99d8b513f08625262388a0c0b16ea1c..8fc672658d2982bc0f63bc2eed7ab2c81ef560b2 100644
--- a/game_core/Cargo.toml
+++ b/game_core/Cargo.toml
@@ -20,11 +20,16 @@ micro_banimate.workspace = true
 micro_musicbox.workspace = true
 
 micro_asset_io = { path = "../micro_asset_io" }
+num-traits = "0.2.15"
 
 bevy_ecs_tilemap = { git = "https://github.com/StarArawn/bevy_ecs_tilemap", rev = "eb20fcaccdd253ea5bf280cac7ffc5a69b674df2" }
 bevy_tweening = "0.6.0"
 toml = "0.5.9"
 noise = "0.8.2"
+ldtk_rust = "0.6.0"
+
+kayak_ui.workspace = true
+kayak_font.workspace = true
 
 #remote_events = { git = "https://lab.lcr.gr/microhacks/micro-bevy-remote-events.git", rev = "be0c6b43a73e4c5e7ece20797e3d6f59340147b4"}
 
diff --git a/game_core/src/assets/apack_handler.rs b/game_core/src/assets/apack_handler.rs
index ea3e271e2ac612e24c7003c90ff3f171c37dbc24..422ad53e118acd33da5c8eeefcc40ef1124ba931 100644
--- a/game_core/src/assets/apack_handler.rs
+++ b/game_core/src/assets/apack_handler.rs
@@ -12,9 +12,12 @@ use bevy::ecs::system::{SystemParam, SystemParamFetch, SystemState};
 use bevy::math::vec2;
 use bevy::prelude::*;
 use bevy::render::texture::{CompressedImageFormats, ImageType};
+use kayak_font::{KayakFont, Sdf};
+use ldtk_rust::Project;
 use micro_asset_io::{APack, APackProcessingComplete};
 use serde::{Deserialize, Serialize};
 
+use crate::assets::asset_types::ldtk_project::LdtkProject;
 use crate::assets::AssetHandles;
 
 fn file_prefix(path: &PathBuf) -> Option<String> {
@@ -47,6 +50,21 @@ pub struct ManifestGenericAsset {
 	pub path: String,
 }
 
+#[derive(Deserialize, Serialize, Debug)]
+#[serde(untagged)]
+pub enum ManifestFontAsset {
+	Basic {
+		name: String,
+		path: String,
+	},
+	MSDF {
+		name: String,
+		ttf: String,
+		image: String,
+		msdf: String,
+	},
+}
+
 #[derive(Deserialize, Serialize, Debug)]
 pub struct ManifestImage {
 	pub name: String,
@@ -75,7 +93,9 @@ pub struct APackManifest {
 	#[serde(default = "Vec::new")]
 	pub spritesheets: Vec<ManifestSpriteSheet>,
 	#[serde(default = "Vec::new")]
-	pub fonts: Vec<ManifestGenericAsset>,
+	pub fonts: Vec<ManifestFontAsset>,
+	#[serde(default = "Vec::new")]
+	pub ldtk: Vec<ManifestGenericAsset>,
 }
 
 fn in_world<Param: SystemParam + 'static>(
@@ -157,23 +177,75 @@ pub fn handle_apack_process_events(world: &mut World) {
 			}
 
 			for font in &manifest.fonts {
-				let asset = pack
-					.get(&format!("./{}", &font.path))
-					.expect("Missing asset");
+				match font {
+					ManifestFontAsset::Basic { path, name } => {
+						let asset = pack.get(&format!("./{}", &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(name.clone(), handle);
+								},
+							);
+						} else {
+							log::warn!("Malformed font {}", path);
+						}
+					}
+					ManifestFontAsset::MSDF {
+						name,
+						ttf,
+						msdf,
+						image,
+					} => {
+						let image_asset =
+							pack.get(&format!("./{}", &image)).expect("Missing asset");
+						let ttf_asset = pack.get(&format!("./{}", &ttf)).expect("Missing asset");
+						let msdf_asset = pack.get(&format!("./{}", &msdf)).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);
+						in_world::<
+							ParamSet<(
+								ResMut<Assets<Font>>,
+								ResMut<Assets<Image>>,
+								ResMut<Assets<KayakFont>>,
+								ResMut<AssetHandles>,
+							)>,
+						>(
+							world,
+							move |params: &mut ParamSet<(
+								ResMut<Assets<Font>>,
+								ResMut<Assets<Image>>,
+								ResMut<Assets<KayakFont>>,
+								ResMut<AssetHandles>,
+							)>| {
+								let ttf_handle = params
+									.p0()
+									.add(Font::try_from_bytes(ttf_asset.to_vec()).unwrap());
+
+								let image_handle = params.p1().add(
+									Image::from_buffer(
+										image_asset.as_slice(),
+										ImageType::Extension("png"),
+										CompressedImageFormats::all(),
+										true,
+									)
+									.unwrap(),
+								);
+								let msdf_handle = params.p2().add(KayakFont::new(
+									Sdf::from_bytes(msdf_asset.as_slice()),
+									kayak_font::ImageType::Atlas(image_handle.clone_weak()),
+								));
+
+								let mut assets = params.p3();
+								assets.kayak_fonts.insert(name.clone(), msdf_handle);
+								assets.images.insert(name.clone(), image_handle);
+								assets.fonts.insert(name.clone(), ttf_handle);
+							},
+						)
+					}
 				}
 			}
 
@@ -221,6 +293,33 @@ pub fn handle_apack_process_events(world: &mut World) {
 					log::warn!("Malformed spritesheet {}", &spritesheet.path);
 				}
 			}
+
+			for ldtk_entry in &manifest.ldtk {
+				let asset = pack
+					.get(&format!("./{}", &ldtk_entry.path))
+					.expect("Missing asset");
+
+				let mut ldtk_file: Project = serde_json::from_slice(asset.as_slice()).unwrap();
+				in_world::<ParamSet<(ResMut<Assets<LdtkProject>>, ResMut<AssetHandles>)>>(
+					world,
+					move |params: &mut ParamSet<(
+						ResMut<Assets<LdtkProject>>,
+						ResMut<AssetHandles>,
+					)>| {
+						let handle = params.p0().add(LdtkProject(ldtk_file));
+						params.p1().ldtk.insert(ldtk_entry.name.clone(), handle);
+					},
+				);
+			}
+		}
+
+		log::info!("LOADED AN APACK");
+		{
+			let mut state: SystemState<ResMut<Assets<APack>>> = SystemState::new(world);
+			let mut packs = state.get_mut(world);
+			if let Some(mut pack) = packs.get_mut(&event.0) {
+				pack.loaded = true
+			}
 		}
 	}
 }
diff --git a/game_core/src/assets/asset_types/ldtk_project.rs b/game_core/src/assets/asset_types/ldtk_project.rs
new file mode 100644
index 0000000000000000000000000000000000000000..7135d2e88647d72ab89229c8dd2d5d96189d3a01
--- /dev/null
+++ b/game_core/src/assets/asset_types/ldtk_project.rs
@@ -0,0 +1,84 @@
+use std::collections::HashMap;
+use std::ops::{Deref, DerefMut};
+
+use anyhow::Error;
+use bevy::asset::{AssetEvent, AssetLoader, Assets, BoxedFuture, LoadContext, LoadedAsset};
+use bevy::prelude::{EventReader, Res, ResMut, Resource};
+use bevy::reflect::TypeUuid;
+use ldtk_rust::{Level, Project};
+
+#[derive(TypeUuid)]
+#[uuid = "eb97c508-61ea-11ed-abd3-db91dd0a6e8b"]
+pub struct LdtkProject(pub Project);
+impl Deref for LdtkProject {
+	type Target = Project;
+	fn deref(&self) -> &Self::Target {
+		&self.0
+	}
+}
+impl DerefMut for LdtkProject {
+	fn deref_mut(&mut self) -> &mut Self::Target {
+		&mut self.0
+	}
+}
+
+impl LdtkProject {}
+
+#[derive(Default)]
+pub struct LdtkLoader;
+impl AssetLoader for LdtkLoader {
+	fn load<'a>(
+		&'a self,
+		bytes: &'a [u8],
+		load_context: &'a mut LoadContext,
+	) -> BoxedFuture<'a, anyhow::Result<(), Error>> {
+		Box::pin(async move {
+			let mut ldtk: Project = serde_json::from_slice(bytes)?;
+			if ldtk.external_levels {
+				ldtk.load_external_levels(load_context.path());
+			}
+			load_context.set_default_asset(LoadedAsset::new(LdtkProject(ldtk)));
+			Ok(())
+		})
+	}
+
+	fn extensions(&self) -> &[&str] {
+		&["ldtk"]
+	}
+}
+
+#[derive(Resource, Default)]
+pub struct LevelIndex(pub HashMap<String, Level>);
+impl Deref for LevelIndex {
+	type Target = HashMap<String, Level>;
+	fn deref(&self) -> &Self::Target {
+		&self.0
+	}
+}
+impl DerefMut for LevelIndex {
+	fn deref_mut(&mut self) -> &mut Self::Target {
+		&mut self.0
+	}
+}
+
+pub fn handle_ldtk_project_events(
+	mut events: EventReader<AssetEvent<LdtkProject>>,
+	assets: Res<Assets<LdtkProject>>,
+	mut level_index: ResMut<LevelIndex>,
+) {
+	for event in events.iter() {
+		match event {
+			AssetEvent::Created { handle } | AssetEvent::Modified { handle } => {
+				if let Some(LdtkProject(project)) = assets.get(handle) {
+					for level in &project.levels {
+						level_index.insert(
+							level.identifier.clone(),
+							serde_json::from_value(serde_json::to_value(level).unwrap()).unwrap(),
+						);
+					}
+				}
+			}
+			_ => {}
+		}
+	}
+}
diff --git a/game_core/src/assets/asset_types/mod.rs b/game_core/src/assets/asset_types/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..fc46ae21f63dbdec25ed5b559b89c5ebb3ae6276
--- /dev/null
+++ b/game_core/src/assets/asset_types/mod.rs
@@ -0,0 +1 @@
+pub mod ldtk_project;
diff --git a/game_core/src/assets/loader.rs b/game_core/src/assets/loader.rs
index e725fbbb70a446d7c4c8ac3a5bcf9f6ace166c8d..cb7994a826dfd21513e9eecfcc7faf6334ae6719 100644
--- a/game_core/src/assets/loader.rs
+++ b/game_core/src/assets/loader.rs
@@ -4,9 +4,11 @@ use bevy::asset::LoadState;
 use bevy::ecs::system::SystemParam;
 use bevy::prelude::*;
 use bevy::reflect::TypeUuid;
+use kayak_font::KayakFont;
 use micro_asset_io::APack;
 use micro_musicbox::prelude::AudioSource;
 
+use crate::assets::asset_types::ldtk_project::LdtkProject;
 use crate::assets::{AssetHandles, FixedAssetNameMapping, SpriteSheetConfig};
 
 #[derive(SystemParam)]
@@ -14,6 +16,7 @@ pub struct AssetTypeLoader<'w, 's> {
 	pub handles: ResMut<'w, AssetHandles>,
 	pub asset_server: Res<'w, AssetServer>,
 	pub atlas: ResMut<'w, Assets<TextureAtlas>>,
+	pub apack: ResMut<'w, Assets<APack>>,
 	#[system_param(ignore)]
 	marker: PhantomData<&'s usize>,
 }
@@ -34,7 +37,7 @@ macro_rules! load_state {
 	($container: expr => $key: ident) => {
 		$container
 			.asset_server
-			.get_group_load_state($container.handles.$key.values().map(|f| f.id))
+			.get_group_load_state($container.handles.$key.values().map(|f| f.id()))
 	};
 }
 
@@ -57,6 +60,8 @@ impl<'w, 's> AssetTypeLoader<'w, 's> {
 	load_basic_type!(load_audio, AudioSource => sounds);
 	load_basic_type!(load_font, Font => fonts);
 	load_basic_type!(load_apack, APack => apacks);
+	load_basic_type!(load_ldtk, LdtkProject => ldtk);
+	load_basic_type!(load_kayak_font, KayakFont => kayak_fonts);
 
 	pub fn load_spritesheet(
 		&mut self,
@@ -97,4 +102,14 @@ impl<'w, 's> AssetTypeLoader<'w, 's> {
 
 		vec![image_state, atlas_state]
 	}
+
+	pub fn check_apack_process_status(&self) -> bool {
+		self.handles
+			.apacks
+			.values()
+			.all(|handle| match self.apack.get(handle) {
+				Some(pack) => pack.processed && pack.loaded,
+				_ => false,
+			})
+	}
 }
diff --git a/game_core/src/assets/mod.rs b/game_core/src/assets/mod.rs
index 37ce6f9abbe8fee11a395aedeaf126468ec8f68b..e5443711f0b5305384b596a19dcbb4e488e63d09 100644
--- a/game_core/src/assets/mod.rs
+++ b/game_core/src/assets/mod.rs
@@ -1,30 +1,32 @@
 mod apack_handler;
+mod asset_types;
 mod loader;
 mod resources;
 mod startup;
 
+pub use asset_types::ldtk_project::{LdtkLoader, LdtkProject, LevelIndex};
 use bevy::app::{App, Plugin};
+use bevy::prelude::AddAsset;
 use iyes_loopless::condition::ConditionSet;
 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::assets::asset_types::ldtk_project::handle_ldtk_project_events;
 use crate::system::flow::AppState;
 
 pub struct AssetsPlugin;
 impl Plugin for AssetsPlugin {
 	fn build(&self, app: &mut App) {
 		app.init_resource::<AssetHandles>()
+			.init_resource::<LevelIndex>()
+			.add_asset::<LdtkProject>()
+			.add_asset_loader(LdtkLoader)
 			.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(handle_ldtk_project_events)
 			.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 3b317e0475cd6e75bf4334f37db84a36a12772e6..459d6ef797b678516f144b19112efa4610ba0ea7 100644
--- a/game_core/src/assets/resources.rs
+++ b/game_core/src/assets/resources.rs
@@ -1,9 +1,12 @@
 use bevy::prelude::*;
 use bevy::utils::HashMap;
+use kayak_font::KayakFont;
 use micro_asset_io::APack;
 use micro_musicbox::prelude::AudioSource;
 use micro_musicbox::utilities::{SuppliesAudio, TrackType};
 
+use crate::assets::asset_types::ldtk_project::LdtkProject;
+
 #[derive(Copy, Clone, Debug)]
 pub struct SpriteSheetConfig {
 	pub tile_width: usize,
@@ -39,6 +42,8 @@ pub struct AssetHandles {
 	pub sounds: HashMap<String, Handle<AudioSource>>,
 	pub fonts: HashMap<String, Handle<Font>>,
 	pub apacks: HashMap<String, Handle<APack>>,
+	pub ldtk: HashMap<String, Handle<LdtkProject>>,
+	pub kayak_fonts: HashMap<String, Handle<KayakFont>>,
 }
 
 macro_rules! fetch_wrapper {
@@ -69,6 +74,8 @@ impl AssetHandles {
 	fetch_wrapper!(sound, AudioSource => sounds);
 	fetch_wrapper!(font, Font => fonts);
 	fetch_wrapper!(apack, APack => apacks);
+	fetch_wrapper!(ldtk, LdtkProject => ldtk);
+	fetch_wrapper!(kayak_font, KayakFont => kayak_fonts);
 }
 
 impl SuppliesAudio for AssetHandles {
diff --git a/game_core/src/assets/startup.rs b/game_core/src/assets/startup.rs
index 18641088458aee209dc9716d4f0420b1b0b0df9f..8fd402053625ec50868410678ac35078e500cb1e 100644
--- a/game_core/src/assets/startup.rs
+++ b/game_core/src/assets/startup.rs
@@ -19,7 +19,8 @@ pub fn start_load_resources(mut loader: AssetTypeLoader) {
 
 pub fn check_load_resources(mut commands: Commands, loader: AssetTypeLoader) {
 	let load_states = loader.get_all_load_state();
-	if load_states.iter().all(|state| *state == LoadState::Loaded) {
+
+	if loader.check_apack_process_status() {
 		log::info!("Assets loaded successfully");
 		commands.insert_resource(NextState(AppState::Splash))
 	}
diff --git a/game_core/src/graphics/mod.rs b/game_core/src/graphics/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..02567b164e2f057ddc0d31236d77cbf46225df2f
--- /dev/null
+++ b/game_core/src/graphics/mod.rs
@@ -0,0 +1,34 @@
+use bevy::prelude::*;
+
+#[derive(Component, Copy, Clone)]
+pub struct AlignedBackground;
+
+pub fn adjust_aligned_backgrounds(
+	mut query: Query<(&mut Transform, &Handle<Image>), With<AlignedBackground>>,
+	windows: Res<Windows>,
+	images: Res<Assets<Image>>,
+) {
+	if let Some(window) = windows.get_primary() {
+		let width = window.width();
+		let height = window.height();
+
+		for (mut transform, handle) in &mut query {
+			if let Some(image) = images.get(handle) {
+				if width > height {
+					let scale = image.texture_descriptor.size.width as f32 / width;
+					transform.scale = Vec3::splat(scale);
+				} else {
+					let scale = image.texture_descriptor.size.height as f32 / height;
+					transform.scale = Vec3::splat(scale);
+				}
+			}
+		}
+	}
+}
+
+pub struct GraphicsPlugin;
+impl Plugin for GraphicsPlugin {
+	fn build(&self, app: &mut App) {
+		app.add_system(adjust_aligned_backgrounds);
+	}
+}
diff --git a/game_core/src/lib.rs b/game_core/src/lib.rs
index d487f56ae4e6cfd195d84d3fa124ee1fe6d0fec9..97f8201f9a76fef42fbb3884b22251854ab5f70e 100644
--- a/game_core/src/lib.rs
+++ b/game_core/src/lib.rs
@@ -1,8 +1,8 @@
-extern crate core;
-
 pub mod assets;
+pub mod graphics;
 pub mod multiplayer;
 pub mod splash_screen;
 pub mod states;
 pub mod system;
+pub mod ui;
 pub mod world;
diff --git a/game_core/src/main.rs b/game_core/src/main.rs
index f41b9b4ffec62f4b80e41baa2c8d82a9ed3937c5..92cafa4914079ebd706afc0613edf057818c7fa0 100644
--- a/game_core/src/main.rs
+++ b/game_core/src/main.rs
@@ -3,6 +3,7 @@ use bevy_ecs_tilemap::TilemapPlugin;
 use game_core::assets::AssetHandles;
 use game_core::system::flow::AppState;
 use game_core::system::resources::InitAppPlugins;
+use game_core::ui::AdventUIPlugins;
 use iyes_loopless::prelude::AppLooplessStateExt;
 use micro_musicbox::CombinedAudioPlugins;
 
@@ -19,5 +20,7 @@ fn main() {
 		.add_plugin(bevy_tweening::TweeningPlugin)
 		.add_plugin(game_core::world::WorldPlugin)
 		.add_plugin(TilemapPlugin)
+		.add_plugin(game_core::graphics::GraphicsPlugin)
+		.add_plugins(AdventUIPlugins)
 		.run();
 }
diff --git a/game_core/src/states/game_state.rs b/game_core/src/states/game_state.rs
new file mode 100644
index 0000000000000000000000000000000000000000..6ffe00fa7b214c0c3bdc6769388de9b072664c09
--- /dev/null
+++ b/game_core/src/states/game_state.rs
@@ -0,0 +1,4 @@
+use bevy::prelude::Component;
+
+#[derive(Component, Debug, Default, Copy, Clone)]
+pub struct Player;
diff --git a/game_core/src/states/menu_state.rs b/game_core/src/states/menu_state.rs
index 23ab273af1a8ac5c9b35a3e6774fd3f70079e2bd..cf9891ce4a751ee94c203c4503ccecaa5c5a8d3b 100644
--- a/game_core/src/states/menu_state.rs
+++ b/game_core/src/states/menu_state.rs
@@ -6,6 +6,7 @@ use bevy_tweening::{Animator, EaseFunction, RepeatCount, RepeatStrategy, Tween};
 use iyes_loopless::state::NextState;
 
 use crate::assets::AssetHandles;
+use crate::graphics::AlignedBackground;
 use crate::system::flow::AppState;
 
 #[derive(Component)]
@@ -39,7 +40,7 @@ pub fn spawn_menu_entities(mut commands: Commands, assets: Res<AssetHandles>) {
 					"Trader Tales",
 					TextStyle {
 						font_size: 72.0,
-						font: assets.font("default"),
+						font: assets.font("compass_pro"),
 						color: Color::ANTIQUE_WHITE,
 					},
 				),
@@ -55,7 +56,7 @@ pub fn spawn_menu_entities(mut commands: Commands, assets: Res<AssetHandles>) {
 						"> Press Space <",
 						TextStyle {
 							font_size: 48.0,
-							font: assets.font("default"),
+							font: assets.font("compass_pro"),
 							color: Color::ANTIQUE_WHITE,
 						},
 					),
diff --git a/game_core/src/states/mod.rs b/game_core/src/states/mod.rs
index a75b179f11527d0472451760455a3929f6957d0a..01a9f906629e6a20cfc43a3491d57e0ab1c8803d 100644
--- a/game_core/src/states/mod.rs
+++ b/game_core/src/states/mod.rs
@@ -4,6 +4,7 @@ use iyes_loopless::prelude::{AppLooplessStateExt, ConditionSet};
 use crate::states::menu_state::go_to_game;
 use crate::system::flow::AppState;
 
+mod game_state;
 mod menu_state;
 
 pub struct StatesPlugin;
@@ -19,3 +20,5 @@ impl Plugin for StatesPlugin {
 			);
 	}
 }
+
+pub use game_state::Player;
diff --git a/game_core/src/system/camera.rs b/game_core/src/system/camera.rs
index 072a1856af668db76a8143c1461f1325d7fd2867..0084e8b46788ea953ebf2a70a1a79b49e07ddb29 100644
--- a/game_core/src/system/camera.rs
+++ b/game_core/src/system/camera.rs
@@ -1,16 +1,18 @@
 use bevy::app::App;
-use bevy::input::mouse::MouseMotion;
-use bevy::math::{Vec2, Vec3Swizzles};
+use bevy::input::mouse::{MouseMotion, MouseWheel};
+use bevy::math::{vec3, Vec2, Vec3Swizzles};
 use bevy::prelude::{
-	Camera2dBundle, Commands, Component, CoreStage, Entity, EventReader, Input, MouseButton,
-	OrthographicProjection, Plugin, Projection, Query, Res, Transform, Windows, With,
+	Camera2dBundle, Commands, Component, CoreStage, Entity, EventReader, Input, Local, MouseButton,
+	OrthographicProjection, Plugin, Projection, Query, Res, Transform, Windows, With, Without,
 };
 use bevy::render::camera::ScalingMode;
 use iyes_loopless::prelude::{AppLooplessStateExt, ConditionSet};
 
 use crate::system::flow::AppState;
 use crate::system::load_config::virtual_size;
+use crate::system::utilities::{f32_max, f32_min};
 use crate::system::window::WindowManager;
+use crate::world::MapQuery;
 
 /// A flag component to indicate which entity should be followed by the camera
 #[derive(Component)]
@@ -43,20 +45,33 @@ pub fn spawn_orthographic_camera(mut commands: Commands) {
 
 pub fn pan_camera(
 	mut ev_motion: EventReader<MouseMotion>,
+	mut ev_scroll: EventReader<MouseWheel>,
 	input_mouse: Res<Input<MouseButton>>,
-	mut query: Query<(&mut Transform, &Projection), With<GameCamera>>,
+	mut query: Query<(&mut Transform, &OrthographicProjection), With<GameCamera>>,
 	windows: Res<Windows>,
+	mut last_mouse_pos: Local<Vec2>,
 ) {
 	let mut pan = Vec2::ZERO;
 
-	if input_mouse.pressed(MouseButton::Right) {
-		for ev in ev_motion.iter() {
-			pan += ev.delta;
-		}
-	}
+	if let Some(mouse) = windows
+		.get_primary()
+		.and_then(|window| window.cursor_position())
+	{
+		let previous_mouse = std::mem::replace(&mut *last_mouse_pos, mouse);
+
+		let mouse_diff = mouse - previous_mouse;
 
-	for (mut transform, _) in &mut query {
-		if pan.length_squared() > 0.0 {}
+		if input_mouse.pressed(MouseButton::Right) {
+			for ev in ev_motion.iter() {
+				pan += ev.delta;
+			}
+
+			if mouse_diff != Vec2::ZERO && mouse_diff.x < 50.0 && mouse_diff.y < 50.0 {
+				for (mut transform, projection) in &mut query {
+					transform.translation -= mouse_diff.extend(0.0);
+				}
+			}
+		}
 	}
 }
 
@@ -67,8 +82,12 @@ pub fn pan_camera(
 /// by updated to the midpoint between the player and the mouse
 pub fn sync_chase_camera_location(
 	mut commands: Commands,
-	chased_query: Query<&Transform, With<ChaseCam>>,
-	camera_query: Query<(Entity, &Transform), With<GameCamera>>,
+	chased_query: Query<&Transform, (With<ChaseCam>, Without<GameCamera>)>,
+	mut camera_query: Query<
+		(Entity, &mut Transform, &OrthographicProjection),
+		(With<GameCamera>, Without<ChaseCam>),
+	>,
+	map_query: MapQuery,
 ) {
 	if chased_query.is_empty() {
 		return;
@@ -85,11 +104,26 @@ pub fn sync_chase_camera_location(
 		average_location /= count as f32;
 	}
 
-	for (entity, location) in camera_query.iter() {
-		commands.entity(entity).insert(Transform {
-			translation: average_location.extend(location.translation.z),
-			..*location
-		});
+	for (_, mut location, proj) in camera_query.iter_mut() {
+		if let Some(bounds) = map_query.get_camera_bounds() {
+			// log::info!("BOUNDS {:?}", bounds);
+
+			let width = proj.right - proj.left;
+			let height = proj.top - proj.bottom;
+
+			let val_x = f32_max(
+				bounds.get_min_x(width),
+				f32_min(bounds.get_max_x(width), average_location.x),
+			);
+			let val_y = f32_max(
+				bounds.get_min_y(height),
+				f32_min(bounds.get_max_y(height), average_location.y),
+			);
+
+			location.translation = vec3(val_x, val_y, location.translation.z);
+		} else {
+			location.translation = average_location.extend(location.translation.z);
+		}
 	}
 }
 
diff --git a/game_core/src/system/load_config.rs b/game_core/src/system/load_config.rs
index a989503b5a093d3b463381b4d121f5efc29be91f..46ed6f71b6c8fbb0ee06122ad2c54ac04bc16fb6 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) {
-		(640.0, 360.0)
+		(320.0, 180.0)
 	}
 }
 
diff --git a/game_core/src/system/utilities.rs b/game_core/src/system/utilities.rs
index 180bb4226b0a162371f898f14644acc439fd5f39..1f4517c03498b76d829bf61502ee19c69819d153 100644
--- a/game_core/src/system/utilities.rs
+++ b/game_core/src/system/utilities.rs
@@ -1,6 +1,7 @@
 use std::ops::{Deref, DerefMut};
 
 use bevy::prelude::UVec2;
+use num_traits::AsPrimitive;
 
 #[inline]
 pub fn f32_max(a: f32, b: f32) -> f32 {
@@ -42,41 +43,83 @@ pub struct Indexer {
 }
 
 impl Indexer {
-	pub fn new(width: usize, height: usize) -> Self {
-		Indexer { width, height }
+	pub fn new(width: impl AsPrimitive<usize>, height: impl AsPrimitive<usize>) -> Self {
+		Indexer {
+			width: width.as_(),
+			height: height.as_(),
+		}
 	}
-	pub fn index(&self, x: usize, y: usize) -> usize {
-		(y * self.width) + x
+	pub fn index(&self, x: impl AsPrimitive<usize>, y: impl AsPrimitive<usize>) -> usize {
+		(y.as_() * self.width) + x.as_()
 	}
-	pub fn checked_index(&self, x: usize, y: usize) -> Option<usize> {
+	pub fn checked_index(
+		&self,
+		x: impl AsPrimitive<usize>,
+		y: impl AsPrimitive<usize>,
+	) -> Option<usize> {
 		if self.is_coordinate_valid(x, y) {
 			Some(self.index(x, y))
 		} else {
 			None
 		}
 	}
-	pub fn reverse(&self, index: usize) -> (usize, usize) {
-		(index % self.width, index / self.width)
+	pub fn reverse(&self, index: impl AsPrimitive<usize>) -> (usize, usize) {
+		(index.as_() % self.width, index.as_() / self.width)
 	}
-	pub fn checked_reverse(&self, idx: usize) -> Option<(usize, usize)> {
+	pub fn checked_reverse(&self, idx: impl AsPrimitive<usize>) -> Option<(usize, usize)> {
 		if self.is_index_valid(idx) {
 			Some(self.reverse(idx))
 		} else {
 			None
 		}
 	}
-	pub fn index_within(&self, idx: usize) -> bool {
+	pub fn index_within(&self, idx: impl AsPrimitive<usize>) -> bool {
 		let (x, y) = self.reverse(idx);
 		x >= 0 && x < self.width && y >= 0 && y < self.height
 	}
 	pub fn is_uvec2_valid(&self, point: UVec2) -> bool {
-		self.is_coordinate_valid(point.x as usize, point.y as usize)
+		self.is_coordinate_valid(point.x, point.y)
+	}
+	pub fn square_adjacent(
+		&self,
+		x: impl AsPrimitive<u32>,
+		y: impl AsPrimitive<u32>,
+	) -> Vec<UVec2> {
+		let x = x.as_();
+		let y = y.as_();
+
+		let initial_point = UVec2::new(x, y);
+
+		let left = UVec2::new(x.saturating_sub(1), y);
+		let right = UVec2::new(x.saturating_add(1), y);
+		let top = UVec2::new(x, y.saturating_add(1));
+		let bottom = UVec2::new(x, y.saturating_sub(1));
+
+		let mut output = Vec::with_capacity(4);
+		if left != initial_point {
+			output.push(left);
+		}
+		if right != initial_point && right.x < self.width as u32 {
+			output.push(right);
+		}
+		if bottom != initial_point {
+			output.push(bottom)
+		}
+		if top != initial_point && top.y < self.height as u32 {
+			output.push(top)
+		}
+
+		output
 	}
-	pub fn is_coordinate_valid(&self, x: usize, y: usize) -> bool {
-		x < self.width && y < self.height
+	pub fn is_coordinate_valid(
+		&self,
+		x: impl AsPrimitive<usize>,
+		y: impl AsPrimitive<usize>,
+	) -> bool {
+		x.as_() < self.width && y.as_() < self.height
 	}
-	pub fn is_index_valid(&self, idx: usize) -> bool {
-		idx < self.width * self.height
+	pub fn is_index_valid(&self, idx: impl AsPrimitive<usize>) -> bool {
+		idx.as_() < self.width * self.height
 	}
 	pub fn width(&self) -> usize {
 		self.width
diff --git a/game_core/src/ui/components/a_text_box.rs b/game_core/src/ui/components/a_text_box.rs
new file mode 100644
index 0000000000000000000000000000000000000000..8cab85aa44bfc210c1d5b4e32aceef28fc020c4c
--- /dev/null
+++ b/game_core/src/ui/components/a_text_box.rs
@@ -0,0 +1,525 @@
+use std::time::Instant;
+
+use bevy::prelude::*;
+use kayak_font::{KayakFont, TextProperties};
+use kayak_ui::prelude::*;
+use kayak_ui::widgets::{
+	BackgroundBundle, ClipBundle, ElementBundle, NinePatch, TextProps, TextWidgetBundle,
+};
+use kayak_ui::DEFAULT_FONT;
+
+use crate::assets::AssetHandles;
+use crate::ui::prelude::{edge_px, px, stretch, value};
+
+/// Props used by the [`TextBox`] widget
+#[derive(Component, PartialEq, Debug, Clone)]
+pub struct ATextBoxProps {
+	/// If true, prevents the widget from being focused (and consequently edited)
+	pub disabled: bool,
+	/// The text to display when the user input is empty
+	pub placeholder: Option<String>,
+	/// The user input
+	///
+	/// This is a controlled state. You _must_ set this to the value to you wish to be displayed.
+	/// You can use the [`on_change`] callback to update this prop as the user types.
+	pub value: String,
+	/// Set the size of the text to be rendered. This will determine element height
+	pub font_size: f32,
+}
+
+impl Default for ATextBoxProps {
+	fn default() -> Self {
+		ATextBoxProps {
+			disabled: Default::default(),
+			placeholder: Default::default(),
+			value: Default::default(),
+			font_size: 18.0,
+		}
+	}
+}
+
+#[derive(Component, Clone, PartialEq)]
+pub struct ATextBoxState {
+	pub focused: bool,
+	pub graphemes: Vec<String>,
+	pub cursor_x: f32,
+	pub cursor_position: usize,
+	pub cursor_visible: bool,
+	pub cursor_last_update: Instant,
+	pub current_value: String,
+}
+
+impl Default for ATextBoxState {
+	fn default() -> Self {
+		Self {
+			focused: Default::default(),
+			graphemes: Default::default(),
+			cursor_x: 0.0,
+			cursor_position: Default::default(),
+			cursor_visible: Default::default(),
+			cursor_last_update: Instant::now(),
+			current_value: String::new(),
+		}
+	}
+}
+
+pub struct ATextBoxValue(pub String);
+
+impl Widget for ATextBoxProps {}
+
+/// A widget that displays a text input field
+/// A text box allows users to input text.
+/// This text box is fairly simple and only supports basic input.
+///
+#[derive(Bundle)]
+pub struct ATextBoxWidget {
+	pub text_box: ATextBoxProps,
+	pub styles: KStyle,
+	pub computed_styles: ComputedStyles,
+	pub on_event: OnEvent,
+	pub on_layout: OnLayout,
+	pub on_change: OnChange,
+	pub focusable: Focusable,
+	pub widget_name: WidgetName,
+}
+
+impl Default for ATextBoxWidget {
+	fn default() -> Self {
+		Self {
+			text_box: Default::default(),
+			styles: Default::default(),
+			computed_styles: ComputedStyles::default(),
+			on_event: Default::default(),
+			on_layout: Default::default(),
+			on_change: Default::default(),
+			focusable: Default::default(),
+			widget_name: ATextBoxProps::default().get_name(),
+		}
+	}
+}
+
+pub fn render_text_box_widget(
+	In((widget_context, entity)): In<(KayakWidgetContext, Entity)>,
+	mut commands: Commands,
+	mut query: Query<(
+		&KStyle,
+		&mut ComputedStyles,
+		&ATextBoxProps,
+		&mut OnEvent,
+		&OnChange,
+	)>,
+	mut state_query: ParamSet<(Query<&ATextBoxState>, Query<&mut ATextBoxState>)>,
+	font_assets: Res<Assets<KayakFont>>,
+	font_mapping: Res<FontMapping>,
+	asset_handles: Res<AssetHandles>,
+) -> bool {
+	if let Ok((styles, mut computed_styles, text_box, mut on_event, on_change)) =
+		query.get_mut(entity)
+	{
+		let state_entity = widget_context.use_state::<ATextBoxState>(
+			&mut commands,
+			entity,
+			ATextBoxState {
+				current_value: text_box.value.clone(),
+				..ATextBoxState::default()
+			},
+		);
+
+		let mut is_different = false;
+		if let Ok(state) = state_query.p0().get(state_entity) {
+			if state.current_value != text_box.value {
+				is_different = true;
+			}
+		}
+
+		let style_font = styles.font.clone();
+		let font_size = text_box.font_size;
+		let inner_height = get_font_size_line_height(font_size);
+		let element_height = inner_height + 24.0;
+
+		if is_different {
+			if let Ok(mut state) = state_query.p1().get_mut(state_entity) {
+				state.current_value = text_box.value.clone();
+				// Update graphemes
+				set_graphemes(&mut state, &font_assets, &font_mapping, &style_font);
+
+				state.cursor_position = state.graphemes.len();
+
+				set_new_cursor_position(
+					&mut state,
+					&font_assets,
+					&font_mapping,
+					&style_font,
+					font_size,
+				);
+			}
+		}
+
+		if let Ok(state) = state_query.p0().get(state_entity) {
+			// let nine_vals = NinePatch {
+			// 	handle: asset_handles.image("basic_panel"),
+			// 	border: Edge::all(12.0),
+			// };
+
+			*computed_styles = KStyle::default()
+				// Required styles
+				.with_style(KStyle {
+					render_command: value(match state.focused {
+						true => RenderCommand::NinePatch {
+							handle: asset_handles.image("panel_text_active"),
+							border: Edge::all(12.0),
+						},
+						false => RenderCommand::NinePatch {
+							handle: asset_handles.image("panel_text_idle"),
+							border: Edge::all(12.0),
+						}
+					}),
+					..Default::default()
+				})
+				// Apply any prop-given styles
+				.with_style(styles)
+				// If not set by props, apply these styles
+				.with_style(KStyle {
+					height: px(element_height),
+					padding: edge_px(12.0),
+					// cursor: CursorIcon::Text.into(),
+					..Default::default()
+				})
+				.into();
+
+			let background_styles = KStyle {
+				render_command: StyleProp::Value(RenderCommand::Quad),
+				height: px(get_font_size_line_height(font_size)),
+				padding_left: px(5.0),
+				padding_right: px(5.0),
+				padding_bottom: px(4.0),
+				..Default::default()
+			};
+
+			let cloned_on_change = on_change.clone();
+
+			*on_event = OnEvent::new(
+				move |In((event_dispatcher_context, _, mut event, _entity)): In<(
+					EventDispatcherContext,
+					WidgetState,
+					Event,
+					Entity,
+				)>,
+				      font_assets: Res<Assets<KayakFont>>,
+				      font_mapping: Res<FontMapping>,
+				      mut state_query: Query<&mut ATextBoxState>| {
+					match event.event_type {
+						EventType::KeyDown(key_event) => {
+							if key_event.key() == KeyCode::Right {
+								if let Ok(mut state) = state_query.get_mut(state_entity) {
+									if state.cursor_position < state.graphemes.len() {
+										state.cursor_position += 1;
+									}
+									set_new_cursor_position(
+										&mut state,
+										&font_assets,
+										&font_mapping,
+										&style_font,
+										font_size,
+									);
+								}
+							}
+							if key_event.key() == KeyCode::Left {
+								if let Ok(mut state) = state_query.get_mut(state_entity) {
+									if state.cursor_position > 0 {
+										state.cursor_position -= 1;
+									}
+									set_new_cursor_position(
+										&mut state,
+										&font_assets,
+										&font_mapping,
+										&style_font,
+										font_size,
+									);
+								}
+							}
+						}
+						EventType::CharInput { c } => {
+							if let Ok(mut state) = state_query.get_mut(state_entity) {
+								let cloned_on_change = cloned_on_change.clone();
+								if !state.focused {
+									return (event_dispatcher_context, event);
+								}
+								let cursor_pos = state.cursor_position;
+								if is_backspace_key(c) {
+									if !state.current_value.is_empty() {
+										let char_pos: usize = state.graphemes[0..cursor_pos - 1]
+											.iter()
+											.map(|g| g.len())
+											.sum();
+										state.current_value.remove(char_pos);
+										state.cursor_position -= 1;
+									}
+								} else if is_delete_key(c) {
+									if state.cursor_position < state.graphemes.len() {
+										let char_pos: usize = state.graphemes[0..cursor_pos]
+											.iter()
+											.map(|g| g.len())
+											.sum();
+										state.current_value.remove(char_pos);
+										// state.cursor_position -= 1;
+									}
+								} else if !c.is_control() {
+									let char_pos: usize = state.graphemes[0..cursor_pos]
+										.iter()
+										.map(|g| g.len())
+										.sum();
+									state.current_value.insert(char_pos, c);
+
+									state.cursor_position += 1;
+								}
+
+								// Update graphemes
+								set_graphemes(&mut state, &font_assets, &font_mapping, &style_font);
+
+								set_new_cursor_position(
+									&mut state,
+									&font_assets,
+									&font_mapping,
+									&style_font,
+									font_size,
+								);
+								cloned_on_change.set_value(state.current_value.clone());
+								event.add_system(cloned_on_change);
+							}
+						}
+						EventType::Focus => {
+							if let Ok(mut state) = state_query.get_mut(state_entity) {
+								state.focused = true;
+								// Update graphemes
+								set_graphemes(&mut state, &font_assets, &font_mapping, &style_font);
+
+								state.cursor_position = state.graphemes.len();
+
+								set_new_cursor_position(
+									&mut state,
+									&font_assets,
+									&font_mapping,
+									&style_font,
+									font_size,
+								);
+							}
+						}
+						EventType::Blur => {
+							if let Ok(mut state) = state_query.get_mut(state_entity) {
+								state.focused = false;
+							}
+						}
+						_ => {}
+					}
+					(event_dispatcher_context, event)
+				},
+			);
+
+			let cursor_styles = KStyle {
+				background_color: Color::ANTIQUE_WHITE.into(),
+				position_type: value(KPositionType::SelfDirected),
+				top: px(4.0),
+				left: px(state.cursor_x),
+				width: px(3.0),
+				height: px(inner_height - 8.0),
+				..Default::default()
+			};
+
+			let (text_value, text_color) = if !state.focused && text_box.value.is_empty() {
+				(
+					text_box.placeholder.clone().unwrap_or_default(),
+					Color::GRAY,
+				)
+			} else {
+				(state.current_value.clone(), Color::WHITE)
+			};
+
+			let text_styles = KStyle {
+				color: value(text_color),
+				top: stretch(1.0),
+				bottom: px(4.0),
+				..Default::default()
+			};
+
+			let shift = if let Some(layout) = widget_context.get_layout(entity) {
+				let font_handle = match &styles.font {
+					StyleProp::Value(font) => font_mapping.get_handle(font.clone()).unwrap(),
+					_ => font_mapping.get_handle(DEFAULT_FONT.into()).unwrap(),
+				};
+				if let Some(font) = font_assets.get(&font_handle) {
+					let string_to_cursor = state.graphemes[0..state.cursor_position].join("");
+					let measurement = font.measure(
+						&string_to_cursor,
+						TextProperties {
+							font_size,
+							line_height: get_font_size_line_height(font_size),
+							max_size: (10000.0, get_font_size_line_height(font_size) + 4.0),
+							alignment: Alignment::Start,
+							tab_size: 4,
+						},
+					);
+					if measurement.size().0 > layout.width {
+						(layout.width - measurement.size().0) - 20.0
+					} else {
+						0.0
+					}
+				} else {
+					0.0
+				}
+			} else {
+				0.0
+			};
+
+			let scroll_styles = KStyle {
+				position_type: value(KPositionType::SelfDirected),
+				padding_left: stretch(0.0),
+				padding_right: stretch(0.0),
+				padding_bottom: stretch(1.0),
+				padding_top: stretch(1.0),
+				left: px(shift),
+				..Default::default()
+			};
+
+			let parent_id = Some(entity);
+			rsx! {
+				<BackgroundBundle styles={background_styles}>
+					<ClipBundle styles={KStyle {
+						height: px(get_font_size_line_height(font_size)),
+						padding_left: stretch(0.0),
+						padding_right: stretch(0.0),
+						..Default::default()
+					}}>
+						<ElementBundle styles={scroll_styles}>
+							<TextWidgetBundle
+								styles={text_styles}
+								text={TextProps {
+									content: text_value,
+									size: font_size,
+									line_height: Some(get_font_size_line_height(font_size)),
+									word_wrap: false,
+									..Default::default()
+								}}
+							/>
+							{
+								if state.focused && state.cursor_visible {
+									constructor! {
+										<BackgroundBundle styles={cursor_styles} />
+									}
+								}
+							}
+						</ElementBundle>
+					</ClipBundle>
+				</BackgroundBundle>
+			}
+		}
+	}
+
+	true
+}
+
+/// Checks if the given character contains the "Backspace" sequence
+///
+/// Context: [Wikipedia](https://en.wikipedia.org/wiki/Backspace#Common_use)
+fn is_backspace_key(c: char) -> bool {
+	c == '\u{8}'
+}
+
+fn is_delete_key(c: char) -> bool {
+	c == '\u{7f}'
+}
+
+fn set_graphemes(
+	state: &mut ATextBoxState,
+	font_assets: &Res<Assets<KayakFont>>,
+	font_mapping: &FontMapping,
+	style_font: &StyleProp<String>,
+) {
+	let font_handle = match style_font {
+		StyleProp::Value(font) => font_mapping.get_handle(font.clone()).unwrap(),
+		_ => font_mapping.get_handle(DEFAULT_FONT.into()).unwrap(),
+	};
+
+	if let Some(font) = font_assets.get(&font_handle) {
+		state.graphemes = font
+			.get_graphemes(&state.current_value)
+			.iter()
+			.map(|s| s.to_string())
+			.collect::<Vec<_>>();
+	}
+}
+
+fn get_single_grapheme_length(
+	font_assets: &Res<Assets<KayakFont>>,
+	font_mapping: &FontMapping,
+	style_font: &StyleProp<String>,
+	text: &String,
+) -> usize {
+	let font_handle = match style_font {
+		StyleProp::Value(font) => font_mapping.get_handle(font.clone()).unwrap(),
+		_ => font_mapping.get_handle(DEFAULT_FONT.into()).unwrap(),
+	};
+
+	if let Some(font) = font_assets.get(&font_handle) {
+		let graphemes = font.get_graphemes(&text);
+		return graphemes[0].len();
+	}
+
+	0
+}
+
+fn set_new_cursor_position(
+	state: &mut ATextBoxState,
+	font_assets: &Res<Assets<KayakFont>>,
+	font_mapping: &FontMapping,
+	style_font: &StyleProp<String>,
+	font_size: f32,
+) {
+	let font_handle = match style_font {
+		StyleProp::Value(font) => font_mapping.get_handle(font.clone()).unwrap(),
+		_ => font_mapping.get_handle(DEFAULT_FONT.into()).unwrap(),
+	};
+
+	if let Some(font) = font_assets.get(&font_handle) {
+		let string_to_cursor = state.graphemes[0..state.cursor_position].join("");
+		let measurement = font.measure(
+			&string_to_cursor,
+			TextProperties {
+				font_size,
+				line_height: get_font_size_line_height(font_size),
+				max_size: (10000.0, get_font_size_line_height(font_size) + 4.0),
+				alignment: Alignment::Start,
+				tab_size: 4,
+			},
+		);
+
+		state.cursor_x = measurement.size().0;
+	}
+}
+
+pub fn get_font_size_line_height(size: f32) -> f32 {
+	if size <= 20.0 {
+		size + 2.0
+	} else {
+		size + 4.0
+	}
+}
+
+pub fn cursor_animation_system(
+	mut state_query: ParamSet<(Query<(Entity, &ATextBoxState)>, Query<&mut ATextBoxState>)>,
+) {
+	let mut should_update = Vec::new();
+
+	for (entity, state) in state_query.p0().iter() {
+		if state.cursor_last_update.elapsed().as_secs_f32() > 0.5 && state.focused {
+			should_update.push(entity);
+		}
+	}
+
+	for state_entity in should_update.drain(..) {
+		if let Ok(mut state) = state_query.p1().get_mut(state_entity) {
+			state.cursor_last_update = Instant::now();
+			state.cursor_visible = !state.cursor_visible;
+		}
+	}
+}
diff --git a/game_core/src/ui/components/button.rs b/game_core/src/ui/components/button.rs
new file mode 100644
index 0000000000000000000000000000000000000000..201720adbc4f38b6a59131819881ebafdc386f33
--- /dev/null
+++ b/game_core/src/ui/components/button.rs
@@ -0,0 +1,194 @@
+use bevy::prelude::*;
+use kayak_ui::prelude::*;
+use kayak_ui::widgets::{NinePatch, NinePatchBundle, TextProps, TextWidgetBundle};
+use kayak_ui::DEFAULT_FONT;
+use num_traits::AsPrimitive;
+
+use crate::assets::AssetHandles;
+use crate::parent_widget;
+use crate::ui::prelude::{edge_px, px, stretch, val_auto, value};
+
+#[derive(Component, Clone, PartialEq, Default)]
+pub struct ButtonWidgetProps {
+	pub text: String,
+	pub font_size: f32,
+	pub is_disabled: bool,
+	pub is_fixed: bool,
+}
+
+impl ButtonWidgetProps {
+	pub fn text(text: impl ToString, font_size: impl AsPrimitive<f32>) -> Self {
+		Self {
+			text: text.to_string(),
+			font_size: font_size.as_(),
+			..Default::default()
+		}
+	}
+}
+
+pub fn button_props(text: impl ToString, is_disabled: bool, is_fixed: bool) -> ButtonWidgetProps {
+	ButtonWidgetProps {
+		text: text.to_string(),
+		is_fixed,
+		is_disabled,
+		font_size: 32.0,
+	}
+}
+
+impl Widget for ButtonWidgetProps {}
+
+parent_widget!(ButtonWidgetProps => ButtonWidget);
+
+#[derive(Component, PartialEq, Clone, Default)]
+pub struct ButtonWidgetState {
+	pub is_pressed: bool,
+	pub is_hovered: bool,
+}
+
+pub fn render_button_widget(
+	In((mut widget_context, entity)): In<(KayakWidgetContext, Entity)>,
+	mut commands: Commands,
+	state_query: Query<&ButtonWidgetState>,
+	mut query: Query<(&ButtonWidgetProps, &KChildren, &mut ComputedStyles, &KStyle)>,
+	assets: Res<AssetHandles>,
+) -> bool {
+	if let Ok((props, children, mut computed, style)) = query.get_mut(entity) {
+		let state_entity =
+			widget_context.use_state(&mut commands, entity, ButtonWidgetState::default());
+
+		let parent_id = Some(entity);
+
+		if let Ok(state) = state_query.get(state_entity) {
+			let events = OnEvent::new(
+				move |In((event_dispatcher_context, _, mut event, _)): In<(
+					EventDispatcherContext,
+					WidgetState,
+					Event,
+					Entity,
+				)>,
+				      mut params: ParamSet<(
+					Query<&ButtonWidgetProps>,
+					Query<&mut ButtonWidgetState>,
+				)>| {
+					let widget_props = match params.p0().get(entity) {
+						Ok(p) => p.clone(),
+						Err(..) => return (event_dispatcher_context, event),
+					};
+
+					if let Ok(mut state) = params.p1().get_mut(state_entity) {
+						match &event.event_type {
+							EventType::Hover(..) | EventType::MouseIn(..) => {
+								if !widget_props.is_disabled {
+									state.is_hovered = true;
+								}
+							}
+							EventType::MouseOut(..) => {
+								state.is_hovered = false;
+								state.is_pressed = false;
+							}
+							EventType::MouseDown(..) => {
+								if !widget_props.is_disabled {
+									state.is_pressed = true;
+								}
+							}
+							EventType::MouseUp(..) => {
+								state.is_pressed = false;
+							}
+							EventType::Click(..) => {
+								if widget_props.is_disabled {
+									event.prevent_default();
+									event.stop_propagation();
+								}
+							}
+							_ => {}
+						}
+					}
+
+					(event_dispatcher_context, event)
+				},
+			);
+
+			let button_height = props.font_size + 2.0 + 24.0; // + 8.0;
+
+			let nine_vals = if props.is_disabled {
+				NinePatch {
+					handle: assets.image("button_disabled"),
+					border: Edge::all(12.0),
+				}
+			} else if state.is_pressed {
+				NinePatch {
+					handle: assets.image("button_down"),
+					border: Edge::all(12.0),
+				}
+			} else if state.is_hovered {
+				NinePatch {
+					handle: assets.image("button_active"),
+					border: Edge::all(12.0),
+				}
+			} else {
+				NinePatch {
+					handle: assets.image("button_idle"),
+					border: Edge::all(12.0),
+				}
+			};
+
+			let padding = if state.is_pressed {
+				StyleProp::Value(Edge::new(
+					Units::Pixels(6.0),
+					Units::Stretch(1.0),
+					Units::Pixels(16.0),
+					Units::Stretch(1.0),
+				))
+			} else {
+				StyleProp::Value(Edge::new(
+					Units::Pixels(3.0),
+					Units::Stretch(1.0),
+					Units::Pixels(19.0),
+					Units::Stretch(1.0),
+				))
+			};
+
+			*computed = KStyle {
+				render_command: value(RenderCommand::Layout),
+				min_height: px(32.0),
+				min_width: px(32.0),
+				height: px(button_height),
+				padding: value(Edge::all(Units::Stretch(0.0))),
+				..Default::default()
+			}
+			.with_style(style)
+			.into();
+
+			let ninepatch_styles = KStyle {
+				padding,
+				..Default::default()
+			};
+
+			let text_style = KStyle {
+				color: value(Color::BLACK),
+				..Default::default()
+			};
+
+			rsx! {
+				<NinePatchBundle
+					on_event={events}
+					nine_patch={nine_vals}
+					styles={ninepatch_styles}
+				>
+					<TextWidgetBundle
+						text={TextProps {
+							content: props.text.clone(),
+							word_wrap: false,
+							subpixel: true,
+							size: props.font_size,
+							line_height: Some(props.font_size + 2.0),
+							..Default::default()
+						}}
+						styles={text_style}
+					/>
+				</NinePatchBundle>
+			}
+		}
+	}
+	true
+}
diff --git a/game_core/src/ui/components/debug_info.rs b/game_core/src/ui/components/debug_info.rs
new file mode 100644
index 0000000000000000000000000000000000000000..8e5e9010f66fdf69327485c5aa6bca25e9fd244c
--- /dev/null
+++ b/game_core/src/ui/components/debug_info.rs
@@ -0,0 +1,70 @@
+// use kayak_ui::widgets::{Element, Text};
+use bevy::prelude::*;
+use kayak_ui::prelude::*;
+use kayak_ui::widgets::{ElementBundle, TextWidgetBundle};
+
+use crate::ui::prelude::{px, simple_text, stretch, val_auto, value};
+use crate::{basic_widget, empty_props};
+
+empty_props!(DebugInfoProps);
+basic_widget!(DebugInfoProps => DebugInfoWidget);
+
+pub fn render_debug_info(
+	In((mut widget_context, entity)): In<(KayakWidgetContext, Entity)>,
+	mut commands: Commands,
+) -> bool {
+	let parent_id = Some(entity);
+	static VERSION: &str = env!("CARGO_PKG_VERSION");
+
+	let element_style = KStyle {
+		position_type: value(KPositionType::SelfDirected),
+		right: px(20.0),
+		bottom: px(20.0),
+		left: stretch(1.0),
+		top: stretch(1.0),
+		width: val_auto(),
+		height: val_auto(),
+		..Default::default()
+	};
+
+	rsx! {
+		<ElementBundle styles={element_style}>
+			<TextWidgetBundle text={simple_text(VERSION, "mono", 32.0)} />
+		</ElementBundle>
+	}
+
+	true
+}
+
+// #[widget]
+// pub fn DebugInfo() {
+// 	// let f = WidgetN
+// 	let container_style = Style {
+// 		position_type: PositionType::SelfDirected.into(),
+// 		bottom: px(20.0),
+// 		right: px(20.0),
+// 		left: stretch(1.0),
+// 		top: stretch(1.0),
+// 		width: px(120.0),
+// 		height: Units::Auto.into(),
+// 		..Default::default()
+// 	};
+// 	let text_style = Style {
+// 		color: Color::WHITE.into(),
+// 		font_size: (24.0).into(),
+// 		left: stretch(1.0),
+// 		font: context
+// 			.get_font_id("mono")
+// 			.map(StyleProp::Value)
+// 			.unwrap_or(StyleProp::Unset),
+// 		..Default::default()
+// 	};
+//
+// 	let val = env!("CARGO_PKG_VERSION");
+//
+// 	rsx! {
+// 		<Element styles={Some(container_style)}>
+// 			<Text styles={Some(text_style)} content={format!("v{}", val)} />
+// 		</Element>
+// 	}
+// }
diff --git a/game_core/src/ui/components/image_button.rs b/game_core/src/ui/components/image_button.rs
new file mode 100644
index 0000000000000000000000000000000000000000..da5eb6dd1974656655f3dc0aad42fa60d7240fe2
--- /dev/null
+++ b/game_core/src/ui/components/image_button.rs
@@ -0,0 +1,252 @@
+use bevy::prelude::*;
+use kayak_ui::prelude::*;
+use kayak_ui::widgets::{
+	KImage, KImageBundle, NinePatch, NinePatchBundle, TextProps, TextWidgetBundle,
+	TextureAtlasBundle, TextureAtlasProps,
+};
+use kayak_ui::DEFAULT_FONT;
+
+use crate::assets::AssetHandles;
+use crate::parent_widget;
+use crate::ui::prelude::{edge_px, px, stretch, value};
+
+#[derive(Component, Clone, Eq, PartialEq, Default)]
+pub enum ImageButtonContent {
+	Image(String),
+	Atlas(String, usize),
+	#[default]
+	None,
+}
+
+#[derive(Component, Clone, Eq, PartialEq, Default)]
+pub struct ImageButtonWidgetProps {
+	pub content: ImageButtonContent,
+	pub is_disabled: bool,
+	pub is_fixed: bool,
+}
+
+impl ImageButtonWidgetProps {
+	pub fn image(name: impl ToString) -> Self {
+		Self {
+			content: ImageButtonContent::Image(name.to_string()),
+			is_disabled: false,
+			is_fixed: false,
+		}
+	}
+	pub fn atlas(name: impl ToString, index: usize) -> Self {
+		Self {
+			content: ImageButtonContent::Atlas(name.to_string(), index),
+			is_disabled: false,
+			is_fixed: false,
+		}
+	}
+}
+
+impl Widget for ImageButtonWidgetProps {}
+
+parent_widget!(ImageButtonWidgetProps => ImageButtonWidget);
+
+#[derive(Component, PartialEq, Clone, Default)]
+pub struct ImageButtonWidgetState {
+	pub is_pressed: bool,
+	pub is_hovered: bool,
+}
+
+pub struct ButtonClickEvent {
+	pub button: Entity,
+}
+
+pub fn render_image_button_widget(
+	In((mut widget_context, entity)): In<(KayakWidgetContext, Entity)>,
+	mut commands: Commands,
+	state_query: Query<&ImageButtonWidgetState>,
+	mut query: Query<(
+		&ImageButtonWidgetProps,
+		&KChildren,
+		&mut ComputedStyles,
+		&KStyle,
+	)>,
+	assets: Res<AssetHandles>,
+	atlass: Res<Assets<TextureAtlas>>,
+) -> bool {
+	if let Ok((props, children, mut computed, style)) = query.get_mut(entity) {
+		let state_entity =
+			widget_context.use_state(&mut commands, entity, ImageButtonWidgetState::default());
+
+		let parent_id = Some(entity);
+
+		if let Ok(state) = state_query.get(state_entity) {
+			let events = OnEvent::new(
+				move |In((event_dispatcher_context, _, mut event, _)): In<(
+					EventDispatcherContext,
+					WidgetState,
+					Event,
+					Entity,
+				)>,
+				      mut params: ParamSet<(
+					Query<&ImageButtonWidgetProps>,
+					Query<&mut ImageButtonWidgetState>,
+				)>| {
+					let widget_props = match params.p0().get(entity) {
+						Ok(p) => p.clone(),
+						Err(..) => return (event_dispatcher_context, event),
+					};
+
+					if let Ok(mut state) = params.p1().get_mut(state_entity) {
+						match &event.event_type {
+							EventType::Hover(..) | EventType::MouseIn(..) => {
+								if !widget_props.is_disabled {
+									state.is_hovered = true;
+								}
+							}
+							EventType::MouseOut(..) => {
+								state.is_hovered = false;
+								state.is_pressed = false;
+							}
+							EventType::MouseDown(..) => {
+								if !widget_props.is_disabled {
+									state.is_pressed = true;
+								}
+							}
+							EventType::MouseUp(..) => {
+								state.is_pressed = false;
+							}
+							EventType::Click(..) => {
+								if widget_props.is_disabled {
+									event.prevent_default();
+									event.stop_propagation();
+								}
+							}
+							_ => {}
+						}
+					}
+
+					(event_dispatcher_context, event)
+				},
+			);
+
+			let nine_vals = if props.is_disabled {
+				NinePatch {
+					handle: assets.image("button_disabled"),
+					border: Edge::all(3.0),
+				}
+			} else if state.is_pressed {
+				NinePatch {
+					handle: assets.image("button_down"),
+					border: Edge::all(3.0),
+				}
+			} else if state.is_hovered {
+				NinePatch {
+					handle: assets.image("button_active"),
+					border: Edge::all(3.0),
+				}
+			} else {
+				NinePatch {
+					handle: assets.image("button_idle"),
+					border: Edge::all(3.0),
+				}
+			};
+
+			let padding = if state.is_pressed {
+				StyleProp::Value(Edge::new(
+					Units::Pixels(12.0),
+					Units::Pixels(8.0),
+					Units::Pixels(12.0),
+					Units::Pixels(8.0),
+				))
+			} else {
+				StyleProp::Value(Edge::new(
+					Units::Pixels(8.0),
+					Units::Pixels(8.0),
+					Units::Pixels(16.0),
+					Units::Pixels(8.0),
+				))
+			};
+
+			// let styles = KStyle {
+			// 	padding,
+			// 	..Default::default()
+			// }
+			// .with_style(style)
+			// .with_style(KStyle {
+			// 	width: stretch(1.0),
+			// 	height: px(48.0),
+			// 	bottom: px(20.0),
+			// 	min_height: px(32.0),
+			// 	min_width: px(48.0),
+			// 	..Default::default()
+			// });
+
+			*computed = KStyle {
+				render_command: value(RenderCommand::Layout),
+				min_height: px(32.0),
+				min_width: px(32.0),
+				padding: value(Edge::all(Units::Stretch(0.0))),
+				..Default::default()
+			}
+			.with_style(style)
+			.into();
+
+			let ninepatch_styles = KStyle {
+				padding,
+				..Default::default()
+			};
+
+			let image_styles = KStyle {
+				background_color: Color::RED.into(),
+				width: stretch(1.0),
+				height: stretch(1.0),
+				// padding_left: px(12.0),
+				// padding_right: px(12.0),
+				// padding_top: px(20.0),
+				// padding_bottom: px(20.0),
+				..Default::default()
+			};
+
+			rsx! {
+				<NinePatchBundle
+					on_event={events}
+					nine_patch={nine_vals}
+					styles={ninepatch_styles}
+				>
+					{match &props.content {
+						ImageButtonContent::Image(name) => {
+							let handle = assets.image(name);
+
+							constructor! {
+								<KImageBundle
+									image={KImage(handle)}
+									styles={image_styles}
+								/>
+							}
+						},
+						ImageButtonContent::Atlas(name, index) => {
+							let atlas_handle = assets.atlas(name);
+							let image_handle = assets.image(name);
+							if let Some(atlas) = atlass.get(&atlas_handle) {
+								let rect = atlas.textures[*index];
+								let position = rect.min;
+								let tile_size = rect.max - rect.min;
+
+								constructor! {
+									<TextureAtlasBundle
+										atlas={TextureAtlasProps {
+											tile_size,
+											position,
+											handle: image_handle,
+										}}
+										styles={image_styles}
+									/>
+								}
+
+							}
+
+						},
+						ImageButtonContent::None => {}
+					} }
+				</NinePatchBundle>
+			}
+		}
+	}
+	true
+}
diff --git a/game_core/src/ui/components/mod.rs b/game_core/src/ui/components/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..fe7558c26d5fd8de55f1202afe9c94b3bb9b242e
--- /dev/null
+++ b/game_core/src/ui/components/mod.rs
@@ -0,0 +1,21 @@
+/// Almost all of these base components are copy/pasted from Advent Realms, but will soon be published
+/// in the `advent_ui` package. Take a peak!
+mod a_text_box;
+mod button;
+mod debug_info;
+mod image_button;
+mod panel;
+mod v_divider;
+
+pub use self::a_text_box::{
+	cursor_animation_system, render_text_box_widget, ATextBoxProps, ATextBoxState, ATextBoxWidget,
+};
+pub use self::button::{
+	button_props, render_button_widget, ButtonWidget, ButtonWidgetProps, ButtonWidgetState,
+};
+pub use self::debug_info::{render_debug_info, DebugInfoProps, DebugInfoWidget};
+pub use self::image_button::{
+	render_image_button_widget, ImageButtonWidget, ImageButtonWidgetProps, ImageButtonWidgetState,
+};
+pub use self::panel::{render_panel_widget, PanelProps, PanelVariant, PanelWidget};
+pub use self::v_divider::{render_v_divider, VDividerWidget, VDividerWidgetProps};
diff --git a/game_core/src/ui/components/panel.rs b/game_core/src/ui/components/panel.rs
new file mode 100644
index 0000000000000000000000000000000000000000..45eca813056217e17bdb120711ab3046e25b3177
--- /dev/null
+++ b/game_core/src/ui/components/panel.rs
@@ -0,0 +1,125 @@
+use bevy::prelude::*;
+use kayak_ui::prelude::*;
+use kayak_ui::widgets::{NinePatch, NinePatchBundle};
+
+use crate::assets::AssetHandles;
+use crate::ui::prelude::{edge_px, px, stretch, value};
+use crate::{basic_widget, empty_props, parent_widget};
+
+#[derive(Default, Copy, Clone, Eq, PartialEq)]
+pub enum PanelVariant {
+	#[default]
+	Regular,
+	Simple,
+}
+
+#[derive(Component, Default, Clone, PartialEq, Eq)]
+pub struct PanelProps {
+	pub variant: PanelVariant,
+}
+
+impl Widget for PanelProps {}
+
+parent_widget!(PanelProps => PanelWidget);
+
+pub fn render_panel_widget(
+	In((mut widget_context, entity)): In<(KayakWidgetContext, Entity)>,
+	mut commands: Commands,
+	mut query: Query<(&PanelProps, &KChildren, &mut ComputedStyles, &KStyle)>,
+	assets: Res<AssetHandles>,
+) -> bool {
+	if let Ok((props, children, mut computed, style)) = query.get_mut(entity) {
+		let parent_id = Some(entity);
+		let (handle, edge_size) = match props.variant {
+			PanelVariant::Regular => (assets.image("panel"), 16.0),
+			PanelVariant::Simple => (assets.image("scroll_panel"), 44.0),
+		};
+
+		*computed = KStyle {
+			render_command: value(RenderCommand::Layout),
+			min_height: px(edge_size * 2.0 + 8.0),
+			min_width: px(edge_size * 2.0 + 8.0),
+			padding: value(Edge::all(Units::Stretch(0.0))),
+			..Default::default()
+		}
+		.with_style(style)
+		.into();
+
+		let inner_style = match &style.padding {
+			StyleProp::Unset => KStyle {
+				padding: edge_px(edge_size),
+				..Default::default()
+			},
+			pad => KStyle {
+				padding: pad.clone(),
+				..Default::default()
+			},
+		};
+
+		rsx! {
+			<NinePatchBundle
+				nine_patch={NinePatch {
+					handle,
+					border: Edge::all(edge_size),
+				}}
+				styles={inner_style}
+				children={children.clone()}
+			/>
+		}
+	}
+	true
+}
+
+// #[widget]
+// pub fn Panel(props: PanelProps) {
+// 	let styles = Some(
+// 		Style::default()
+// 			.with_style(Style {
+// 				min_height: Units::Pixels(72.0).into(),
+// 				min_width: Units::Pixels(72.0).into(),
+// 				..Default::default()
+// 			})
+// 			.with_style(props.get_styles())
+// 			.with_style(Style {
+// 				color: Color::WHITE.into(),
+// 				padding: StyleProp::Value(Edge::all(Units::Pixels(32.0))),
+// 				..Default::default()
+// 			}),
+// 	);
+//
+// 	let children = props.get_children();
+// 	let patch = context.get_ninepatch("panel");
+//
+// 	rsx! {
+// 		<NinePatch border={Edge::all(4.0 * 8.0)} handle={patch} styles={styles}>
+// 			{ children }
+// 		</NinePatch>
+// 	}
+// }
+//
+// #[widget]
+// pub fn BasicPanel(props: PanelProps) {
+// 	let styles = Some(
+// 		Style::default()
+// 			.with_style(Style {
+// 				min_height: Units::Pixels(28.0).into(),
+// 				min_width: Units::Pixels(28.0).into(),
+// 				..Default::default()
+// 			})
+// 			.with_style(props.get_styles())
+// 			.with_style(Style {
+// 				color: Color::WHITE.into(),
+// 				padding: StyleProp::Value(Edge::all(Units::Pixels(12.0))),
+// 				..Default::default()
+// 			}),
+// 	);
+//
+// 	let children = props.get_children();
+// 	let patch = context.get_ninepatch("basic_panel");
+//
+// 	rsx! {
+// 		<NinePatch border={Edge::all(12.0)} handle={patch} styles={styles} on_event={props.get_on_event()} on_layout={props.get_on_layout()}>
+// 			{ children }
+// 		</NinePatch>
+// 	}
+// }
diff --git a/game_core/src/ui/components/v_divider.rs b/game_core/src/ui/components/v_divider.rs
new file mode 100644
index 0000000000000000000000000000000000000000..7017566b7c31c681678d2c229240c26e5ebd73de
--- /dev/null
+++ b/game_core/src/ui/components/v_divider.rs
@@ -0,0 +1,49 @@
+use bevy::prelude::*;
+use kayak_ui::prelude::{
+	ComputedStyles, KStyle, KayakWidgetContext, RenderCommand, StyleProp, Widget,
+};
+
+use crate::basic_widget;
+use crate::ui::prelude::{px, stretch, value};
+
+#[derive(Component, Debug, Clone, PartialEq)]
+pub struct VDividerWidgetProps {
+	pub height: f32,
+	pub padding: f32,
+	pub color: Color,
+}
+
+impl Default for VDividerWidgetProps {
+	fn default() -> Self {
+		Self {
+			height: 1.0,
+			padding: 5.0,
+			color: Color::ANTIQUE_WHITE,
+		}
+	}
+}
+
+impl Widget for VDividerWidgetProps {}
+
+basic_widget!(VDividerWidgetProps => VDividerWidget);
+
+pub fn render_v_divider(
+	In((_, entity)): In<(KayakWidgetContext, Entity)>,
+	_: Commands,
+	mut query: Query<(&VDividerWidgetProps, &KStyle, &mut ComputedStyles)>,
+) -> bool {
+	if let Ok((props, style, mut computed)) = query.get_mut(entity) {
+		*computed = KStyle {
+			render_command: value(RenderCommand::Quad),
+			background_color: StyleProp::Value(props.color),
+			height: px(props.height),
+			top: px(props.padding),
+			bottom: px(props.padding),
+			width: stretch(1.0),
+			..Default::default()
+		}
+		.with_style(style)
+		.into();
+	}
+	true
+}
diff --git a/game_core/src/ui/mod.rs b/game_core/src/ui/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..d8151710891543860ee6a0c5b9b2be90ac90ca83
--- /dev/null
+++ b/game_core/src/ui/mod.rs
@@ -0,0 +1,123 @@
+use bevy::app::{PluginGroup, PluginGroupBuilder};
+use kayak_ui::prelude::KayakContextPlugin;
+use kayak_ui::widgets::KayakWidgets;
+
+// pub mod clrs;
+pub mod components;
+pub mod screens;
+pub mod utilities;
+// pub mod widgets;
+pub mod sync;
+
+pub mod prelude {
+	use bevy::prelude::FromReflect;
+	use kayak_ui::prelude::{Alignment, Edge, StyleProp, Units};
+
+	#[inline(always)]
+	pub fn px(val: f32) -> StyleProp<Units> {
+		StyleProp::Value(Units::Pixels(val))
+	}
+
+	#[inline(always)]
+	pub fn edge_px(val: f32) -> StyleProp<Edge<Units>> {
+		StyleProp::Value(Edge::all(Units::Pixels(val)))
+	}
+
+	#[inline(always)]
+	pub fn pct(val: f32) -> StyleProp<Units> {
+		StyleProp::Value(Units::Percentage(val))
+	}
+
+	#[inline(always)]
+	pub fn stretch(val: f32) -> StyleProp<Units> {
+		StyleProp::Value(Units::Stretch(val))
+	}
+
+	#[inline(always)]
+	pub fn val_auto() -> StyleProp<Units> {
+		StyleProp::Value(Units::Auto)
+	}
+
+	#[inline(always)]
+	pub fn value<T: Clone + Default + FromReflect>(val: T) -> StyleProp<T> {
+		StyleProp::Value(val)
+	}
+
+	#[inline(always)]
+	pub fn simple_text<Value: ToString, Font: ToString>(
+		value: Value,
+		font: Font,
+		size: f32,
+	) -> kayak_ui::widgets::TextProps {
+		kayak_ui::widgets::TextProps {
+			content: value.to_string(),
+			font: Some(font.to_string()),
+			show_cursor: true,
+			size,
+			alignment: Alignment::Start,
+			..Default::default()
+		}
+	}
+}
+
+mod _config {
+	use bevy::app::{App, Plugin};
+	use bevy::prelude::{Commands, DespawnRecursiveExt, Entity, Local, Query, Res, ResMut, With};
+	use iyes_loopless::prelude::{AppLooplessStateExt, ConditionSet};
+	use kayak_ui::prelude::FontMapping;
+
+	use crate::assets::AssetHandles;
+	use crate::system::flow::AppState;
+	use crate::ui::components::cursor_animation_system;
+	use crate::ui::utilities::StateUIRoot;
+	// use crate::ui::utilities::App;
+
+	pub fn configure_kayak_ui(
+		mut commands: Commands,
+		assets: Res<AssetHandles>,
+		mut font_mapping: ResMut<FontMapping>,
+	) {
+		log::info!("Configuring Fonts And UI");
+		font_mapping.set_default(assets.kayak_font("compass_pro"));
+	}
+
+	pub fn remove_ui(mut commands: Commands, query: Query<Entity, With<StateUIRoot>>) {
+		for entity in &query {
+			commands.entity(entity).despawn_recursive();
+		}
+	}
+
+	pub struct ConfigureKayakPlugin;
+	impl Plugin for ConfigureKayakPlugin {
+		fn build(&self, app: &mut App) {
+			app
+				// .init_resource::<MenuInterface>()
+				// .add_system(cursor_animation_system)
+				.add_exit_system(AppState::Setup, configure_kayak_ui)
+				// .add_enter_system(
+				// 	AppState::Menu,
+				// 	super::screens::main_menu::main_menu::render_main_menu,
+				// )
+				// .add_system_set(
+				// 	ConditionSet::new()
+				// 		.run_in_state(AppState::Menu)
+				// 		.with_system(main_menu_router)
+				// 		.into(),
+				// )
+				.add_enter_system(AppState::InGame, super::screens::render_in_game_ui)
+				.add_exit_system(AppState::Menu, remove_ui);
+		}
+	}
+}
+
+pub struct AdventUIPlugins;
+impl PluginGroup for AdventUIPlugins {
+	fn build(self) -> PluginGroupBuilder {
+		PluginGroupBuilder::start::<Self>()
+			// .add(kayak_ui::bevy::BevyKayakUIPlugin)
+			.add(_config::ConfigureKayakPlugin)
+			.add(KayakContextPlugin)
+			.add(KayakWidgets)
+			.add(sync::UISyncPlugin)
+	}
+}
diff --git a/game_core/src/ui/screens/in_game.rs b/game_core/src/ui/screens/in_game.rs
new file mode 100644
index 0000000000000000000000000000000000000000..3638d0e6dc2b00dbd31acfd67368d91ded5fb4ef
--- /dev/null
+++ b/game_core/src/ui/screens/in_game.rs
@@ -0,0 +1,207 @@
+use bevy::prelude::*;
+use kayak_ui::prelude::*;
+use kayak_ui::widgets::{ElementBundle, KayakAppBundle, TextProps, TextWidgetBundle};
+
+use crate::assets::AssetHandles;
+use crate::states::Player;
+use crate::ui::components::*;
+use crate::ui::prelude::{edge_px, pct, px, stretch, value};
+use crate::ui::sync::UITravelInfo;
+use crate::ui::utilities::context::create_root_context;
+use crate::ui::utilities::{widget_update_with_resource, StateUIRoot};
+use crate::world::{CurrentResidence, MapQuery, TownPaths};
+use crate::{
+	empty_props, on_button_click, parent_widget, register_widget_with_resource,
+	register_widget_with_update,
+};
+
+empty_props!(InGameProps);
+parent_widget!(InGameProps => InGameLayout);
+
+pub fn transit_button_factory(target: String) -> OnEvent {
+	let target = target.clone();
+	on_button_click!(
+		ParamSet<(
+			Commands,
+			Res<TownPaths>,
+			Query<(Entity, &CurrentResidence), With<Player>>,
+			MapQuery,
+		)>,
+		|mut params: ParamSet<(
+			Commands,
+			Res<TownPaths>,
+			Query<(Entity, &CurrentResidence), With<Player>>,
+			MapQuery,
+		)>| {
+			let target = target.clone();
+			let (entity, current) = {
+				match params.p2().get_single() {
+					Ok((entity, current)) => (entity.clone(), (current.get_location()).clone()),
+					_ => return,
+				}
+			};
+
+			let places = match params.p1().routes.get(&current) {
+				Some(places) => places.clone(),
+				None => return,
+			};
+
+			let bundle = match params.p3().get_active_level() {
+				Some(level) => places.create_route_bundle_for(target, level).unwrap(),
+				None => return,
+			};
+
+			params.p0().entity(entity).insert(bundle);
+		}
+	)
+}
+
+pub fn render_game_panels(
+	In((widget_context, entity)): In<(KayakWidgetContext, Entity)>,
+	mut commands: Commands,
+	ui_data: Res<UITravelInfo>,
+	assets: Res<AssetHandles>,
+	places: Res<TownPaths>,
+) -> bool {
+	let parent_id = Some(entity);
+
+	let distance_style = KStyle {
+		position_type: value(KPositionType::SelfDirected),
+		top: stretch(1.0),
+		left: stretch(1.0),
+		right: stretch(1.0),
+		bottom: px(50.0),
+		..Default::default()
+	};
+
+	let panel_style = KStyle {
+		position_type: value(KPositionType::SelfDirected),
+		width: pct(65.0),
+		height: pct(80.0),
+		min_width: px(400.0),
+		min_height: px(300.0),
+		max_width: px(850.0),
+		max_height: px(550.0),
+		top: stretch(1.0),
+		left: stretch(1.0),
+		right: stretch(1.0),
+		bottom: stretch(1.0),
+		layout_type: value(LayoutType::Column),
+		row_between: px(20.0),
+		..Default::default()
+	};
+
+	rsx! {
+		<ElementBundle
+			styles={KStyle {
+				..Default::default()
+			}}
+		>
+			{ if ui_data.distance_remaining > 0.1 {
+				constructor! {
+					<TextWidgetBundle
+						text={TextProps {
+							content: format!("{:.2}KM", ui_data.distance_remaining),
+							size: 48.0,
+							..Default::default()
+						}}
+						styles={distance_style}
+					/>
+				}
+			}}
+
+			{if ui_data.is_in_town {
+				if let Some(ref place) = ui_data.current_town {
+					constructor! {
+						<PanelWidget
+							styles={panel_style}
+						>
+							<TextWidgetBundle
+								text={TextProps {
+									content: format!("Hark! You enter {}", &place),
+									size: 48.0,
+									..Default::default()
+								}}
+								styles={KStyle {
+									color: value(Color::BLACK),
+									padding: edge_px(20.0),
+									left: stretch(1.0),
+									right: stretch(1.0),
+									..Default::default()
+								}}
+							/>
+
+							<VDividerWidget props={VDividerWidgetProps { height: 4.0, padding: 5.0, color: Color::rgb(0.52, 0.369, 0.18)}} />
+
+							<TextWidgetBundle
+								text={TextProps {
+									content: format!("Set off for:"),
+									size: 32.0,
+									..Default::default()
+								}}
+								styles={KStyle {
+									color: value(Color::BLACK),
+									padding: edge_px(20.0),
+									left: stretch(1.0),
+									right: stretch(1.0),
+									..Default::default()
+								}}
+							/>
+
+							{
+								for (place, distance) in ui_data.travel_options.iter() {
+									constructor! {
+										<ButtonWidget
+											styles={
+												KStyle {
+													left: stretch(1.0),
+													right: stretch(1.0),
+													width: pct(70.0),
+													min_width: px(300.0),
+													max_width: px(600.0),
+													bottom: px(10.0),
+													..Default::default()
+												}
+											}
+											props={ButtonWidgetProps::text(format!("{}: {:.2}KM", &place, distance), 28.0)}
+											on_event={transit_button_factory(place.clone())}
+										/>
+									}
+								}
+							}
+						</PanelWidget>
+					}
+				}
+			}}
+		</ElementBundle>
+	}
+
+	true
+}
+
+pub fn render_in_game_ui(mut commands: Commands) {
+	let parent_id = None;
+	let mut widget_context = create_ingame_context();
+
+	rsx! {
+		<KayakAppBundle>
+			<InGameLayout />
+		</KayakAppBundle>
+	}
+
+	commands.spawn((UICameraBundle::new(widget_context), StateUIRoot));
+}
+
+fn create_ingame_context() -> KayakRootContext {
+	let mut widget_context = create_root_context();
+
+	register_widget_with_resource!(
+		widget_context,
+		InGameProps,
+		EmptyState,
+		UITravelInfo,
+		render_game_panels
+	);
+
+	widget_context
+}
diff --git a/game_core/src/ui/screens/mod.rs b/game_core/src/ui/screens/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..2cce8e4c4109137b7a1ea0cfe3698bae1fa1fc9b
--- /dev/null
+++ b/game_core/src/ui/screens/mod.rs
@@ -0,0 +1,3 @@
+mod in_game;
+
+pub use in_game::render_in_game_ui;
diff --git a/game_core/src/ui/sync/mod.rs b/game_core/src/ui/sync/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..d6358a72b17dccf006a1c82ee9659129e3a7daa1
--- /dev/null
+++ b/game_core/src/ui/sync/mod.rs
@@ -0,0 +1,26 @@
+use bevy::app::{App, Plugin};
+use iyes_loopless::prelude::ConditionSet;
+
+use crate::system::flow::AppState;
+
+mod sync_travel;
+
+pub struct UISyncPlugin;
+impl Plugin for UISyncPlugin {
+	fn build(&self, app: &mut App) {
+		app.insert_resource(sync_travel::UITravelInfo {
+			travel_options: Vec::new(),
+			is_in_town: false,
+			current_town: None,
+			distance_remaining: 0.0,
+		})
+		.add_system_set(
+			ConditionSet::new()
+				.run_in_state(AppState::InGame)
+				.with_system(sync_travel::sync_player_travel_info_to_ui)
+				.into(),
+		);
+	}
+}
+
+pub use sync_travel::UITravelInfo;
diff --git a/game_core/src/ui/sync/sync_travel.rs b/game_core/src/ui/sync/sync_travel.rs
new file mode 100644
index 0000000000000000000000000000000000000000..585f492b45f649e764e5788250be8035e26c32d5
--- /dev/null
+++ b/game_core/src/ui/sync/sync_travel.rs
@@ -0,0 +1,52 @@
+use bevy::math::Vec3Swizzles;
+use bevy::prelude::*;
+
+use crate::states::Player;
+use crate::world::{CurrentResidence, MapQuery, TownPaths, TravelPath};
+
+#[derive(Resource, Clone)]
+pub struct UITravelInfo {
+	pub distance_remaining: f32,
+	pub is_in_town: bool,
+	pub travel_options: Vec<(String, f32)>,
+	pub current_town: Option<String>,
+}
+
+pub fn sync_player_travel_info_to_ui(
+	player_query: Query<(&Transform, Option<&TravelPath>, &CurrentResidence), With<Player>>,
+	mut ui_info: ResMut<UITravelInfo>,
+	places: Res<TownPaths>,
+	map_query: MapQuery,
+) {
+	for (transform, maybe_path, residence) in &player_query {
+		if let Some(path) = maybe_path {
+			let remaining_distance = path.ui_distance_remaining();
+			let current_edge_length = path.get_current_edge_distance();
+			let distance_to_next = path.distance_to_next_node(transform.translation.xy());
+
+			ui_info.distance_remaining =
+				remaining_distance - (current_edge_length - distance_to_next);
+		}
+		match residence {
+			CurrentResidence::TravellingFrom(place) => {
+				if ui_info.is_in_town {
+					ui_info.current_town = None;
+					ui_info.is_in_town = false;
+				}
+			}
+			CurrentResidence::RestingAt(place) => {
+				if !ui_info.is_in_town {
+					ui_info.is_in_town = true;
+					ui_info.current_town = Some(place.clone());
+					let level = map_query.get_active_level().unwrap();
+					let destinations = places.routes.get(place).unwrap();
+					ui_info.travel_options = destinations
+						.routes
+						.iter()
+						.map(|(loc, route)| (loc.clone(), route.calculate_distance(level)))
+						.collect();
+				}
+			}
+		}
+	}
+}
diff --git a/game_core/src/ui/utilities.rs b/game_core/src/ui/utilities.rs
new file mode 100644
index 0000000000000000000000000000000000000000..54c2ca887fef587d356ed7a67a01b8dea5558b26
--- /dev/null
+++ b/game_core/src/ui/utilities.rs
@@ -0,0 +1,216 @@
+use bevy::prelude::{
+	Commands, Component, DespawnRecursiveExt, Entity, In, Query, Res, Resource, With,
+};
+use kayak_ui::prelude::{
+	Event, EventDispatcherContext, KayakRootContext, KayakWidgetContext, WidgetParam, WidgetState,
+};
+
+#[derive(bevy::prelude::Component, Clone, PartialEq, Default)]
+pub struct EmptyProps;
+
+#[macro_export]
+macro_rules! empty_props {
+	($name: ident) => {
+		#[derive(::bevy::prelude::Component, Clone, PartialEq, Default)]
+		#[repr(transparent)]
+		pub struct $name($crate::ui::utilities::EmptyProps);
+		impl kayak_ui::prelude::Widget for $name {}
+	};
+}
+
+#[macro_export]
+macro_rules! basic_widget {
+	($props: ident => $name: ident) => {
+		#[derive(bevy::prelude::Bundle)]
+		pub struct $name {
+			pub props: $props,
+			pub name: ::kayak_ui::prelude::WidgetName,
+			pub styles: ::kayak_ui::prelude::KStyle,
+			pub computed_styles: ::kayak_ui::prelude::ComputedStyles,
+		}
+
+		impl std::default::Default for $name {
+			fn default() -> $name {
+				let props = $props::default();
+				$name {
+					name: kayak_ui::prelude::Widget::get_name(&props),
+					props,
+					styles: ::kayak_ui::prelude::KStyle::default(),
+					computed_styles: ::kayak_ui::prelude::ComputedStyles::default(),
+				}
+			}
+		}
+	};
+}
+#[macro_export]
+macro_rules! parent_widget {
+	($props: ident => $name: ident) => {
+		#[derive(bevy::prelude::Bundle)]
+		pub struct $name {
+			pub props: $props,
+			pub name: ::kayak_ui::prelude::WidgetName,
+			pub styles: ::kayak_ui::prelude::KStyle,
+			pub computed_styles: ::kayak_ui::prelude::ComputedStyles,
+			pub children: ::kayak_ui::prelude::KChildren,
+			pub on_event: ::kayak_ui::prelude::OnEvent,
+		}
+
+		impl std::default::Default for $name {
+			fn default() -> $name {
+				let props = $props::default();
+				$name {
+					name: kayak_ui::prelude::Widget::get_name(&props),
+					props,
+					styles: ::kayak_ui::prelude::KStyle::default(),
+					computed_styles: ::kayak_ui::prelude::ComputedStyles::default(),
+					children: ::kayak_ui::prelude::KChildren::default(),
+					on_event: ::kayak_ui::prelude::OnEvent::default(),
+				}
+			}
+		}
+	};
+}
+
+#[macro_export]
+macro_rules! register_widget {
+	($ctx: expr, $props: ident, $state: ident, $system: ident) => {{
+		$ctx.add_widget_data::<$props, $state>();
+		$ctx.add_widget_system(
+			::kayak_ui::prelude::Widget::get_name(&$props::default()),
+			::kayak_ui::prelude::widget_update::<$props, $state>,
+			$system,
+		);
+	}};
+}
+
+#[macro_export]
+macro_rules! register_widget_with_update {
+	($ctx: expr, $props: ident, $state: ident, $system: ident, $update: expr) => {{
+		$ctx.add_widget_data::<$props, $state>();
+		$ctx.add_widget_system(
+			::kayak_ui::prelude::Widget::get_name(&$props::default()),
+			$update,
+			$system,
+		);
+	}};
+}
+
+#[macro_export]
+macro_rules! register_widget_with_resource {
+	($ctx: expr, $props: ident, $state: ident, $resource: ident, $system: ident) => {{
+		$ctx.add_widget_data::<$props, $state>();
+		$ctx.add_widget_system(
+			::kayak_ui::prelude::Widget::get_name(&$props::default()),
+			$crate::ui::utilities::widget_update_with_resource::<$props, $state, $resource>,
+			$system,
+		);
+	}};
+}
+
+pub fn widget_update_with_resource<
+	Props: PartialEq + Component + Clone,
+	State: PartialEq + Component + Clone,
+	External: Resource,
+>(
+	In((widget_context, entity, previous_entity)): In<(KayakWidgetContext, Entity, Entity)>,
+	resource: Res<External>,
+	widget_param: WidgetParam<Props, State>,
+) -> bool {
+	widget_param.has_changed(&widget_context, entity, previous_entity) || resource.is_changed()
+}
+
+#[derive(Component)]
+pub struct StateUIRoot;
+
+fn remove_root_ui(
+	mut commands: Commands,
+	query: Query<Entity, (With<KayakRootContext>, With<StateUIRoot>)>,
+) {
+	for entity in &query {
+		commands.entity(entity).despawn_recursive();
+	}
+}
+
+pub mod context {
+	use kayak_ui::prelude::{widget_update, EmptyState, KayakRootContext};
+	use kayak_ui::widgets::KayakWidgetsContextPlugin;
+	use kayak_ui::KayakUIPlugin;
+
+	use crate::register_widget;
+	use crate::ui::components::*;
+
+	pub fn create_root_context() -> KayakRootContext {
+		let mut widget_context = KayakRootContext::new();
+		widget_context.add_plugin(KayakWidgetsContextPlugin);
+		widget_context.add_plugin(AdventWidgetsPlugin);
+		widget_context
+	}
+
+	pub struct AdventWidgetsPlugin;
+	impl KayakUIPlugin for AdventWidgetsPlugin {
+		fn build(&self, widget_context: &mut KayakRootContext) {
+			register_widget!(
+				widget_context,
+				DebugInfoProps,
+				EmptyState,
+				render_debug_info
+			);
+			register_widget!(widget_context, PanelProps, EmptyState, render_panel_widget);
+			register_widget!(
+				widget_context,
+				ButtonWidgetProps,
+				ButtonWidgetState,
+				render_button_widget
+			);
+			register_widget!(
+				widget_context,
+				ImageButtonWidgetProps,
+				ImageButtonWidgetState,
+				render_image_button_widget
+			);
+			register_widget!(
+				widget_context,
+				VDividerWidgetProps,
+				EmptyState,
+				render_v_divider
+			);
+			register_widget!(
+				widget_context,
+				ATextBoxProps,
+				ATextBoxState,
+				render_text_box_widget
+			);
+		}
+	}
+}
+
+pub type OnEventParams = In<(EventDispatcherContext, WidgetState, Event, Entity)>;
+
+#[macro_export]
+macro_rules! on_button_click {
+	($param_type: ty, $func_body: expr) => {
+		::kayak_ui::prelude::OnEvent::new(
+			move |::bevy::prelude::In((
+				event_dispatcher_context,
+				_widget_state,
+				event,
+				_entity,
+			)): ::bevy::prelude::In<(
+				::kayak_ui::prelude::EventDispatcherContext,
+				::kayak_ui::prelude::WidgetState,
+				::kayak_ui::prelude::Event,
+				::bevy::prelude::Entity,
+			)>,
+			      params: $param_type| {
+				match event.event_type {
+					::kayak_ui::prelude::EventType::Click(..) => {
+						$func_body(params);
+					}
+					_ => {}
+				}
+
+				(event_dispatcher_context, event)
+			},
+		)
+	};
+}
diff --git a/game_core/src/world/debug.rs b/game_core/src/world/debug.rs
new file mode 100644
index 0000000000000000000000000000000000000000..97c9eae4e2f8f66a96dea13b9db91374bba8bb0c
--- /dev/null
+++ b/game_core/src/world/debug.rs
@@ -0,0 +1,32 @@
+use bevy::prelude::*;
+
+use crate::assets::AssetHandles;
+use crate::world::TravelPath;
+
+#[derive(Component)]
+pub struct Tombstone;
+
+pub fn create_tombstones(
+	mut commands: Commands,
+	assets: Res<AssetHandles>,
+	query: Query<&TravelPath, Or<(Added<TravelPath>, Changed<TravelPath>)>>,
+	existing: Query<Entity, With<Tombstone>>,
+) {
+	for path in &query {
+		for entity in &existing {
+			commands.entity(entity).despawn_recursive();
+		}
+
+		for point in &path.path {
+			commands.spawn((
+				SpriteSheetBundle {
+					transform: Transform::from_translation(point.extend(900.0)),
+					texture_atlas: assets.atlas("characters"),
+					sprite: TextureAtlasSprite::new(1),
+					..Default::default()
+				},
+				Tombstone,
+			));
+		}
+	}
+}
diff --git a/game_core/src/world/debug_gen.rs b/game_core/src/world/generators/debug_gen.rs
similarity index 95%
rename from game_core/src/world/debug_gen.rs
rename to game_core/src/world/generators/debug_gen.rs
index d8481accaf111268b0a819c6e1760ce879d457a1..48c2c2b18e303f7b7b84163ec4f35dccd3e695a9 100644
--- a/game_core/src/world/debug_gen.rs
+++ b/game_core/src/world/generators/debug_gen.rs
@@ -4,7 +4,7 @@ use bevy::render::texture::{BevyDefault, TextureFormatPixelInfo};
 use fastrand::Rng;
 
 use crate::system::utilities::Indexer;
-use crate::world::generate_overworld::{generate_overworld, WorldTileType};
+use crate::world::generators::generate_overworld::{generate_overworld, WorldTileType};
 
 pub fn generate_and_spawn_world(mut commands: Commands, mut images: ResMut<Assets<Image>>) {
 	let world = generate_overworld(300, 300, Rng::new());
diff --git a/game_core/src/world/generate_overworld.rs b/game_core/src/world/generators/generate_overworld.rs
similarity index 100%
rename from game_core/src/world/generate_overworld.rs
rename to game_core/src/world/generators/generate_overworld.rs
diff --git a/game_core/src/world/handle_overworld.rs b/game_core/src/world/generators/handle_overworld.rs
similarity index 82%
rename from game_core/src/world/handle_overworld.rs
rename to game_core/src/world/generators/handle_overworld.rs
index 27f432d0158c37838bad6257ac22030863aead92..70837fed8cc83d9b54d4ed1d7b5f609997c05c2a 100644
--- a/game_core/src/world/handle_overworld.rs
+++ b/game_core/src/world/generators/handle_overworld.rs
@@ -9,19 +9,27 @@ use fastrand::Rng;
 use crate::assets::AssetHandles;
 use crate::system::camera::GameCamera;
 use crate::system::utilities::Indexer;
-use crate::world::generate_overworld::{generate_overworld, Overworld};
+use crate::world::generators::generate_overworld::{generate_overworld, Overworld};
+use crate::world::generators::{ParticleRollingGenerator, WorldGenerator};
 
 pub fn spawn_new_map(
 	mut commands: Commands,
 	assets: Res<AssetHandles>,
 	mut query: Query<&mut Transform, With<GameCamera>>,
 ) {
-	let world = generate_overworld(300, 300, Rng::new());
+	let indexer = Indexer::new(500, 500);
+	let rng = Rng::new();
+
+	let world = Overworld {
+		width: 500,
+		height: 500,
+		terrain: ParticleRollingGenerator::new(21000, 400).generate(&indexer, &rng),
+	};
+
 	spawn_overworld(&world, &mut commands, &assets);
 	for mut camera_transform in &mut query {
-		log::info!("SET CAM TRANSFORM");
 		camera_transform.translation =
-			vec3(150.0 * 4.0, 150.0 * 4.0, camera_transform.translation.z);
+			vec3(250.0 * 4.0, 250.0 * 4.0, camera_transform.translation.z);
 	}
 }
 
diff --git a/game_core/src/world/generators/mod.rs b/game_core/src/world/generators/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..d012fc5280727e9761d0f73dbef598bf27c4de6d
--- /dev/null
+++ b/game_core/src/world/generators/mod.rs
@@ -0,0 +1,30 @@
+use fastrand::Rng;
+use num_traits::AsPrimitive;
+
+use crate::system::utilities::Indexer;
+use crate::world::generators::generate_overworld::WorldTileType;
+
+pub mod debug_gen;
+mod generate_overworld;
+pub mod handle_overworld;
+mod particle_rolling;
+
+pub trait WorldGenerator {
+	fn generate(&self, indexer: &Indexer, rng: &Rng) -> Vec<WorldTileType>;
+	fn map_noise_value(noise: impl AsPrimitive<f64>) -> WorldTileType {
+		let noise = noise.as_();
+		if noise < 0.1 {
+			WorldTileType::Water
+		} else if noise < 0.2 {
+			WorldTileType::Sand
+		} else if noise < 0.6 {
+			WorldTileType::Grass
+		} else if noise < 0.8 {
+			WorldTileType::Snow
+		} else {
+			WorldTileType::Mountain
+		}
+	}
+}
+
+pub use self::particle_rolling::ParticleRollingGenerator;
diff --git a/game_core/src/world/generators/particle_rolling.rs b/game_core/src/world/generators/particle_rolling.rs
new file mode 100644
index 0000000000000000000000000000000000000000..d43a13411b2f5d8787631f8c595a9d488a1d0649
--- /dev/null
+++ b/game_core/src/world/generators/particle_rolling.rs
@@ -0,0 +1,118 @@
+use std::iter::zip;
+
+use bevy::prelude::UVec2;
+use fastrand::Rng;
+use noise::{NoiseFn, Perlin};
+
+use crate::system::utilities::Indexer;
+use crate::world::generators::generate_overworld::WorldTileType;
+use crate::world::generators::WorldGenerator;
+
+#[derive(Copy, Clone, Debug)]
+pub struct ParticleRollingGenerator {
+	particle_iterations: usize,
+	particle_lifespan: usize,
+}
+
+impl ParticleRollingGenerator {
+	pub fn new(iterations: usize, lifespan: usize) -> Self {
+		Self {
+			particle_iterations: iterations,
+			particle_lifespan: lifespan,
+		}
+	}
+}
+
+impl Default for ParticleRollingGenerator {
+	fn default() -> Self {
+		ParticleRollingGenerator::new(5000, 50)
+	}
+}
+
+impl WorldGenerator for ParticleRollingGenerator {
+	fn generate(&self, indexer: &Indexer, rng: &Rng) -> Vec<WorldTileType> {
+		let mut map = self.create_map(indexer, rng);
+		let mut attempts = 500;
+
+		while attempts > 0 {
+			let grass_count = map.iter().fold(0, |count, tile| {
+				if *tile == WorldTileType::Grass {
+					count + 1
+				} else {
+					count
+				}
+			});
+
+			let water_count = map.iter().fold(0, |count, tile| {
+				if *tile == WorldTileType::Water {
+					count + 1
+				} else {
+					count
+				}
+			});
+
+			if grass_count > (map.len() - water_count) / 2 {
+				log::info!("Got good map");
+				break;
+			}
+
+			log::info!("Doing a new map now; remaining {}", attempts);
+			map = self.create_map(indexer, rng);
+			attempts -= 1;
+		}
+
+		map
+	}
+}
+
+impl ParticleRollingGenerator {
+	fn create_map(&self, indexer: &Indexer, rng: &Rng) -> Vec<WorldTileType> {
+		let width = indexer.width();
+		let height = indexer.height();
+		let mut values = vec![0.0; width * height];
+		let mut mask = vec![0.0; width * height];
+
+		for _ in 0..self.particle_iterations {
+			let mut lifespan = self.particle_lifespan;
+			let mut particle_x = rng.usize(width / 6..width - width / 6);
+			let mut particle_y = rng.usize(height / 6..height - height / 6);
+
+			while lifespan > 0 {
+				let idx = indexer.index(particle_x, particle_y);
+				let new_value = mask[idx] + 1.0 / 100.0;
+
+				mask[idx] = new_value;
+				let choices: Vec<UVec2> = indexer
+					.square_adjacent(particle_x, particle_y)
+					.iter()
+					.filter(|UVec2 { x, y }| mask[indexer.index(*x, *y)] <= new_value)
+					.copied()
+					.collect();
+
+				if !choices.is_empty() {
+					let next = choices[rng.usize(0..choices.len())];
+					particle_x = next.x as usize;
+					particle_y = next.y as usize;
+				}
+
+				lifespan -= 1;
+			}
+		}
+
+		// return mask.iter().copied().map(Self::map_noise_value).collect();
+
+		let noise = Perlin::new(rng.u32(0..u32::MAX));
+		let values: Vec<f64> = values
+			.iter()
+			.enumerate()
+			.map(|(index, _)| {
+				let (x, y) = indexer.reverse(index);
+				noise.get([x as f64 / width as f64, y as f64 / height as f64])
+			})
+			.collect();
+
+		zip(values, mask)
+			.map(|(left, right)| Self::map_noise_value(left * right))
+			.collect()
+	}
+}
diff --git a/game_core/src/world/mod.rs b/game_core/src/world/mod.rs
index f9931258f6b783ec6d1ac18f0ba83f0d0387e288..2e9349e79aeaf894cb52586ca312bf4f993fd678 100644
--- a/game_core/src/world/mod.rs
+++ b/game_core/src/world/mod.rs
@@ -1,18 +1,35 @@
 use bevy::app::App;
-use bevy::prelude::Plugin;
-use fastrand::Rng;
-use iyes_loopless::prelude::AppLooplessStateExt;
+use bevy::prelude::{Commands, Plugin};
+use iyes_loopless::prelude::{AppLooplessStateExt, ConditionSet};
 
 use crate::system::flow::AppState;
-use crate::world::debug_gen::generate_and_spawn_world;
+use crate::world::utils::ActiveLevel;
 
-mod debug_gen;
-mod generate_overworld;
-mod handle_overworld;
+mod debug;
+mod generators;
+mod spawning;
+mod towns;
+mod travel;
+mod utils;
+mod world_query;
 
 pub struct WorldPlugin;
 impl Plugin for WorldPlugin {
 	fn build(&self, app: &mut App) {
-		app.add_enter_system(AppState::InGame, handle_overworld::spawn_new_map);
+		app.init_resource::<TownPaths>()
+			.add_enter_system(AppState::InGame, |mut commands: Commands| {
+				commands.insert_resource(ActiveLevel::new("Grantswaith"));
+			})
+			.add_system_set(
+				ConditionSet::new()
+					.run_in_state(AppState::InGame)
+					// .with_system(debug::create_tombstones)
+					.with_system(spawning::spawn_world_data)
+					.with_system(travel::tick_travelling_merchant)
+					.into(),
+			);
 	}
 }
+
+pub use towns::{CurrentResidence, PathingResult, TownPaths, TravelPath, TravelTarget};
+pub use world_query::{CameraBounds, MapQuery};
diff --git a/game_core/src/world/spawning.rs b/game_core/src/world/spawning.rs
new file mode 100644
index 0000000000000000000000000000000000000000..1c94ea2b1b70368c270fd65b974b990b897e6dff
--- /dev/null
+++ b/game_core/src/world/spawning.rs
@@ -0,0 +1,157 @@
+use bevy::prelude::*;
+use bevy_ecs_tilemap::prelude::*;
+use num_traits::AsPrimitive;
+
+use crate::assets::{AssetHandles, LdtkProject, LevelIndex};
+use crate::states::Player;
+use crate::system::camera::ChaseCam;
+use crate::world::towns::{CurrentResidence, TownPaths};
+use crate::world::utils::{grid_to_px, px_to_grid, ActiveLevel, WorldLinked, TILE_SCALE_F32};
+use crate::world::world_query::MapQuery;
+
+pub fn spawn_world_data(
+	mut commands: Commands,
+	mut active_level: Option<ResMut<ActiveLevel>>,
+	assets: Res<AssetHandles>,
+	projects: Res<Assets<LdtkProject>>,
+	level_index: Res<LevelIndex>,
+	mut last_spawned_level: Local<String>,
+) {
+	let mut active_level = match active_level {
+		Some(l) => l,
+		None => return,
+	};
+
+	if *last_spawned_level == active_level.map && !active_level.dirty {
+		return;
+	} else {
+		*last_spawned_level = active_level.map.clone();
+	}
+
+	if active_level.is_changed() || active_level.is_added() {
+		let level = match level_index.get(&active_level.map) {
+			Some(l) => l,
+			_ => return,
+		};
+
+		let map_tile_width = px_to_grid(level.px_wid);
+		let map_tile_height = px_to_grid(level.px_hei);
+
+		let tilemap_size = TilemapSize {
+			x: map_tile_width.as_(),
+			y: map_tile_height.as_(),
+		};
+		let tilemap_tile_size = TilemapTileSize {
+			x: TILE_SCALE_F32,
+			y: TILE_SCALE_F32,
+		};
+
+		MapQuery::for_each_layer_of(level, |layer| {
+			if !layer.has_tiles() {
+				return;
+			}
+
+			let map_entity = commands.spawn_empty().id();
+			let mut storage = TileStorage::empty(tilemap_size);
+
+			layer.for_each_tile(|x, y, tile| {
+				let mut tile_pos = TilePos {
+					x: px_to_grid(x).as_(),
+					y: px_to_grid(y).as_(),
+				};
+
+				tile_pos.y = map_tile_height as u32 - tile_pos.y - 1;
+
+				let tile_entity = commands
+					.spawn(TileBundle {
+						position: tile_pos,
+						tilemap_id: TilemapId(map_entity),
+						texture_index: TileTextureIndex(tile.tile.t as u32),
+						..Default::default()
+					})
+					.id();
+
+				storage.set(&tile_pos, tile_entity);
+			});
+
+			let grid_size = tilemap_tile_size.into();
+			let map_type = TilemapType::Square;
+
+			log::info!("Spawning tilemap");
+
+			let bg = level
+				.level_bg_color
+				.clone()
+				.unwrap_or_else(|| level.bg_color.clone());
+
+			match bg.len() {
+				6 => commands.insert_resource(ClearColor(
+					Color::hex(bg.clone()).expect("Failed to set background color"),
+				)),
+				7 => commands.insert_resource(ClearColor(
+					Color::hex(&bg[1..]).expect("Failed to set background color"),
+				)),
+				_ => {
+					log::warn!("Strange colour {}", &bg)
+				}
+			}
+
+			commands.entity(map_entity).insert((
+				TilemapBundle {
+					grid_size,
+					map_type,
+					size: tilemap_size,
+					storage,
+					texture: TilemapTexture::Single(assets.image("overworld")),
+					tile_size: tilemap_tile_size,
+					transform: Transform::from_xyz(0.0, 0.0, 5.0 + layer.get_z_delta()), // get_tilemap_center_transform(&tilemap_size, &grid_size, 5.0),
+					..Default::default()
+				},
+				WorldLinked,
+			));
+		});
+
+		let trade_routes = TownPaths::from(MapQuery::get_entities_of(level));
+		let random_start = trade_routes
+			.routes
+			.values()
+			.nth(fastrand::usize(0..trade_routes.routes.len()));
+
+		if let Some(start) = random_start {
+			if let Some(route) = start.routes.values().next() {
+				let point = route.nodes[0];
+
+				commands.spawn((
+					SpriteSheetBundle {
+						transform: Transform::from_xyz(
+							grid_to_px(point.tile_x),
+							level.px_hei as f32 - grid_to_px(point.tile_y),
+							400.0,
+						),
+						texture_atlas: assets.atlas("characters"),
+						sprite: TextureAtlasSprite {
+							index: 0,
+							..Default::default()
+						},
+						..Default::default()
+					},
+					start
+						.create_route_bundle_for(
+							start
+								.routes
+								.keys()
+								.nth(fastrand::usize(0..start.routes.len()))
+								.cloned()
+								.unwrap(),
+							level,
+						)
+						.unwrap(),
+					Player,
+					ChaseCam,
+				));
+			}
+		}
+
+		commands.insert_resource(trade_routes);
+	}
+}
diff --git a/game_core/src/world/towns.rs b/game_core/src/world/towns.rs
new file mode 100644
index 0000000000000000000000000000000000000000..67705f35b18fceac8cce7d5a81860f15df67a5f7
--- /dev/null
+++ b/game_core/src/world/towns.rs
@@ -0,0 +1,247 @@
+use bevy::math::{vec2, Vec2};
+use bevy::prelude::{Bundle, Component, Resource};
+use bevy::utils::HashMap;
+use ldtk_rust::{EntityInstance, Level};
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+
+use crate::world::utils::{grid_to_px, px_to_grid};
+
+pub type Town = String;
+
+/// Stores the ID of the most recent town that has been inhabited,
+/// as well as the current travel state
+#[derive(Component)]
+pub enum CurrentResidence {
+	TravellingFrom(String),
+	RestingAt(String),
+}
+
+impl CurrentResidence {
+	pub fn is_travelling(&self) -> bool {
+		match self {
+			CurrentResidence::TravellingFrom(..) => true,
+			_ => false,
+		}
+	}
+	pub fn get_location(&self) -> &String {
+		match self {
+			CurrentResidence::TravellingFrom(place) | CurrentResidence::RestingAt(place) => place,
+		}
+	}
+}
+
+#[derive(Component)]
+pub struct TravelTarget(pub String);
+
+#[derive(Component)]
+pub struct TravelPath {
+	pub path: Vec<Vec2>,
+	last_index: usize,
+	next_index: usize,
+}
+
+pub enum PathingResult {
+	Travelling,
+	NextNode,
+	PathComplete,
+}
+
+fn calculate_distance(points: &mut impl Iterator<Item = Vec2>) -> f32 {
+	let mut total = 0.0;
+	let mut previous = points.next().unwrap();
+	while let Some(point) = points.next() {
+		total += previous.distance(point);
+		previous = point;
+	}
+	total
+}
+
+impl TravelPath {
+	pub fn increment_indexes(&mut self) {
+		self.last_index += 1;
+		self.next_index += 1;
+	}
+
+	pub fn get_step(&self, current: Vec2, speed: f32) -> Vec2 {
+		if let Some(next) = self.path.get(self.next_index) {
+			let step = current - *next;
+			step.normalize_or_zero() * speed
+		} else {
+			Vec2::ZERO
+		}
+	}
+	pub fn get_route_time(&self, speed: f32) -> f32 {
+		if let (Some(last), Some(next)) = (
+			self.path.get(self.last_index),
+			self.path.get(self.next_index),
+		) {
+			next.distance(*last) / speed
+		} else {
+			0.0
+		}
+	}
+
+	pub fn check_position(&mut self, current: Vec2) -> PathingResult {
+		if let Some(next) = self.path.get(self.next_index) {
+			if current.distance(*next).abs() < 0.05 {
+				PathingResult::NextNode
+			} else {
+				PathingResult::Travelling
+			}
+		} else {
+			PathingResult::PathComplete
+		}
+	}
+
+	pub fn ui_distance_remaining(&self) -> f32 {
+		calculate_distance(&mut self.path.iter().skip(self.last_index).copied())
+	}
+
+	pub fn distance_to_next_node(&self, other: Vec2) -> f32 {
+		if let Some(next) = self.path.get(self.next_index) {
+			other.distance(*next).abs()
+		} else {
+			0.0
+		}
+	}
+
+	pub fn get_current_edge_distance(&self) -> f32 {
+		if let (Some(last), Some(next)) = (
+			self.path.get(self.last_index),
+			self.path.get(self.next_index),
+		) {
+			last.distance(*next).abs()
+		} else {
+			0.0
+		}
+	}
+}
+
+#[derive(Serialize, Deserialize, Clone, Copy, Eq, Ord, PartialOrd, PartialEq, Debug)]
+pub struct RouteNode {
+	#[serde(alias = "cx")]
+	pub tile_x: usize,
+	#[serde(alias = "cy")]
+	pub tile_y: usize,
+}
+
+#[derive(Clone, Eq, Ord, PartialOrd, PartialEq, Debug)]
+pub struct Route {
+	pub nodes: Vec<RouteNode>,
+}
+
+impl Route {
+	pub fn create_travel_path_for(&self, level: &Level) -> TravelPath {
+		TravelPath {
+			path: self
+				.nodes
+				.iter()
+				.map(|node| {
+					vec2(
+						grid_to_px(node.tile_x),
+						level.px_hei as f32 - grid_to_px(node.tile_y),
+					)
+				})
+				.collect(),
+			last_index: 0,
+			next_index: 1,
+		}
+	}
+
+	pub fn calculate_distance(&self, level: &Level) -> f32 {
+		calculate_distance(&mut self.nodes.iter().map(|node| {
+			vec2(
+				grid_to_px(node.tile_x),
+				level.px_hei as f32 - grid_to_px(node.tile_y),
+			)
+		}))
+	}
+}
+
+#[derive(Clone, Eq, PartialEq, Debug)]
+pub struct Destinations {
+	pub source: Town,
+	pub routes: HashMap<String, Route>,
+}
+
+impl Destinations {
+	pub fn create_route_bundle_for(&self, target: String, level: &Level) -> Option<impl Bundle> {
+		self.routes.get(&target).map(|route| {
+			(
+				route.create_travel_path_for(level),
+				CurrentResidence::TravellingFrom(self.source.clone()),
+				TravelTarget(target.clone()),
+			)
+		})
+	}
+}
+
+#[derive(Clone, Eq, PartialEq, Debug, Default, Resource)]
+pub struct TownPaths {
+	pub routes: HashMap<Town, Destinations>,
+}
+
+impl From<Vec<&EntityInstance>> for TownPaths {
+	fn from(value: Vec<&EntityInstance>) -> Self {
+		let mut routes = HashMap::with_capacity(value.len() * 2);
+
+		'process_entity: for entity in value {
+			if entity.identifier != String::from("TradeRoute") {
+				continue;
+			}
+
+			let mut from_place = None;
+			let mut to_place = None;
+			let mut nodes: Vec<RouteNode> = Vec::new();
+
+			for field in &entity.field_instances {
+				match &*field.identifier {
+					"from" => match &field.value {
+						Some(Value::String(string)) => from_place = Some((*string).clone()),
+						_ => continue 'process_entity,
+					},
+					"to" => match &field.value {
+						Some(Value::String(string)) => to_place = Some((*string).clone()),
+						_ => continue 'process_entity,
+					},
+					"nodes" => {
+						if let Some(field_value) = &field.value {
+							nodes = match serde_json::from_value(field_value.clone()) {
+								Ok(val) => val,
+								_ => continue 'process_entity,
+							};
+						}
+					}
+					_ => {}
+				}
+			}
+
+			if let (Some(from), Some(to)) = (from_place, to_place) {
+				let mut route_entry = routes.entry(from.clone()).or_insert_with(|| Destinations {
+					source: from.clone(),
+					routes: Default::default(),
+				});
+				route_entry.routes.insert(
+					to.clone(),
+					Route {
+						nodes: nodes.clone(),
+					},
+				);
+
+				let mut route_entry = routes.entry(to.clone()).or_insert_with(|| Destinations {
+					source: to.clone(),
+					routes: Default::default(),
+				});
+				route_entry.routes.insert(
+					from.clone(),
+					Route {
+						nodes: nodes.iter().rev().cloned().collect(),
+					},
+				);
+			}
+		}
+
+		TownPaths { routes }
+	}
+}
diff --git a/game_core/src/world/travel.rs b/game_core/src/world/travel.rs
new file mode 100644
index 0000000000000000000000000000000000000000..157bcda0d3d780466204adefcacade95635fca50
--- /dev/null
+++ b/game_core/src/world/travel.rs
@@ -0,0 +1,38 @@
+use std::ops::SubAssign;
+
+use bevy::math::Vec3Swizzles;
+use bevy::prelude::*;
+
+use crate::world::towns::{CurrentResidence, TravelPath, TravelTarget};
+use crate::world::PathingResult;
+
+pub fn tick_travelling_merchant(
+	mut commands: Commands,
+	time: Res<Time>,
+	mut merchant_query: Query<(
+		Entity,
+		&mut Transform,
+		&mut TravelPath,
+		&mut TravelTarget,
+		&mut CurrentResidence,
+	)>,
+) {
+	let delta = time.delta_seconds();
+	for (entity, mut transform, mut path, mut target, mut residence) in &mut merchant_query {
+		let step = path.get_step(transform.translation.xy(), 16.0) * delta;
+		transform.translation.sub_assign(step.extend(0.0));
+
+		match path.check_position(transform.translation.xy()) {
+			PathingResult::PathComplete => {
+				commands
+					.entity(entity)
+					.remove::<(TravelPath, TravelTarget)>();
+				*residence = CurrentResidence::RestingAt(target.0.clone());
+			}
+			PathingResult::NextNode => {
+				path.increment_indexes();
+			}
+			PathingResult::Travelling => {}
+		}
+	}
+}
diff --git a/game_core/src/world/utils.rs b/game_core/src/world/utils.rs
new file mode 100644
index 0000000000000000000000000000000000000000..63abfb2914481d6d1c9d2301cb1f3c8fd2272db5
--- /dev/null
+++ b/game_core/src/world/utils.rs
@@ -0,0 +1,42 @@
+use bevy::prelude::{Component, Resource};
+use ldtk_rust::EntityInstance;
+use num_traits::AsPrimitive;
+
+pub const TILE_SCALE_INT: i64 = 4;
+pub const TILE_SCALE_F32: f32 = 4.0;
+
+pub fn px_to_grid<T: AsPrimitive<i64>>(t: T) -> i64 {
+	t.as_() / TILE_SCALE_INT
+}
+
+pub fn grid_to_px<T: AsPrimitive<f32>>(t: T) -> f32 {
+	t.as_() * TILE_SCALE_F32 + (TILE_SCALE_F32 / 2.0)
+}
+
+pub fn entity_to_worldspace(level_height: i64, entity: &EntityInstance) -> (f32, f32) {
+	let centre_align_pixel_x = grid_to_px(entity.grid[0]) - (TILE_SCALE_F32 / 2.0);
+	let centre_align_pixel_y = grid_to_px(entity.grid[1]) - (TILE_SCALE_F32 / 2.0);
+	let inverted_pixel_y = level_height as f32 - centre_align_pixel_y - TILE_SCALE_F32;
+	let box_aligned_x = centre_align_pixel_x + (entity.width / 2) as f32;
+	let box_aligned_y = inverted_pixel_y - (entity.height / 2) as f32;
+
+	(box_aligned_x, box_aligned_y)
+}
+
+#[derive(Component)]
+pub struct WorldLinked;
+
+#[derive(Default, Resource, Clone)]
+pub struct ActiveLevel {
+	pub map: String,
+	pub dirty: bool,
+}
+
+impl ActiveLevel {
+	pub fn new<T: ToString>(map: T) -> Self {
+		ActiveLevel {
+			map: map.to_string(),
+			dirty: false,
+		}
+	}
+}
diff --git a/game_core/src/world/world_query.rs b/game_core/src/world/world_query.rs
new file mode 100644
index 0000000000000000000000000000000000000000..5d646ac4f32bd721980b6e43d945086435feb276
--- /dev/null
+++ b/game_core/src/world/world_query.rs
@@ -0,0 +1,154 @@
+use std::marker::PhantomData;
+
+use bevy::ecs::system::SystemParam;
+use bevy::prelude::*;
+use ldtk_rust::{EntityInstance, LayerInstance, Level, TileInstance};
+use num_traits::AsPrimitive;
+
+// use crate::assets::level_index::LevelIndex;
+use crate::assets::{LdtkProject, LevelIndex};
+use crate::system::utilities::{f32_max, f32_min};
+use crate::world::utils::{ActiveLevel, TILE_SCALE_F32, TILE_SCALE_INT};
+
+#[derive(SystemParam)]
+pub struct MapQuery<'w, 's> {
+	assets: Res<'w, Assets<LdtkProject>>,
+	active: Option<Res<'w, ActiveLevel>>,
+	index: Res<'w, LevelIndex>,
+	#[system_param(ignore)]
+	_e: PhantomData<&'s ()>,
+}
+
+pub struct TileRef<'a> {
+	pub tile: &'a TileInstance,
+}
+
+impl<'a> TileRef<'a> {
+	pub fn new(tile: &'a TileInstance) -> Self {
+		TileRef { tile }
+	}
+}
+
+impl<'a> TileRef<'a> {
+	pub fn gid(&self) -> usize {
+		(self.tile.src[0] * self.tile.src[1]).unsigned_abs() as usize
+	}
+}
+
+pub struct LayerRef<'a> {
+	pub idx: usize,
+	pub layer: &'a LayerInstance,
+}
+
+impl<'a> LayerRef<'a> {
+	pub fn new(idx: usize, layer: &'a LayerInstance) -> Self {
+		LayerRef { layer, idx }
+	}
+	pub fn has_tiles(&self) -> bool {
+		!(self.layer.auto_layer_tiles.is_empty() && self.layer.grid_tiles.is_empty())
+	}
+	pub fn for_each_tile(&self, mut cb: impl FnMut(i64, i64, TileRef)) {
+		self.layer
+			.grid_tiles
+			.iter()
+			.chain(self.layer.auto_layer_tiles.iter())
+			.for_each(|tile: &TileInstance| {
+				let (x, y) = match tile.px.as_slice() {
+					&[x, y] => (x, y),
+					_ => {
+						return;
+					}
+				};
+
+				cb(x, y, TileRef::new(tile));
+			});
+	}
+	pub fn get_z_delta(&self) -> f32 {
+		(self.idx as f32) / 100.0
+	}
+
+	// pub fn get_tile_metadata_of(&self, tile_id: i64) {
+	// 	self.layer.til
+	// }
+}
+
+#[derive(Copy, Clone, Debug)]
+pub struct CameraBounds {
+	pub left: f32,
+	pub top: f32,
+	pub bottom: f32,
+	pub right: f32,
+}
+
+impl CameraBounds {
+	pub fn get_min_x(&self, camera_width: f32) -> f32 {
+		self.left + (camera_width / 2.0) - (TILE_SCALE_F32 / 2.0)
+	}
+	pub fn get_max_x(&self, camera_width: f32) -> f32 {
+		self.right - (camera_width / 2.0) - (TILE_SCALE_F32 / 2.0)
+	}
+	pub fn get_min_y(&self, camera_height: f32) -> f32 {
+		self.bottom + (camera_height / 2.0) - (TILE_SCALE_F32 / 2.0)
+	}
+	pub fn get_max_y(&self, camera_height: f32) -> f32 {
+		self.top - (camera_height / 2.0) - (TILE_SCALE_F32 / 2.0)
+	}
+}
+
+impl<'w, 's> MapQuery<'w, 's> {
+	// --- We put our logic in static accessors because we might source a level other
+	// --- than the currently active one. 'active' methods are a convenience to
+	// --- call the static accessors on whatever the current level is
+
+	pub fn for_each_layer_of(level: &Level, mut cb: impl FnMut(LayerRef)) {
+		if let Some(layers) = level.layer_instances.as_ref() {
+			for layer in layers.iter().rev().enumerate() {
+				cb(LayerRef::new(layer.0, layer.1));
+			}
+		}
+	}
+
+	pub fn get_entities_of(level: &Level) -> Vec<&EntityInstance> {
+		level
+			.layer_instances
+			.as_ref()
+			.map(|layers| {
+				layers
+					.iter()
+					.flat_map(|layer| layer.entity_instances.iter())
+					.collect()
+			})
+			.unwrap_or_default()
+	}
+
+	pub fn get_camera_bounds_of(level: &Level) -> CameraBounds {
+		CameraBounds {
+			left: 0.0,
+			top: level.px_hei as f32,
+			bottom: 0.0,
+			right: level.px_wid as f32,
+		}
+	}
+
+	pub fn get_active_level(&self) -> Option<&Level> {
+		self.active
+			.as_ref()
+			.and_then(|index| self.index.get(&index.map))
+	}
+
+	pub fn get_entities(&self) -> Vec<&EntityInstance> {
+		self.get_active_level()
+			.map(|level| MapQuery::get_entities_of(level))
+			.unwrap_or_default()
+	}
+
+	pub fn get_camera_bounds(&self) -> Option<CameraBounds> {
+		self.get_active_level().map(MapQuery::get_camera_bounds_of)
+	}
+
+	pub fn for_each_layer(&self, mut cb: impl FnMut(LayerRef)) {
+		if let Some(level) = self.get_active_level() {
+			Self::for_each_layer_of(level, cb);
+		}
+	}
+}
diff --git a/micro_asset_io/src/apack_loader.rs b/micro_asset_io/src/apack_loader.rs
index 2417fc45c3e15d9a4b579f81394589b2cab170ef..ecc98bc727bfc00fb03c176b7c84c16880066dff 100644
--- a/micro_asset_io/src/apack_loader.rs
+++ b/micro_asset_io/src/apack_loader.rs
@@ -21,6 +21,7 @@ pub struct APackLoader;
 #[uuid = "bce5dd7a-726c-11ed-b53f-071b386b4896"]
 pub struct APack {
 	pub processed: bool,
+	pub loaded: bool,
 	pub compressed_bytes: Vec<u8>,
 	pub vfs: HashMap<String, Vec<u8>>,
 }
@@ -68,6 +69,7 @@ impl DerefMut for ClonableAPack {
 impl Clone for ClonableAPack {
 	fn clone(&self) -> Self {
 		ClonableAPack(APack {
+			loaded: self.loaded,
 			processed: self.processed,
 			compressed_bytes: self.compressed_bytes.clone(),
 			vfs: self.vfs.clone(),
@@ -83,6 +85,7 @@ impl Into<APack> for ClonableAPack {
 impl APack {
 	pub fn new(bytes: Vec<u8>) -> APack {
 		APack {
+			loaded: false,
 			processed: false,
 			compressed_bytes: bytes,
 			vfs: Default::default(),
@@ -97,6 +100,7 @@ impl APack {
 
 	pub fn clone_ref(apack: &APack) -> APack {
 		APack {
+			loaded: apack.loaded,
 			processed: apack.processed,
 			compressed_bytes: apack.compressed_bytes.clone(),
 			vfs: apack.vfs.clone(),
diff --git a/raw_assets/.gitignore b/raw_assets/.gitignore
index fbeaca80e3ce81fa39959a7c162c6f3e2bccd7ff..6d9884b663e9accb8ff0f70cee0dd3736afd8c43 100644
--- a/raw_assets/.gitignore
+++ b/raw_assets/.gitignore
@@ -1,6 +1,8 @@
 backgrounds/
 fonts/
 sprites/
+ldtk/
+ui/
 
 !.gitignore
 !manifest.toml
\ No newline at end of file
diff --git a/raw_assets/manifest.toml b/raw_assets/manifest.toml
index c6536724a339df51bbfcb7d77dcea613885082c0..0bd6b567f23769667a42ccee6ee51c1500a6b2a9 100644
--- a/raw_assets/manifest.toml
+++ b/raw_assets/manifest.toml
@@ -3,11 +3,52 @@ path = "sprites/overworld.png"
 name = "overworld"
 tiles = { size = 4, columns = 32, rows = 64 }
 
+[[spritesheets]]
+path = "sprites/characters.png"
+name = "characters"
+tiles = { size = 8, columns = 16, rows = 16 }
+
 [[images]]
 path = "backgrounds/main_menu.png"
 name = "menu_background"
 format = "png"
 
+[[images]]
+path = "ui/panel.png"
+name = "panel"
+format = "png"
+
+[[images]]
+path = "ui/scroll_panel.png"
+name = "scroll_panel"
+format = "png"
+
+[[images]]
+path = "ui/button_idle.png"
+name = "button_idle"
+format = "png"
+
+[[images]]
+path = "ui/button_active.png"
+name = "button_active"
+format = "png"
+
+[[images]]
+path = "ui/button_down.png"
+name = "button_down"
+format = "png"
+
+[[images]]
+path = "ui/button_disabled.png"
+name = "button_disabled"
+format = "png"
+
 [[fonts]]
-path = "fonts/CompassPro.ttf"
-name = "default"
\ No newline at end of file
+name = "compass_pro"
+ttf = "fonts/CompassPro.ttf"
+image = "fonts/CompassPro.png"
+msdf = "fonts/CompassPro.kayak_font"
+
+[[ldtk]]
+path = "ldtk/overworld_maps.ldtk"
+name = "overworld"
\ No newline at end of file