diff --git a/.gitattributes b/.gitattributes
index 24a8e87939aa53cdd833f6be7610cb4972e063ad..d70df3a7aba62e74f20d7b30086de4373bdddbd8 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1 +1,2 @@
 *.png filter=lfs diff=lfs merge=lfs -text
+*.ldtk filter=lfs diff=lfs merge=lfs -text
diff --git a/Cargo.lock b/Cargo.lock
index fd483eaacec4653e5b991c28121a8ccc0943131f..ee925fb3a9cbcec0830f42b814b28c721670d827 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -200,6 +200,12 @@ dependencies = [
  "num-traits",
 ]
 
+[[package]]
+name = "arrayref"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545"
+
 [[package]]
 name = "arrayvec"
 version = "0.7.4"
@@ -309,25 +315,6 @@ dependencies = [
  "bevy_ecs",
 ]
 
-[[package]]
-name = "bevy_animation"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d130cb8b7e2b81304591c5c8e511accd2df58b8d8185ab4836ed2f377e6a61f"
-dependencies = [
- "bevy_app",
- "bevy_asset",
- "bevy_core",
- "bevy_ecs",
- "bevy_hierarchy",
- "bevy_math",
- "bevy_reflect",
- "bevy_render",
- "bevy_time",
- "bevy_transform",
- "bevy_utils",
-]
-
 [[package]]
 name = "bevy_app"
 version = "0.11.0"
@@ -372,26 +359,6 @@ dependencies = [
  "web-sys",
 ]
 
-[[package]]
-name = "bevy_audio"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6bade3f5389f9463e150af874aebe672b5101df4268d28b0109a66f9cdce56e"
-dependencies = [
- "anyhow",
- "bevy_app",
- "bevy_asset",
- "bevy_derive",
- "bevy_ecs",
- "bevy_math",
- "bevy_reflect",
- "bevy_transform",
- "bevy_utils",
- "oboe",
- "parking_lot",
- "rodio",
-]
-
 [[package]]
 name = "bevy_core"
 version = "0.11.0"
@@ -405,6 +372,7 @@ dependencies = [
  "bevy_tasks",
  "bevy_utils",
  "bytemuck",
+ "serde",
 ]
 
 [[package]]
@@ -507,22 +475,6 @@ dependencies = [
  "encase_derive_impl",
 ]
 
-[[package]]
-name = "bevy_gilrs"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b877a371caa64edd6ec5d66b47c67b9e9e9acff2f3bcc51e31e175463e89f6ba"
-dependencies = [
- "bevy_app",
- "bevy_ecs",
- "bevy_input",
- "bevy_log",
- "bevy_time",
- "bevy_utils",
- "gilrs",
- "thiserror",
-]
-
 [[package]]
 name = "bevy_gizmos"
 version = "0.11.0"
@@ -535,7 +487,6 @@ dependencies = [
  "bevy_core_pipeline",
  "bevy_ecs",
  "bevy_math",
- "bevy_pbr",
  "bevy_reflect",
  "bevy_render",
  "bevy_sprite",
@@ -543,37 +494,6 @@ dependencies = [
  "bevy_utils",
 ]
 
-[[package]]
-name = "bevy_gltf"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f09b699698a2f5843ef63064010a5e7783403f99a697a04f41a2f8141cb4245d"
-dependencies = [
- "anyhow",
- "base64",
- "bevy_animation",
- "bevy_app",
- "bevy_asset",
- "bevy_core",
- "bevy_core_pipeline",
- "bevy_ecs",
- "bevy_hierarchy",
- "bevy_log",
- "bevy_math",
- "bevy_pbr",
- "bevy_reflect",
- "bevy_render",
- "bevy_scene",
- "bevy_tasks",
- "bevy_transform",
- "bevy_utils",
- "gltf",
- "percent-encoding",
- "serde",
- "serde_json",
- "thiserror",
-]
-
 [[package]]
 name = "bevy_hierarchy"
 version = "0.11.0"
@@ -600,6 +520,7 @@ dependencies = [
  "bevy_math",
  "bevy_reflect",
  "bevy_utils",
+ "serde",
  "thiserror",
 ]
 
@@ -610,23 +531,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0f232e7bd2566abd05656789e3c6278a5ca2a24f1232dff525e5b0233a99a610"
 dependencies = [
  "bevy_a11y",
- "bevy_animation",
  "bevy_app",
  "bevy_asset",
- "bevy_audio",
  "bevy_core",
  "bevy_core_pipeline",
  "bevy_derive",
  "bevy_diagnostic",
  "bevy_ecs",
- "bevy_gilrs",
  "bevy_gizmos",
- "bevy_gltf",
  "bevy_hierarchy",
  "bevy_input",
  "bevy_log",
  "bevy_math",
- "bevy_pbr",
  "bevy_ptr",
  "bevy_reflect",
  "bevy_render",
@@ -702,29 +618,6 @@ dependencies = [
  "glam",
 ]
 
-[[package]]
-name = "bevy_pbr"
-version = "0.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3efec2ae4b4f9fd38b82b93350499dac2dc6f07e63ef50a03c00c52075e2dea8"
-dependencies = [
- "bevy_app",
- "bevy_asset",
- "bevy_core_pipeline",
- "bevy_derive",
- "bevy_ecs",
- "bevy_math",
- "bevy_reflect",
- "bevy_render",
- "bevy_transform",
- "bevy_utils",
- "bevy_window",
- "bitflags 2.3.3",
- "bytemuck",
- "naga_oil",
- "radsort",
-]
-
 [[package]]
 name = "bevy_ptr"
 version = "0.11.0"
@@ -813,12 +706,10 @@ dependencies = [
  "hexasphere",
  "image",
  "js-sys",
- "ktx2",
  "naga",
  "naga_oil",
  "parking_lot",
  "regex",
- "ruzstd",
  "serde",
  "smallvec",
  "thiserror",
@@ -936,6 +827,7 @@ dependencies = [
  "bevy_reflect",
  "bevy_utils",
  "crossbeam-channel",
+ "serde",
  "thiserror",
 ]
 
@@ -950,6 +842,7 @@ dependencies = [
  "bevy_hierarchy",
  "bevy_math",
  "bevy_reflect",
+ "serde",
 ]
 
 [[package]]
@@ -1023,6 +916,7 @@ dependencies = [
  "bevy_reflect",
  "bevy_utils",
  "raw-window-handle",
+ "serde",
 ]
 
 [[package]]
@@ -1160,6 +1054,20 @@ version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
 
+[[package]]
+name = "calloop"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52e0d00eb1ea24371a97d2da6201c6747a633dc6dc1988ef503403b4c59504a8"
+dependencies = [
+ "bitflags 1.3.2",
+ "log",
+ "nix 0.25.1",
+ "slotmap",
+ "thiserror",
+ "vec_map",
+]
+
 [[package]]
 name = "cargo-emit"
 version = "0.2.1"
@@ -1429,7 +1337,7 @@ dependencies = [
  "autocfg",
  "cfg-if",
  "crossbeam-utils",
- "memoffset",
+ "memoffset 0.9.0",
  "scopeguard",
 ]
 
@@ -1541,6 +1449,15 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
 
+[[package]]
+name = "dlib"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
+dependencies = [
+ "libloading 0.7.4",
+]
+
 [[package]]
 name = "downcast-rs"
 version = "1.2.0"
@@ -1725,11 +1642,14 @@ dependencies = [
  "bevy",
  "bevy_embedded_assets",
  "bevy_rapier2d",
- "micro_banimate",
  "micro_bevy_web_utils",
  "micro_bevy_world_utils",
  "micro_ldtk",
  "micro_musicbox",
+ "num-traits",
+ "paste",
+ "serde",
+ "serde_json",
 ]
 
 [[package]]
@@ -1745,39 +1665,6 @@ dependencies = [
  "wasm-bindgen",
 ]
 
-[[package]]
-name = "gilrs"
-version = "0.10.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62fd19844d0eb919aca41d3e4ea0e0b6bf60e1e827558b101c269015b8f5f27a"
-dependencies = [
- "fnv",
- "gilrs-core",
- "log",
- "uuid",
- "vec_map",
-]
-
-[[package]]
-name = "gilrs-core"
-version = "0.5.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f85b0f27572f0560cfc4a067a2978a4a490f9fa5cf1326d30b142a288312a965"
-dependencies = [
- "core-foundation",
- "io-kit-sys",
- "js-sys",
- "libc",
- "libudev-sys",
- "log",
- "nix 0.26.2",
- "uuid",
- "vec_map",
- "wasm-bindgen",
- "web-sys",
- "windows 0.48.0",
-]
-
 [[package]]
 name = "gimli"
 version = "0.27.3"
@@ -1813,41 +1700,6 @@ dependencies = [
  "web-sys",
 ]
 
-[[package]]
-name = "gltf"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f4fe8d5192923fbd783c15e74627de8e27c97e1e3dec22bf54515a407249febf"
-dependencies = [
- "byteorder",
- "gltf-json",
- "lazy_static",
-]
-
-[[package]]
-name = "gltf-derive"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec223c88f017861193ae128239aff8fbc4478f38a036d9d7b2ce10a52b46b1f2"
-dependencies = [
- "inflections",
- "proc-macro2 1.0.66",
- "quote 1.0.31",
- "syn 2.0.27",
-]
-
-[[package]]
-name = "gltf-json"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b1ba7523fcf32541f4aec96e13024c255d928eab3223f99ab945045f2a6de18"
-dependencies = [
- "gltf-derive",
- "serde",
- "serde_derive",
- "serde_json",
-]
-
 [[package]]
 name = "glyph_brush_layout"
 version = "0.2.3"
@@ -2021,12 +1873,6 @@ dependencies = [
  "hashbrown 0.14.0",
 ]
 
-[[package]]
-name = "inflections"
-version = "1.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a257582fdcde896fd96463bf2d40eefea0580021c0712a0e2b028b60b47a837a"
-
 [[package]]
 name = "inotify"
 version = "0.9.6"
@@ -2059,16 +1905,6 @@ dependencies = [
  "web-sys",
 ]
 
-[[package]]
-name = "io-kit-sys"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b2d4429acc1deff0fbdece0325b4997bdb02b2c245ab7023fd5deca0f6348de"
-dependencies = [
- "core-foundation-sys 0.8.4",
- "mach2",
-]
-
 [[package]]
 name = "itoa"
 version = "1.0.9"
@@ -2171,15 +2007,6 @@ dependencies = [
  "libc",
 ]
 
-[[package]]
-name = "ktx2"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87d65e08a9ec02e409d27a0139eaa6b9756b4d81fe7cde71f6941a83730ce838"
-dependencies = [
- "bitflags 1.3.2",
-]
-
 [[package]]
 name = "lazy_static"
 version = "1.4.0"
@@ -2192,17 +2019,6 @@ version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
 
-[[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.147"
@@ -2235,16 +2051,6 @@ version = "0.2.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4"
 
-[[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.10"
@@ -2304,6 +2110,24 @@ version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
 
+[[package]]
+name = "memmap2"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "memoffset"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
+dependencies = [
+ "autocfg",
+]
+
 [[package]]
 name = "memoffset"
 version = "0.9.0"
@@ -2336,17 +2160,6 @@ dependencies = [
  "fastrand",
 ]
 
-[[package]]
-name = "micro_banimate"
-version = "0.6.0-rc.1"
-source = "git+https://lab.lcr.gr/microhacks/micro-banimate.git?rev=33e56278471f32cbcd1a843aca83298c8b80c7e3#33e56278471f32cbcd1a843aca83298c8b80c7e3"
-dependencies = [
- "anyhow",
- "bevy",
- "serde",
- "serde_json",
-]
-
 [[package]]
 name = "micro_bevy_web_utils"
 version = "0.3.0"
@@ -2539,18 +2352,20 @@ dependencies = [
  "bitflags 1.3.2",
  "cfg-if",
  "libc",
+ "memoffset 0.6.5",
 ]
 
 [[package]]
 name = "nix"
-version = "0.26.2"
+version = "0.25.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a"
+checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4"
 dependencies = [
+ "autocfg",
  "bitflags 1.3.2",
  "cfg-if",
  "libc",
- "static_assertions",
+ "memoffset 0.6.5",
 ]
 
 [[package]]
@@ -2834,15 +2649,6 @@ 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.18.0"
@@ -3188,16 +2994,6 @@ version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e5864e7ef1a6b7bcf1d6ca3f655e65e724ed3b52546a0d0a663c991522f552ea"
 
-[[package]]
-name = "rodio"
-version = "0.17.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bdf1d4dea18dff2e9eb6dca123724f8b60ef44ad74a9ad283cdfe025df7e73fa"
-dependencies = [
- "cpal",
- "lewton",
-]
-
 [[package]]
 name = "ron"
 version = "0.8.0"
@@ -3221,17 +3017,6 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
 
-[[package]]
-name = "ruzstd"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac3ffab8f9715a0d455df4bbb9d21e91135aab3cd3ca187af0cd0c3c3f868fdc"
-dependencies = [
- "byteorder",
- "thiserror-core",
- "twox-hash",
-]
-
 [[package]]
 name = "ryu"
 version = "1.0.15"
@@ -3256,12 +3041,31 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "scoped-tls"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
+
 [[package]]
 name = "scopeguard"
 version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
 
+[[package]]
+name = "sctk-adwaita"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cda4e97be1fd174ccc2aae81c8b694e803fa99b34e8fd0f057a9d70698e3ed09"
+dependencies = [
+ "ab_glyph",
+ "log",
+ "memmap2",
+ "smithay-client-toolkit",
+ "tiny-skia",
+]
+
 [[package]]
 name = "serde"
 version = "1.0.174"
@@ -3354,6 +3158,25 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "smithay-client-toolkit"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f307c47d32d2715eb2e0ece5589057820e0e5e70d07c247d1063e844e107f454"
+dependencies = [
+ "bitflags 1.3.2",
+ "calloop",
+ "dlib",
+ "lazy_static",
+ "log",
+ "memmap2",
+ "nix 0.24.3",
+ "pkg-config",
+ "wayland-client",
+ "wayland-cursor",
+ "wayland-protocols",
+]
+
 [[package]]
 name = "smol_str"
 version = "0.2.0"
@@ -3391,6 +3214,12 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
 
+[[package]]
+name = "strict-num"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
+
 [[package]]
 name = "strsim"
 version = "0.7.0"
@@ -3480,26 +3309,6 @@ dependencies = [
  "thiserror-impl",
 ]
 
-[[package]]
-name = "thiserror-core"
-version = "1.0.38"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d97345f6437bb2004cd58819d8a9ef8e36cdd7661c2abc4bbde0a7c40d9f497"
-dependencies = [
- "thiserror-core-impl",
-]
-
-[[package]]
-name = "thiserror-core-impl"
-version = "1.0.38"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "10ac1c5050e43014d16b2f94d0d2ce79e65ffdd8b38d8048f9c8f6a8a6da62ac"
-dependencies = [
- "proc-macro2 1.0.66",
- "quote 1.0.31",
- "syn 1.0.109",
-]
-
 [[package]]
 name = "thiserror-impl"
 version = "1.0.44"
@@ -3522,19 +3331,29 @@ dependencies = [
 ]
 
 [[package]]
-name = "tinyvec"
-version = "1.6.0"
+name = "tiny-skia"
+version = "0.8.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+checksum = "df8493a203431061e901613751931f047d1971337153f96d0e5e363d6dbf6a67"
 dependencies = [
- "tinyvec_macros",
+ "arrayref",
+ "arrayvec",
+ "bytemuck",
+ "cfg-if",
+ "png",
+ "tiny-skia-path",
 ]
 
 [[package]]
-name = "tinyvec_macros"
-version = "0.1.1"
+name = "tiny-skia-path"
+version = "0.8.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+checksum = "adbfb5d3f3dd57a0e11d12f4f13d4ebbbc1b5c15b7ab0a156d030b21da5f677c"
+dependencies = [
+ "arrayref",
+ "bytemuck",
+ "strict-num",
+]
 
 [[package]]
 name = "toml_datetime"
@@ -3632,16 +3451,6 @@ version = "0.19.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a464a4b34948a5f67fddd2b823c62d9d92e44be75058b99939eae6c5b6960b33"
 
-[[package]]
-name = "twox-hash"
-version = "1.6.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675"
-dependencies = [
- "cfg-if",
- "static_assertions",
-]
-
 [[package]]
 name = "typenum"
 version = "1.16.0"
@@ -3788,6 +3597,57 @@ version = "0.2.87"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
 
+[[package]]
+name = "wayland-client"
+version = "0.29.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f3b068c05a039c9f755f881dc50f01732214f5685e379829759088967c46715"
+dependencies = [
+ "bitflags 1.3.2",
+ "downcast-rs",
+ "libc",
+ "nix 0.24.3",
+ "scoped-tls",
+ "wayland-commons",
+ "wayland-scanner",
+ "wayland-sys",
+]
+
+[[package]]
+name = "wayland-commons"
+version = "0.29.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8691f134d584a33a6606d9d717b95c4fa20065605f798a3f350d78dced02a902"
+dependencies = [
+ "nix 0.24.3",
+ "once_cell",
+ "smallvec",
+ "wayland-sys",
+]
+
+[[package]]
+name = "wayland-cursor"
+version = "0.29.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6865c6b66f13d6257bef1cd40cbfe8ef2f150fb8ebbdb1e8e873455931377661"
+dependencies = [
+ "nix 0.24.3",
+ "wayland-client",
+ "xcursor",
+]
+
+[[package]]
+name = "wayland-protocols"
+version = "0.29.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b950621f9354b322ee817a23474e479b34be96c2e909c14f7bc0100e9a970bc6"
+dependencies = [
+ "bitflags 1.3.2",
+ "wayland-client",
+ "wayland-commons",
+ "wayland-scanner",
+]
+
 [[package]]
 name = "wayland-scanner"
 version = "0.29.5"
@@ -3799,6 +3659,17 @@ dependencies = [
  "xml-rs",
 ]
 
+[[package]]
+name = "wayland-sys"
+version = "0.29.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be12ce1a3c39ec7dba25594b97b42cb3195d54953ddb9d3d95a7c3902bc6e9d4"
+dependencies = [
+ "dlib",
+ "lazy_static",
+ "pkg-config",
+]
+
 [[package]]
 name = "web-sys"
 version = "0.3.64"
@@ -3976,15 +3847,6 @@ dependencies = [
  "windows-targets 0.42.2",
 ]
 
-[[package]]
-name = "windows"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
-dependencies = [
- "windows-targets 0.48.1",
-]
-
 [[package]]
 name = "windows-implement"
 version = "0.44.0"
@@ -4162,7 +4024,12 @@ dependencies = [
  "percent-encoding",
  "raw-window-handle",
  "redox_syscall 0.3.5",
+ "sctk-adwaita",
+ "smithay-client-toolkit",
  "wasm-bindgen",
+ "wayland-client",
+ "wayland-commons",
+ "wayland-protocols",
  "wayland-scanner",
  "web-sys",
  "windows-sys 0.45.0",
@@ -4189,6 +4056,15 @@ dependencies = [
  "pkg-config",
 ]
 
+[[package]]
+name = "xcursor"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "463705a63313cd4301184381c5e8042f0a7e9b4bb63653f216311d4ae74690b7"
+dependencies = [
+ "nom",
+]
+
 [[package]]
 name = "xi-unicode"
 version = "0.3.0"
diff --git a/Cargo.toml b/Cargo.toml
index bed5ea35acf3f2e09d8575ad6c7dafb0493ae9b9..a9df19cc8c0d3fc65cf7b26ba5bd55b995b55ffd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -13,12 +13,32 @@ opt-level = 1
 opt-level = 3
 
 [workspace.dependencies]
-bevy = "0.11.0"
-bevy_embedded_assets = "0.8.0"
-
 micro_bevy_world_utils = "0.3.0"
 micro_bevy_web_utils = "0.3.0"
 micro_ldtk = { version = "0.6.1", default-features = false, features = ["ldtk_1_3_0", "autotile"] }
-micro_banimate = { git = "https://lab.lcr.gr/microhacks/micro-banimate.git", rev = "33e56278471f32cbcd1a843aca83298c8b80c7e3" }
 micro_musicbox = "0.7.0"
-bevy_rapier2d = { version = "0.22.0", features = ["simd-stable", "wasm-bindgen"]}
\ No newline at end of file
+
+serde = "1.0.164"
+serde_json = "1.0.96"
+num-traits = "0.2.15"
+paste = "1.0.12"
+
+bevy_rapier2d = { version = "0.22.0", features = ["simd-stable", "wasm-bindgen"]}
+bevy_embedded_assets = "0.8.0"
+
+[workspace.dependencies.bevy]
+version = "0.11.0"
+default-features = false
+features = [
+	"bevy_asset",
+	"bevy_sprite",
+	"bevy_winit",
+	"png",
+	"hdr",
+	"x11",
+	"wayland",
+	"serialize",
+	"filesystem_watcher",
+	"bevy_core_pipeline",
+	"bevy_ui"
+]
diff --git a/assets/sprites/characters.png b/assets/sprites/characters.png
new file mode 100644
index 0000000000000000000000000000000000000000..9dc045db769d4a1e3df84186c1a1ce635648fc6d
--- /dev/null
+++ b/assets/sprites/characters.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1702606cb8510ac8454dd93204f3f460d06a0d5071b080c86e856bb684abb929
+size 1973
diff --git a/assets/sprites/world.png b/assets/sprites/world.png
new file mode 100644
index 0000000000000000000000000000000000000000..e917b8fabbd77c5668553d489c87770dcda13677
--- /dev/null
+++ b/assets/sprites/world.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:eb63be0e3ef42965bba1364620643497fa267fa18894debb85a74c8f3faa59b5
+size 43342
diff --git a/assets/world.ldtk b/assets/world.ldtk
new file mode 100644
index 0000000000000000000000000000000000000000..7baf762e183bfc8806dc7bd2428d640e554a0726
--- /dev/null
+++ b/assets/world.ldtk
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:768f14ee00b4997d06c01dca23d1d74620bd0a7b5d1e210a189c22b72d53a6d8
+size 93007
diff --git a/game_core/Cargo.toml b/game_core/Cargo.toml
index 95c541ca8f5679041f528c51fb34633ce32a7662..5d89f4fc49a2301f2f9ff85d76c0dd85bbcd742f 100644
--- a/game_core/Cargo.toml
+++ b/game_core/Cargo.toml
@@ -15,6 +15,10 @@ bevy_embedded_assets = { workspace = true, optional = true }
 micro_bevy_world_utils.workspace = true
 micro_bevy_web_utils.workspace = true
 micro_ldtk.workspace = true
-micro_banimate.workspace = true
 micro_musicbox.workspace = true
-bevy_rapier2d.workspace = true
\ No newline at end of file
+bevy_rapier2d.workspace = true
+
+serde.workspace = true
+serde_json.workspace = true
+num-traits.workspace = true
+paste.workspace = true
\ No newline at end of file
diff --git a/game_core/src/assets/loader.rs b/game_core/src/assets/loader.rs
index 4d26426c8c46efc3e52eb6211f3fe49c9f61d5fc..7ad3ff274a92f9c9a03540edde7d0b083f507c51 100644
--- a/game_core/src/assets/loader.rs
+++ b/game_core/src/assets/loader.rs
@@ -4,7 +4,6 @@ use bevy::asset::LoadState;
 use bevy::ecs::system::SystemParam;
 use bevy::prelude::*;
 use bevy::reflect::{TypePath, TypeUuid};
-use micro_banimate::definitions::AnimationSet;
 use micro_ldtk::LdtkProject;
 use micro_musicbox::prelude::AudioSource;
 
@@ -57,7 +56,6 @@ impl<'w, 's> AssetTypeLoader<'w, 's> {
 	load_basic_type!(load_images, Image => images);
 	load_basic_type!(load_audio, AudioSource => sounds);
 	load_basic_type!(load_font, Font => fonts);
-	load_basic_type!(load_animation, AnimationSet => animations);
 	load_basic_type!(load_ldtk, LdtkProject => ldtk_projects);
 
 	pub fn load_spritesheet(
diff --git a/game_core/src/assets/resources.rs b/game_core/src/assets/resources.rs
index b2cb80874fe022caabd0a9a6558e49810fd47fab..61b629b55325b4c30c196f48d42a221b4f930119 100644
--- a/game_core/src/assets/resources.rs
+++ b/game_core/src/assets/resources.rs
@@ -1,6 +1,5 @@
 use bevy::prelude::*;
 use bevy::utils::HashMap;
-use micro_banimate::definitions::AnimationSet;
 use micro_ldtk::LdtkProject;
 use micro_musicbox::prelude::AudioSource;
 use micro_musicbox::utilities::{SuppliesAudio, TrackType};
@@ -39,7 +38,6 @@ pub struct AssetHandles {
 	pub atlas: HashMap<String, Handle<TextureAtlas>>,
 	pub sounds: HashMap<String, Handle<AudioSource>>,
 	pub fonts: HashMap<String, Handle<Font>>,
-	pub animations: HashMap<String, Handle<AnimationSet>>,
 	pub ldtk_projects: HashMap<String, Handle<LdtkProject>>,
 }
 
@@ -70,7 +68,6 @@ impl AssetHandles {
 	fetch_wrapper!(atlas, TextureAtlas => atlas);
 	fetch_wrapper!(sound, AudioSource => sounds);
 	fetch_wrapper!(font, Font => fonts);
-	fetch_wrapper!(animation, AnimationSet => animations);
 	fetch_wrapper!(ldtk, LdtkProject => ldtk_projects);
 }
 
diff --git a/game_core/src/assets/startup.rs b/game_core/src/assets/startup.rs
index bf9a057ba120d5a291a93309db6541a8772ad80c..3b1e45cfc5dd1025cb555f08f9416e7777404777 100644
--- a/game_core/src/assets/startup.rs
+++ b/game_core/src/assets/startup.rs
@@ -13,11 +13,14 @@ pub fn start_preload_resources(
 }
 
 pub fn start_load_resources(mut loader: AssetTypeLoader) {
-	loader.load_animation(&[("animations/blob.anim.json", "blob")]);
+	loader.load_spritesheet(
+		&SpriteSheetConfig::squares(18, 32, 64),
+		&[("sprites/world.png", "world")],
+	);
 
 	loader.load_spritesheet(
-		&SpriteSheetConfig::rectangles(128, 256, 11, 2),
-		&[("sprites/beige_blob.png", "beige_blob")],
+		&SpriteSheetConfig::squares(24, 9, 3),
+		&[("sprites/characters.png", "characters")],
 	);
 }
 
diff --git a/game_core/src/entities/graphics.rs b/game_core/src/entities/graphics.rs
new file mode 100644
index 0000000000000000000000000000000000000000..4e1a6bc588ee3644f5135757f0b496c7022a352f
--- /dev/null
+++ b/game_core/src/entities/graphics.rs
@@ -0,0 +1,130 @@
+use crate::entities::{PhysicsSet, Velocity as KinVelocity};
+use crate::system::run_in_game;
+use bevy::prelude::*;
+use bevy_rapier2d::control::KinematicCharacterControllerOutput;
+use num_traits::Zero;
+
+#[derive(Clone, Debug, Default, Component, Reflect)]
+pub struct SimpleAnimation {
+	pub frames: Vec<usize>,
+	pub frame_time: f32,
+}
+
+#[derive(Clone, Debug, Default, Component, Reflect)]
+pub struct CharacterMotion {
+	idle: SimpleAnimation,
+	movement: SimpleAnimation,
+}
+
+#[derive(Copy, Clone, Debug, Default, Reflect, Eq, PartialEq)]
+pub enum AnimationName {
+	#[default]
+	Idle,
+	Movement,
+}
+
+#[derive(Copy, Clone, Debug, Default, Component, Reflect)]
+pub struct AnimationState {
+	current: AnimationName,
+	elapsed: f32,
+	idx: usize,
+}
+
+impl AnimationState {
+	pub fn reset_to(&mut self, name: AnimationName) {
+		self.current = name;
+		self.elapsed = 0.0;
+		self.idx = 0;
+	}
+}
+
+#[derive(Bundle)]
+pub struct SimpleAnimationBundle {
+	animations: CharacterMotion,
+	state: AnimationState,
+}
+
+impl SimpleAnimationBundle {
+	pub fn new(idle: SimpleAnimation, movement: SimpleAnimation) -> Self {
+		Self {
+			animations: CharacterMotion { idle, movement },
+			state: Default::default(),
+		}
+	}
+}
+
+pub fn process_kinematic_motion(
+	time: Res<Time>,
+	mut query: Query<(
+		&mut AnimationState,
+		&CharacterMotion,
+		&KinVelocity,
+		&KinematicCharacterControllerOutput,
+		&mut TextureAtlasSprite,
+	)>,
+) {
+	let time = time.delta_seconds();
+	for (mut state, anims, vel, controller, mut sprite) in &mut query {
+		let mut dirty = false;
+
+		state.elapsed += time;
+		let current = match state.current {
+			AnimationName::Idle => &anims.idle,
+			AnimationName::Movement => &anims.movement,
+		};
+
+		while state.elapsed > current.frame_time {
+			state.elapsed -= current.frame_time;
+			state.idx += 1;
+			dirty = true;
+		}
+
+		while state.idx >= current.frames.len() {
+			state.idx -= current.frames.len();
+		}
+
+		if controller.grounded {
+			if vel.x.is_zero() && state.current == AnimationName::Movement {
+				state.reset_to(AnimationName::Idle);
+				dirty = true;
+			} else if !vel.x.is_zero() && state.current == AnimationName::Idle {
+				state.reset_to(AnimationName::Movement);
+				dirty = true;
+			}
+		} else {
+			state.reset_to(AnimationName::Movement);
+			dirty = true;
+		}
+
+		if dirty {
+			if let Some(idx) = match state.current {
+				AnimationName::Idle => anims.idle.frames.get(state.idx),
+				AnimationName::Movement => anims.movement.frames.get(state.idx),
+			} {
+				sprite.index = *idx;
+			}
+		}
+	}
+}
+
+pub fn update_kinematic_sprite(mut query: Query<(&mut TextureAtlasSprite, &KinVelocity)>) {
+	for (mut sprite, vel) in &mut query {
+		if vel.x > 0.0 {
+			sprite.flip_x = true;
+		} else if vel.x < 0.0 {
+			sprite.flip_x = false;
+		}
+	}
+}
+
+pub struct GraphicsPlugin;
+impl Plugin for GraphicsPlugin {
+	fn build(&self, app: &mut App) {
+		app.add_systems(
+			Update,
+			(update_kinematic_sprite, process_kinematic_motion)
+				.run_if(run_in_game)
+				.after(PhysicsSet),
+		);
+	}
+}
diff --git a/game_core/src/entities/mod.rs b/game_core/src/entities/mod.rs
index e48802a3597b3dc6e68860ceaea9ad93df362263..cea322b5d81b9ca3962ace899db460027d5de2d2 100644
--- a/game_core/src/entities/mod.rs
+++ b/game_core/src/entities/mod.rs
@@ -1,3 +1,4 @@
+mod graphics;
 mod physics;
 mod player;
 
@@ -11,9 +12,11 @@ mod _plugin {
 			PluginGroupBuilder::start::<Self>()
 				.add(super::player::PlayerSetupPlugin)
 				.add(super::physics::PhysicsPlugin)
+				.add(super::graphics::GraphicsPlugin)
 		}
 	}
 }
 
 pub use _plugin::EntityPluginSet;
+pub use graphics::{SimpleAnimation, SimpleAnimationBundle};
 pub use physics::{Acceleration, PhysicsLimits, PhysicsSet, Velocity, GRAVITY_ACC, GRAVITY_VEC};
diff --git a/game_core/src/entities/physics.rs b/game_core/src/entities/physics.rs
index c862b46e1dca3c87e8c35d5f32bd449ad22abff1..f97b936f53d011655baf6e84ba1d9b8f1e29d3ee 100644
--- a/game_core/src/entities/physics.rs
+++ b/game_core/src/entities/physics.rs
@@ -2,7 +2,7 @@ use crate::system::run_in_game;
 use bevy::prelude::*;
 use bevy_rapier2d::prelude::KinematicCharacterController;
 
-pub const GRAVITY_ACC: f32 = -384.0;
+pub const GRAVITY_ACC: f32 = -150.0;
 pub const GRAVITY_VEC: Vec2 = Vec2::new(0.0, GRAVITY_ACC);
 
 #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, SystemSet)]
diff --git a/game_core/src/entities/player.rs b/game_core/src/entities/player.rs
index 3a32b2d6eff8bc1607b2336f0c12eafee60e6250..1dda5b356c9f7da0996c59a030808ab891ff0191 100644
--- a/game_core/src/entities/player.rs
+++ b/game_core/src/entities/player.rs
@@ -1,8 +1,11 @@
 use crate::assets::AssetHandles;
-use crate::system::AppState;
+use crate::system::{AppState, ChaseCam, PolyInputManager};
 use bevy::prelude::*;
 
-use crate::entities::{Acceleration, PhysicsLimits, Velocity, GRAVITY_ACC};
+use crate::entities::{
+	Acceleration, PhysicsLimits, PhysicsSet, SimpleAnimation, SimpleAnimationBundle, Velocity,
+	GRAVITY_ACC,
+};
 use bevy_rapier2d::prelude::*;
 
 #[derive(Component, Clone, Copy, Debug)]
@@ -12,17 +15,14 @@ pub fn spawn_player(mut commands: Commands, assets: Res<AssetHandles>) {
 	commands.spawn((
 		Player,
 		SpriteSheetBundle {
-			sprite: TextureAtlasSprite::new(3),
-			texture_atlas: assets.atlas("beige_blob"),
+			sprite: TextureAtlasSprite::new(9),
+			texture_atlas: assets.atlas("characters"),
+			transform: Transform::from_xyz(0.0, 0.0, 100.0),
 			..Default::default()
 		},
 		RigidBody::KinematicPositionBased,
+		Collider::cuboid(8.0, 12.0),
 		KinematicCharacterController {
-			custom_shape: Some((
-				Collider::capsule(Vec2::new(0.0, 8.0), Vec2::new(0.0, -48.0), 48.0),
-				Vec2::new(0.0, -32.0),
-				0.0,
-			)),
 			snap_to_ground: Some(CharacterLength::Absolute(0.5)),
 			offset: CharacterLength::Absolute(0.03),
 			..Default::default()
@@ -36,12 +36,53 @@ pub fn spawn_player(mut commands: Commands, assets: Res<AssetHandles>) {
 			),
 			acceleration: Default::default(),
 		},
+		ChaseCam,
+		SimpleAnimationBundle::new(
+			SimpleAnimation {
+				frames: vec![9],
+				frame_time: 10.0,
+			},
+			SimpleAnimation {
+				frames: vec![10, 9],
+				frame_time: 0.2,
+			},
+		),
 	));
 }
 
+pub fn handle_input(
+	input: PolyInputManager,
+	mut query: Query<(&mut Velocity, &KinematicCharacterControllerOutput), With<Player>>,
+) {
+	let mut delta_x = None;
+	let mut delta_y = None;
+
+	for (mut velocity, controller) in &mut query {
+		if controller.grounded && input.is_jump_just_pressed() {
+			delta_y = Some(40.0);
+		}
+
+		if input.is_right_pressed_or_active() {
+			delta_x = Some(200.0);
+		} else if input.is_left_pressed_or_active() {
+			delta_x = Some(-200.0);
+		} else {
+			delta_x = Some(0.0);
+		}
+
+		if let Some(dy) = delta_y {
+			velocity.y = dy;
+		}
+		if let Some(dx) = delta_x {
+			velocity.x = dx;
+		}
+	}
+}
+
 pub struct PlayerSetupPlugin;
 impl Plugin for PlayerSetupPlugin {
 	fn build(&self, app: &mut App) {
-		app.add_systems(OnEnter(AppState::InGame), spawn_player);
+		app.add_systems(OnEnter(AppState::InGame), spawn_player)
+			.add_systems(Update, handle_input.before(PhysicsSet));
 	}
 }
diff --git a/game_core/src/main.rs b/game_core/src/main.rs
index acc48ff9faf53843b0732ccb07ba957d2bd57a67..9eb1379ab78a9e95ff65244cdd6d976a26ad9dd2 100644
--- a/game_core/src/main.rs
+++ b/game_core/src/main.rs
@@ -6,8 +6,7 @@ fn main() {
 		.add_plugins(game_core::system::SystemPluginSet)
 		.add_plugins(game_core::entities::EntityPluginSet)
 		.add_plugins(game_core::debug::DebugPlugin)
-		.add_plugins(bevy_rapier2d::plugin::RapierPhysicsPlugin::<()>::pixels_per_meter(128.0))
-		.add_plugins(micro_banimate::BanimatePluginGroup)
+		.add_plugins(bevy_rapier2d::plugin::RapierPhysicsPlugin::<()>::pixels_per_meter(18.0))
 		.add_plugins(micro_musicbox::CombinedAudioPlugins::<
 			game_core::assets::AssetHandles,
 		>::new())
diff --git a/game_core/src/system/input.rs b/game_core/src/system/input.rs
new file mode 100644
index 0000000000000000000000000000000000000000..26f33b9ac14a5fb3249c06e314a05e8480132e2b
--- /dev/null
+++ b/game_core/src/system/input.rs
@@ -0,0 +1,247 @@
+use bevy::ecs::system::SystemParam;
+use bevy::input::gamepad::GamepadSettings;
+use bevy::input::{Axis, Input};
+use bevy::prelude::*;
+use num_traits::AsPrimitive;
+use paste::paste;
+use serde::{Deserialize, Serialize};
+
+#[derive(Copy, Clone, Debug, Default, PartialOrd, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum SingleAxisValue {
+	#[default]
+	Ignore,
+	Above(f32),
+	Below(f32),
+}
+impl SingleAxisValue {
+	pub fn ignore() -> Self {
+		Self::Ignore
+	}
+	pub fn above(value: impl AsPrimitive<f32>) -> Self {
+		Self::Above(value.as_())
+	}
+	pub fn below(value: impl AsPrimitive<f32>) -> Self {
+		Self::Below(value.as_())
+	}
+	pub fn above_strict(value: impl AsPrimitive<f32>) -> Self {
+		Self::Above(value.as_() - (1e-8f32))
+	}
+	pub fn below_strict(value: impl AsPrimitive<f32>) -> Self {
+		Self::Below(value.as_() - (1e-8f32))
+	}
+}
+
+impl PartialEq<f32> for SingleAxisValue {
+	fn eq(&self, other: &f32) -> bool {
+		match self {
+			Self::Ignore => true,
+			Self::Above(val) => other >= val,
+			Self::Below(val) => other <= val,
+		}
+	}
+}
+
+#[derive(Copy, Clone, PartialOrd, PartialEq, Debug, Default, Serialize, Deserialize)]
+pub struct AnalogStickMatch {
+	pub x_axis: SingleAxisValue,
+	pub y_axis: SingleAxisValue,
+}
+
+impl AnalogStickMatch {
+	pub fn new(x: SingleAxisValue, y: SingleAxisValue) -> Self {
+		Self {
+			x_axis: x,
+			y_axis: y,
+		}
+	}
+
+	#[inline]
+	pub fn ignore() -> Self {
+		Self::default()
+	}
+
+	pub fn horizontal(x: SingleAxisValue) -> Self {
+		Self {
+			x_axis: x,
+			y_axis: SingleAxisValue::Ignore,
+		}
+	}
+
+	pub fn vertical(y: SingleAxisValue) -> Self {
+		Self {
+			y_axis: y,
+			x_axis: SingleAxisValue::Ignore,
+		}
+	}
+}
+
+#[derive(Copy, Clone, PartialOrd, PartialEq, Debug, Default, Serialize, Deserialize)]
+pub struct AxisControl {
+	left: Option<AnalogStickMatch>,
+	right: Option<AnalogStickMatch>,
+}
+
+#[derive(Copy, Clone, PartialEq, Debug, Serialize, Deserialize, Deref, DerefMut)]
+pub struct GamepadInput(GamepadButtonType);
+impl From<GamepadButtonType> for GamepadInput {
+	fn from(value: GamepadButtonType) -> Self {
+		GamepadInput(value)
+	}
+}
+
+impl GamepadInput {
+	pub fn as_button(&self, gamepad: Gamepad) -> GamepadButton {
+		GamepadButton {
+			gamepad,
+			button_type: self.0,
+		}
+	}
+}
+
+#[derive(Resource, Copy, Clone)]
+pub struct KeyboardControlMap {
+	pub left: KeyCode,
+	pub right: KeyCode,
+	pub up: KeyCode,
+	pub down: KeyCode,
+	pub interact: KeyCode,
+	pub jump: KeyCode,
+	pub inventory: KeyCode,
+	pub character: KeyCode,
+	pub cancel: KeyCode,
+}
+
+#[derive(Resource, Copy, Clone)]
+pub struct GamepadControlMap {
+	pub left: GamepadInput,
+	pub right: GamepadInput,
+	pub up: GamepadInput,
+	pub down: GamepadInput,
+	pub interact: GamepadInput,
+	pub jump: GamepadInput,
+	pub inventory: GamepadInput,
+	pub character: GamepadInput,
+	pub cancel: GamepadInput,
+}
+
+impl Default for KeyboardControlMap {
+	fn default() -> Self {
+		Self {
+			left: KeyCode::A,
+			right: KeyCode::D,
+			up: KeyCode::W,
+			down: KeyCode::S,
+			interact: KeyCode::E,
+			jump: KeyCode::Space,
+
+			inventory: KeyCode::I,
+			character: KeyCode::C,
+			cancel: KeyCode::Escape,
+		}
+	}
+}
+
+impl Default for GamepadControlMap {
+	fn default() -> Self {
+		Self {
+			left: GamepadButtonType::DPadLeft.into(),
+			right: GamepadButtonType::DPadRight.into(),
+			up: GamepadButtonType::DPadUp.into(),
+			down: GamepadButtonType::DPadDown.into(),
+			interact: GamepadButtonType::South.into(),
+			jump: GamepadButtonType::North.into(),
+
+			inventory: GamepadButtonType::Start.into(),
+			character: GamepadButtonType::Select.into(),
+			cancel: GamepadButtonType::East.into(),
+		}
+	}
+}
+
+macro_rules! impl_button_check {
+	($property: ident) => {
+		paste! {
+			pub fn [<is_ $property _pressed>](&self) -> bool {
+				self.keyboard.pressed(self.kbd_controls.$property)
+					|| self.gamepads.iter().any(|gpd| {
+						self.gamepad
+							.pressed(self.gpd_controls.$property.as_button(gpd))
+					})
+			}
+			pub fn [<is_ $property _just_pressed>](&self) -> bool {
+				self.keyboard.just_pressed(self.kbd_controls.$property)
+					|| self.gamepads.iter().any(|gpd| {
+						self.gamepad
+							.just_pressed(self.gpd_controls.$property.as_button(gpd))
+					})
+			}
+			pub fn [<is_ $property _just_released>](&self) -> bool {
+				self.keyboard.just_released(self.kbd_controls.$property)
+					|| self.gamepads.iter().any(|gpd| {
+						self.gamepad
+							.just_released(self.gpd_controls.$property.as_button(gpd))
+					})
+			}
+		}
+	};
+
+	($property: ident, ($horizaxis: expr, $vertaxis: expr)) => {
+		impl_button_check!($property);
+		paste! {
+			pub fn [<is_ $property _pressed_or_active>](&self) -> bool {
+				self.[<is_ $property _pressed>]()
+					|| self.gamepads.iter().any(|gpd| {
+						self.axes.get(GamepadAxis { gamepad: gpd, axis_type: GamepadAxisType::LeftStickX }).map(|value| $horizaxis == value).unwrap_or(false)
+						&& self.axes.get(GamepadAxis { gamepad: gpd, axis_type: GamepadAxisType::LeftStickY }).map(|value| $vertaxis == value).unwrap_or(false)
+					})
+			}
+		}
+	};
+}
+
+#[derive(SystemParam)]
+pub struct PolyInputManager<'w> {
+	keyboard: Res<'w, Input<KeyCode>>,
+	gamepad: Res<'w, Input<GamepadButton>>,
+	gamepads: Res<'w, Gamepads>,
+	axes: Res<'w, Axis<GamepadAxis>>,
+	kbd_controls: Res<'w, KeyboardControlMap>,
+	gpd_controls: Res<'w, GamepadControlMap>,
+}
+
+impl<'w> PolyInputManager<'w> {
+	impl_button_check!(
+		left,
+		(SingleAxisValue::below(-0.4), SingleAxisValue::ignore())
+	);
+	impl_button_check!(
+		right,
+		(SingleAxisValue::above(0.4), SingleAxisValue::ignore())
+	);
+	impl_button_check!(up, (SingleAxisValue::ignore(), SingleAxisValue::above(0.4)));
+	impl_button_check!(
+		down,
+		(SingleAxisValue::ignore(), SingleAxisValue::Below(-0.4))
+	);
+	impl_button_check!(interact);
+	impl_button_check!(inventory);
+	impl_button_check!(character);
+	impl_button_check!(cancel);
+	impl_button_check!(jump);
+}
+
+pub fn configure_gamepads(mut settings: ResMut<GamepadSettings>) {
+	settings.default_axis_settings.set_deadzone_lowerbound(-0.2);
+	settings.default_axis_settings.set_deadzone_upperbound(0.2);
+	settings.default_axis_settings.set_threshold(0.02);
+}
+
+pub struct ControlInputPlugin;
+impl Plugin for ControlInputPlugin {
+	fn build(&self, app: &mut App) {
+		app.init_resource::<KeyboardControlMap>()
+			.init_resource::<GamepadControlMap>()
+			.add_systems(Startup, configure_gamepads);
+	}
+}
diff --git a/game_core/src/system/mod.rs b/game_core/src/system/mod.rs
index 0cf6bb6dd0f72488ba6b0fb99d5326e15e074747..b0186198a87f97a4627ce21df665121187109ff9 100644
--- a/game_core/src/system/mod.rs
+++ b/game_core/src/system/mod.rs
@@ -1,5 +1,6 @@
 mod camera;
 mod flow;
+mod input;
 mod resource_config;
 mod resources;
 mod web;
@@ -33,7 +34,8 @@ mod _plugin {
 			let plugins = InitAppPlugins
 				.build()
 				.add(super::flow::FlowPlugin)
-				.add(super::camera::CameraManagementPlugin);
+				.add(super::camera::CameraManagementPlugin)
+				.add(super::input::ControlInputPlugin);
 
 			#[cfg(feature = "phys-debug")]
 			{
@@ -52,5 +54,6 @@ mod _plugin {
 pub use _plugin::SystemPluginSet;
 pub use camera::{ChaseCam, GameCamera};
 pub use flow::{run_in_game, run_in_menu, run_in_setup, run_in_splash, AppState};
+pub use input::PolyInputManager;
 pub use resource_config::{get_asset_path_string, initial_size, virtual_size};
 pub use resources::configure_default_plugins;
diff --git a/game_core/src/system/resource_config.rs b/game_core/src/system/resource_config.rs
index d6ba2d556ba71a51b037324536cc30fac6ab4c9c..6001a2912b8545cc8389fd061fd92b1999d9ab30 100644
--- a/game_core/src/system/resource_config.rs
+++ b/game_core/src/system/resource_config.rs
@@ -1,4 +1,4 @@
-const WINDOW_SCALER: f32 = 1.0;
+const WINDOW_SCALER: f32 = 4.0;
 
 #[cfg(not(target_arch = "wasm32"))]
 mod setup {
diff --git a/game_core/src/system/resources.rs b/game_core/src/system/resources.rs
index a60ba9aec094377b1677edf1cab75e4c697dedcc..36c54824060c7d4bfa724395a7f57a008e4d13ce 100644
--- a/game_core/src/system/resources.rs
+++ b/game_core/src/system/resources.rs
@@ -33,7 +33,7 @@ pub fn configure_default_plugins() -> PluginGroupBuilder {
 			asset_folder: get_asset_path_string(),
 			watch_for_changes: ChangeWatcher::with_delay(Duration::from_secs(1)),
 		})
-		.set(ImagePlugin::default_linear())
+		.set(ImagePlugin::default_nearest())
 		.set(LogPlugin {
 			filter: String::from(
 				"info,game_core=debug,symphonia_core=warn,symphonia_format_ogg=warn,winit=warn,symphonia_bundle_mp3=warn,wgpu_core=warn,wgpu_hal=warn",