diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3c25e1e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+node_modules/
+dist/
+*.log
diff --git a/assets/asset-manifest.json b/assets/asset-manifest.json
new file mode 100644
index 0000000..e8cb0f1
--- /dev/null
+++ b/assets/asset-manifest.json
@@ -0,0 +1,266 @@
+{
+ "spritesheets": [
+ {
+ "key": "player_rw_idle",
+ "path": "assets/spritesheets/rw/player_rw_idle.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "player_rw_walk",
+ "path": "assets/spritesheets/rw/player_rw_walk.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "player_rw_run",
+ "path": "assets/spritesheets/rw/player_rw_run.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "player_rw_attack",
+ "path": "assets/spritesheets/rw/player_rw_attack.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "player_gw_idle",
+ "path": "assets/spritesheets/gw/player_gw_idle.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "player_gw_walk",
+ "path": "assets/spritesheets/gw/player_gw_walk.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "player_gw_run",
+ "path": "assets/spritesheets/gw/player_gw_run.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "player_gw_attack",
+ "path": "assets/spritesheets/gw/player_gw_attack.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "enemy_intern_idle",
+ "path": "assets/spritesheets/gw/enemy_intern_idle.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "enemy_intern_walk",
+ "path": "assets/spritesheets/gw/enemy_intern_walk.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "enemy_intern_run",
+ "path": "assets/spritesheets/gw/enemy_intern_run.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "enemy_intern_attack",
+ "path": "assets/spritesheets/gw/enemy_intern_attack.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "enemy_hr_idle",
+ "path": "assets/spritesheets/gw/enemy_hr_idle.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "enemy_hr_walk",
+ "path": "assets/spritesheets/gw/enemy_hr_walk.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "enemy_hr_run",
+ "path": "assets/spritesheets/gw/enemy_hr_run.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "enemy_hr_attack",
+ "path": "assets/spritesheets/gw/enemy_hr_attack.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "enemy_dev_idle",
+ "path": "assets/spritesheets/gw/enemy_dev_idle.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "enemy_dev_walk",
+ "path": "assets/spritesheets/gw/enemy_dev_walk.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "enemy_dev_run",
+ "path": "assets/spritesheets/gw/enemy_dev_run.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "enemy_dev_attack",
+ "path": "assets/spritesheets/gw/enemy_dev_attack.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "enemy_security_idle",
+ "path": "assets/spritesheets/gw/enemy_security_idle.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "enemy_security_walk",
+ "path": "assets/spritesheets/gw/enemy_security_walk.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "enemy_security_run",
+ "path": "assets/spritesheets/gw/enemy_security_run.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "enemy_security_attack",
+ "path": "assets/spritesheets/gw/enemy_security_attack.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "enemy_accountant_idle",
+ "path": "assets/spritesheets/gw/enemy_accountant_idle.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "enemy_accountant_walk",
+ "path": "assets/spritesheets/gw/enemy_accountant_walk.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "enemy_accountant_run",
+ "path": "assets/spritesheets/gw/enemy_accountant_run.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "enemy_accountant_attack",
+ "path": "assets/spritesheets/gw/enemy_accountant_attack.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "miniboss_vp_idle",
+ "path": "assets/spritesheets/gw/miniboss_vp_idle.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "miniboss_vp_walk",
+ "path": "assets/spritesheets/gw/miniboss_vp_walk.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "miniboss_vp_run",
+ "path": "assets/spritesheets/gw/miniboss_vp_run.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "miniboss_vp_attack",
+ "path": "assets/spritesheets/gw/miniboss_vp_attack.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "boss_ceo_idle",
+ "path": "assets/spritesheets/gw/boss_ceo_idle.png",
+ "frameWidth": 64,
+ "frameHeight": 64
+ },
+ {
+ "key": "boss_ceo_walk",
+ "path": "assets/spritesheets/gw/boss_ceo_walk.png",
+ "frameWidth": 64,
+ "frameHeight": 64
+ },
+ {
+ "key": "boss_ceo_run",
+ "path": "assets/spritesheets/gw/boss_ceo_run.png",
+ "frameWidth": 64,
+ "frameHeight": 64
+ },
+ {
+ "key": "boss_ceo_attack",
+ "path": "assets/spritesheets/gw/boss_ceo_attack.png",
+ "frameWidth": 64,
+ "frameHeight": 64
+ },
+ {
+ "key": "fx_hitspark",
+ "path": "assets/spritesheets/gw/fx_hitspark.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "fx_slasharc",
+ "path": "assets/spritesheets/gw/fx_slasharc.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "fx_projectile",
+ "path": "assets/spritesheets/gw/fx_projectile.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ },
+ {
+ "key": "fx_pickup",
+ "path": "assets/spritesheets/gw/fx_pickup.png",
+ "frameWidth": 32,
+ "frameHeight": 32
+ }
+ ],
+ "atlases": [
+ {
+ "key": "atlas_rw_world",
+ "png": "assets/atlas/rw/atlas_rw_world.png",
+ "json": "assets/atlas/rw/atlas_rw_world.json"
+ },
+ {
+ "key": "atlas_rw_ui",
+ "png": "assets/atlas/rw/atlas_rw_ui.png",
+ "json": "assets/atlas/rw/atlas_rw_ui.json"
+ },
+ {
+ "key": "atlas_gw_world",
+ "png": "assets/atlas/gw/atlas_gw_world.png",
+ "json": "assets/atlas/gw/atlas_gw_world.json"
+ },
+ {
+ "key": "atlas_gw_ui",
+ "png": "assets/atlas/gw/atlas_gw_ui.png",
+ "json": "assets/atlas/gw/atlas_gw_ui.json"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/assets/atlas/gw/atlas_gw_ui.json b/assets/atlas/gw/atlas_gw_ui.json
new file mode 100644
index 0000000..330ea3d
--- /dev/null
+++ b/assets/atlas/gw/atlas_gw_ui.json
@@ -0,0 +1,355 @@
+{
+ "frames": {
+ "gw_ui_button_normal": {
+ "frame": {
+ "x": 2,
+ "y": 2,
+ "w": 120,
+ "h": 40
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 120,
+ "h": 40
+ },
+ "sourceSize": {
+ "w": 120,
+ "h": 40
+ }
+ },
+ "gw_ui_button_hover": {
+ "frame": {
+ "x": 124,
+ "y": 2,
+ "w": 120,
+ "h": 40
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 120,
+ "h": 40
+ },
+ "sourceSize": {
+ "w": 120,
+ "h": 40
+ }
+ },
+ "gw_ui_button_pressed": {
+ "frame": {
+ "x": 246,
+ "y": 2,
+ "w": 120,
+ "h": 40
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 120,
+ "h": 40
+ },
+ "sourceSize": {
+ "w": 120,
+ "h": 40
+ }
+ },
+ "gw_ui_panel": {
+ "frame": {
+ "x": 2,
+ "y": 44,
+ "w": 200,
+ "h": 150
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 200,
+ "h": 150
+ },
+ "sourceSize": {
+ "w": 200,
+ "h": 150
+ }
+ },
+ "gw_ui_health_bar_bg": {
+ "frame": {
+ "x": 204,
+ "y": 44,
+ "w": 100,
+ "h": 16
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 100,
+ "h": 16
+ },
+ "sourceSize": {
+ "w": 100,
+ "h": 16
+ }
+ },
+ "gw_ui_health_bar_fill": {
+ "frame": {
+ "x": 306,
+ "y": 44,
+ "w": 100,
+ "h": 16
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 100,
+ "h": 16
+ },
+ "sourceSize": {
+ "w": 100,
+ "h": 16
+ }
+ },
+ "gw_ui_energy_bar_bg": {
+ "frame": {
+ "x": 408,
+ "y": 44,
+ "w": 100,
+ "h": 16
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 100,
+ "h": 16
+ },
+ "sourceSize": {
+ "w": 100,
+ "h": 16
+ }
+ },
+ "gw_ui_energy_bar_fill": {
+ "frame": {
+ "x": 2,
+ "y": 196,
+ "w": 100,
+ "h": 16
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 100,
+ "h": 16
+ },
+ "sourceSize": {
+ "w": 100,
+ "h": 16
+ }
+ },
+ "gw_ui_icon_hp": {
+ "frame": {
+ "x": 104,
+ "y": 196,
+ "w": 24,
+ "h": 24
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 24,
+ "h": 24
+ },
+ "sourceSize": {
+ "w": 24,
+ "h": 24
+ }
+ },
+ "gw_ui_icon_mp": {
+ "frame": {
+ "x": 130,
+ "y": 196,
+ "w": 24,
+ "h": 24
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 24,
+ "h": 24
+ },
+ "sourceSize": {
+ "w": 24,
+ "h": 24
+ }
+ },
+ "gw_ui_icon_exp": {
+ "frame": {
+ "x": 156,
+ "y": 196,
+ "w": 24,
+ "h": 24
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 24,
+ "h": 24
+ },
+ "sourceSize": {
+ "w": 24,
+ "h": 24
+ }
+ },
+ "gw_ui_icon_gold": {
+ "frame": {
+ "x": 182,
+ "y": 196,
+ "w": 24,
+ "h": 24
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 24,
+ "h": 24
+ },
+ "sourceSize": {
+ "w": 24,
+ "h": 24
+ }
+ },
+ "gw_ui_dialog_box": {
+ "frame": {
+ "x": 208,
+ "y": 196,
+ "w": 300,
+ "h": 80
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 300,
+ "h": 80
+ },
+ "sourceSize": {
+ "w": 300,
+ "h": 80
+ }
+ },
+ "gw_ui_cursor": {
+ "frame": {
+ "x": 2,
+ "y": 278,
+ "w": 32,
+ "h": 32
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 32,
+ "h": 32
+ },
+ "sourceSize": {
+ "w": 32,
+ "h": 32
+ }
+ },
+ "gw_ui_minimap_bg": {
+ "frame": {
+ "x": 36,
+ "y": 278,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "gw_ui_hud_frame": {
+ "frame": {
+ "x": 166,
+ "y": 278,
+ "w": 256,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 256,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 256,
+ "h": 64
+ }
+ },
+ "gw_ui_level_up_banner": {
+ "frame": {
+ "x": 2,
+ "y": 408,
+ "w": 200,
+ "h": 48
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 200,
+ "h": 48
+ },
+ "sourceSize": {
+ "w": 200,
+ "h": 48
+ }
+ }
+ },
+ "meta": {
+ "app": "asset-generator",
+ "version": "1.0",
+ "image": "atlas_gw_ui.png",
+ "format": "RGBA8888",
+ "size": {
+ "w": 512,
+ "h": 512
+ },
+ "scale": "1"
+ }
+}
\ No newline at end of file
diff --git a/assets/atlas/gw/atlas_gw_ui.png b/assets/atlas/gw/atlas_gw_ui.png
new file mode 100644
index 0000000..7ef446d
Binary files /dev/null and b/assets/atlas/gw/atlas_gw_ui.png differ
diff --git a/assets/atlas/gw/atlas_gw_world.json b/assets/atlas/gw/atlas_gw_world.json
new file mode 100644
index 0000000..652ce8d
--- /dev/null
+++ b/assets/atlas/gw/atlas_gw_world.json
@@ -0,0 +1,515 @@
+{
+ "frames": {
+ "gw_building_server": {
+ "frame": {
+ "x": 2,
+ "y": 2,
+ "w": 64,
+ "h": 96
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 96
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 96
+ }
+ },
+ "gw_building_arena": {
+ "frame": {
+ "x": 68,
+ "y": 2,
+ "w": 96,
+ "h": 96
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 96,
+ "h": 96
+ },
+ "sourceSize": {
+ "w": 96,
+ "h": 96
+ }
+ },
+ "gw_building_portal": {
+ "frame": {
+ "x": 166,
+ "y": 2,
+ "w": 48,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 48,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 48,
+ "h": 64
+ }
+ },
+ "gw_prop_terminal": {
+ "frame": {
+ "x": 216,
+ "y": 2,
+ "w": 32,
+ "h": 48
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 32,
+ "h": 48
+ },
+ "sourceSize": {
+ "w": 32,
+ "h": 48
+ }
+ },
+ "gw_prop_chest": {
+ "frame": {
+ "x": 250,
+ "y": 2,
+ "w": 32,
+ "h": 32
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 32,
+ "h": 32
+ },
+ "sourceSize": {
+ "w": 32,
+ "h": 32
+ }
+ },
+ "gw_prop_barrel": {
+ "frame": {
+ "x": 284,
+ "y": 2,
+ "w": 24,
+ "h": 32
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 24,
+ "h": 32
+ },
+ "sourceSize": {
+ "w": 24,
+ "h": 32
+ }
+ },
+ "gw_prop_crate": {
+ "frame": {
+ "x": 310,
+ "y": 2,
+ "w": 32,
+ "h": 32
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 32,
+ "h": 32
+ },
+ "sourceSize": {
+ "w": 32,
+ "h": 32
+ }
+ },
+ "gw_prop_crystal": {
+ "frame": {
+ "x": 344,
+ "y": 2,
+ "w": 24,
+ "h": 32
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 24,
+ "h": 32
+ },
+ "sourceSize": {
+ "w": 24,
+ "h": 32
+ }
+ },
+ "item_sword": {
+ "frame": {
+ "x": 370,
+ "y": 2,
+ "w": 16,
+ "h": 32
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 16,
+ "h": 32
+ },
+ "sourceSize": {
+ "w": 16,
+ "h": 32
+ }
+ },
+ "item_shield": {
+ "frame": {
+ "x": 388,
+ "y": 2,
+ "w": 24,
+ "h": 24
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 24,
+ "h": 24
+ },
+ "sourceSize": {
+ "w": 24,
+ "h": 24
+ }
+ },
+ "item_potion_red": {
+ "frame": {
+ "x": 414,
+ "y": 2,
+ "w": 16,
+ "h": 24
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 16,
+ "h": 24
+ },
+ "sourceSize": {
+ "w": 16,
+ "h": 24
+ }
+ },
+ "item_potion_blue": {
+ "frame": {
+ "x": 432,
+ "y": 2,
+ "w": 16,
+ "h": 24
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 16,
+ "h": 24
+ },
+ "sourceSize": {
+ "w": 16,
+ "h": 24
+ }
+ },
+ "item_coin": {
+ "frame": {
+ "x": 450,
+ "y": 2,
+ "w": 16,
+ "h": 16
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 16,
+ "h": 16
+ },
+ "sourceSize": {
+ "w": 16,
+ "h": 16
+ }
+ },
+ "item_key": {
+ "frame": {
+ "x": 468,
+ "y": 2,
+ "w": 16,
+ "h": 24
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 16,
+ "h": 24
+ },
+ "sourceSize": {
+ "w": 16,
+ "h": 24
+ }
+ },
+ "item_badge": {
+ "frame": {
+ "x": 486,
+ "y": 2,
+ "w": 24,
+ "h": 24
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 24,
+ "h": 24
+ },
+ "sourceSize": {
+ "w": 24,
+ "h": 24
+ }
+ },
+ "item_power_chip": {
+ "frame": {
+ "x": 2,
+ "y": 100,
+ "w": 16,
+ "h": 16
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 16,
+ "h": 16
+ },
+ "sourceSize": {
+ "w": 16,
+ "h": 16
+ }
+ },
+ "gw_tile_floor_dungeon": {
+ "frame": {
+ "x": 20,
+ "y": 100,
+ "w": 32,
+ "h": 32
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 32,
+ "h": 32
+ },
+ "sourceSize": {
+ "w": 32,
+ "h": 32
+ }
+ },
+ "gw_tile_floor_circuit": {
+ "frame": {
+ "x": 54,
+ "y": 100,
+ "w": 32,
+ "h": 32
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 32,
+ "h": 32
+ },
+ "sourceSize": {
+ "w": 32,
+ "h": 32
+ }
+ },
+ "gw_tile_wall_dungeon": {
+ "frame": {
+ "x": 88,
+ "y": 100,
+ "w": 32,
+ "h": 32
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 32,
+ "h": 32
+ },
+ "sourceSize": {
+ "w": 32,
+ "h": 32
+ }
+ },
+ "gw_tile_wall_circuit": {
+ "frame": {
+ "x": 122,
+ "y": 100,
+ "w": 32,
+ "h": 32
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 32,
+ "h": 32
+ },
+ "sourceSize": {
+ "w": 32,
+ "h": 32
+ }
+ },
+ "gw_tile_floor_boss": {
+ "frame": {
+ "x": 156,
+ "y": 100,
+ "w": 32,
+ "h": 32
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 32,
+ "h": 32
+ },
+ "sourceSize": {
+ "w": 32,
+ "h": 32
+ }
+ },
+ "gw_env_pillar": {
+ "frame": {
+ "x": 190,
+ "y": 100,
+ "w": 32,
+ "h": 48
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 32,
+ "h": 48
+ },
+ "sourceSize": {
+ "w": 32,
+ "h": 48
+ }
+ },
+ "gw_env_spike": {
+ "frame": {
+ "x": 224,
+ "y": 100,
+ "w": 32,
+ "h": 32
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 32,
+ "h": 32
+ },
+ "sourceSize": {
+ "w": 32,
+ "h": 32
+ }
+ },
+ "gw_env_portal_ring": {
+ "frame": {
+ "x": 258,
+ "y": 100,
+ "w": 48,
+ "h": 48
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 48,
+ "h": 48
+ },
+ "sourceSize": {
+ "w": 48,
+ "h": 48
+ }
+ },
+ "gw_env_data_stream": {
+ "frame": {
+ "x": 308,
+ "y": 100,
+ "w": 16,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 16,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 16,
+ "h": 64
+ }
+ }
+ },
+ "meta": {
+ "app": "asset-generator",
+ "version": "1.0",
+ "image": "atlas_gw_world.png",
+ "format": "RGBA8888",
+ "size": {
+ "w": 512,
+ "h": 256
+ },
+ "scale": "1"
+ }
+}
\ No newline at end of file
diff --git a/assets/atlas/gw/atlas_gw_world.png b/assets/atlas/gw/atlas_gw_world.png
new file mode 100644
index 0000000..8bd54fd
Binary files /dev/null and b/assets/atlas/gw/atlas_gw_world.png differ
diff --git a/assets/atlas/rw/atlas_rw_ui.json b/assets/atlas/rw/atlas_rw_ui.json
new file mode 100644
index 0000000..d8c34d2
--- /dev/null
+++ b/assets/atlas/rw/atlas_rw_ui.json
@@ -0,0 +1,315 @@
+{
+ "frames": {
+ "ui_button_normal": {
+ "frame": {
+ "x": 2,
+ "y": 2,
+ "w": 120,
+ "h": 40
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 120,
+ "h": 40
+ },
+ "sourceSize": {
+ "w": 120,
+ "h": 40
+ }
+ },
+ "ui_button_hover": {
+ "frame": {
+ "x": 124,
+ "y": 2,
+ "w": 120,
+ "h": 40
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 120,
+ "h": 40
+ },
+ "sourceSize": {
+ "w": 120,
+ "h": 40
+ }
+ },
+ "ui_button_pressed": {
+ "frame": {
+ "x": 246,
+ "y": 2,
+ "w": 120,
+ "h": 40
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 120,
+ "h": 40
+ },
+ "sourceSize": {
+ "w": 120,
+ "h": 40
+ }
+ },
+ "ui_panel": {
+ "frame": {
+ "x": 2,
+ "y": 44,
+ "w": 200,
+ "h": 150
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 200,
+ "h": 150
+ },
+ "sourceSize": {
+ "w": 200,
+ "h": 150
+ }
+ },
+ "ui_panel_header": {
+ "frame": {
+ "x": 204,
+ "y": 44,
+ "w": 200,
+ "h": 40
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 200,
+ "h": 40
+ },
+ "sourceSize": {
+ "w": 200,
+ "h": 40
+ }
+ },
+ "ui_health_bar_bg": {
+ "frame": {
+ "x": 406,
+ "y": 44,
+ "w": 100,
+ "h": 16
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 100,
+ "h": 16
+ },
+ "sourceSize": {
+ "w": 100,
+ "h": 16
+ }
+ },
+ "ui_health_bar_fill": {
+ "frame": {
+ "x": 2,
+ "y": 196,
+ "w": 100,
+ "h": 16
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 100,
+ "h": 16
+ },
+ "sourceSize": {
+ "w": 100,
+ "h": 16
+ }
+ },
+ "ui_icon_health": {
+ "frame": {
+ "x": 104,
+ "y": 196,
+ "w": 24,
+ "h": 24
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 24,
+ "h": 24
+ },
+ "sourceSize": {
+ "w": 24,
+ "h": 24
+ }
+ },
+ "ui_icon_stamina": {
+ "frame": {
+ "x": 130,
+ "y": 196,
+ "w": 24,
+ "h": 24
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 24,
+ "h": 24
+ },
+ "sourceSize": {
+ "w": 24,
+ "h": 24
+ }
+ },
+ "ui_icon_coin": {
+ "frame": {
+ "x": 156,
+ "y": 196,
+ "w": 24,
+ "h": 24
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 24,
+ "h": 24
+ },
+ "sourceSize": {
+ "w": 24,
+ "h": 24
+ }
+ },
+ "ui_icon_key": {
+ "frame": {
+ "x": 182,
+ "y": 196,
+ "w": 24,
+ "h": 24
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 24,
+ "h": 24
+ },
+ "sourceSize": {
+ "w": 24,
+ "h": 24
+ }
+ },
+ "ui_dialog_box": {
+ "frame": {
+ "x": 208,
+ "y": 196,
+ "w": 300,
+ "h": 80
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 300,
+ "h": 80
+ },
+ "sourceSize": {
+ "w": 300,
+ "h": 80
+ }
+ },
+ "ui_cursor": {
+ "frame": {
+ "x": 2,
+ "y": 278,
+ "w": 32,
+ "h": 32
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 32,
+ "h": 32
+ },
+ "sourceSize": {
+ "w": 32,
+ "h": 32
+ }
+ },
+ "ui_minimap_bg": {
+ "frame": {
+ "x": 36,
+ "y": 278,
+ "w": 128,
+ "h": 128
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 128,
+ "h": 128
+ },
+ "sourceSize": {
+ "w": 128,
+ "h": 128
+ }
+ },
+ "ui_minimap_player": {
+ "frame": {
+ "x": 166,
+ "y": 278,
+ "w": 8,
+ "h": 8
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 8,
+ "h": 8
+ },
+ "sourceSize": {
+ "w": 8,
+ "h": 8
+ }
+ }
+ },
+ "meta": {
+ "app": "asset-generator",
+ "version": "1.0",
+ "image": "atlas_rw_ui.png",
+ "format": "RGBA8888",
+ "size": {
+ "w": 512,
+ "h": 512
+ },
+ "scale": "1"
+ }
+}
\ No newline at end of file
diff --git a/assets/atlas/rw/atlas_rw_ui.png b/assets/atlas/rw/atlas_rw_ui.png
new file mode 100644
index 0000000..63ddf38
Binary files /dev/null and b/assets/atlas/rw/atlas_rw_ui.png differ
diff --git a/assets/atlas/rw/atlas_rw_world.json b/assets/atlas/rw/atlas_rw_world.json
new file mode 100644
index 0000000..10ffb59
--- /dev/null
+++ b/assets/atlas/rw/atlas_rw_world.json
@@ -0,0 +1,475 @@
+{
+ "frames": {
+ "building_office": {
+ "frame": {
+ "x": 2,
+ "y": 2,
+ "w": 64,
+ "h": 96
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 96
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 96
+ }
+ },
+ "building_shop": {
+ "frame": {
+ "x": 68,
+ "y": 2,
+ "w": 64,
+ "h": 80
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 80
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 80
+ }
+ },
+ "building_house": {
+ "frame": {
+ "x": 134,
+ "y": 2,
+ "w": 64,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 64,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 64,
+ "h": 64
+ }
+ },
+ "building_apartment": {
+ "frame": {
+ "x": 200,
+ "y": 2,
+ "w": 48,
+ "h": 96
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 48,
+ "h": 96
+ },
+ "sourceSize": {
+ "w": 48,
+ "h": 96
+ }
+ },
+ "prop_desk": {
+ "frame": {
+ "x": 250,
+ "y": 2,
+ "w": 48,
+ "h": 32
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 48,
+ "h": 32
+ },
+ "sourceSize": {
+ "w": 48,
+ "h": 32
+ }
+ },
+ "prop_chair": {
+ "frame": {
+ "x": 300,
+ "y": 2,
+ "w": 32,
+ "h": 32
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 32,
+ "h": 32
+ },
+ "sourceSize": {
+ "w": 32,
+ "h": 32
+ }
+ },
+ "prop_computer": {
+ "frame": {
+ "x": 334,
+ "y": 2,
+ "w": 32,
+ "h": 32
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 32,
+ "h": 32
+ },
+ "sourceSize": {
+ "w": 32,
+ "h": 32
+ }
+ },
+ "prop_plant": {
+ "frame": {
+ "x": 368,
+ "y": 2,
+ "w": 32,
+ "h": 48
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 32,
+ "h": 48
+ },
+ "sourceSize": {
+ "w": 32,
+ "h": 48
+ }
+ },
+ "prop_filing_cabinet": {
+ "frame": {
+ "x": 402,
+ "y": 2,
+ "w": 32,
+ "h": 48
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 32,
+ "h": 48
+ },
+ "sourceSize": {
+ "w": 32,
+ "h": 48
+ }
+ },
+ "prop_coffee_machine": {
+ "frame": {
+ "x": 436,
+ "y": 2,
+ "w": 32,
+ "h": 48
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 32,
+ "h": 48
+ },
+ "sourceSize": {
+ "w": 32,
+ "h": 48
+ }
+ },
+ "prop_water_cooler": {
+ "frame": {
+ "x": 470,
+ "y": 2,
+ "w": 24,
+ "h": 48
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 24,
+ "h": 48
+ },
+ "sourceSize": {
+ "w": 24,
+ "h": 48
+ }
+ },
+ "prop_printer": {
+ "frame": {
+ "x": 2,
+ "y": 100,
+ "w": 48,
+ "h": 32
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 48,
+ "h": 32
+ },
+ "sourceSize": {
+ "w": 48,
+ "h": 32
+ }
+ },
+ "tile_floor_office": {
+ "frame": {
+ "x": 52,
+ "y": 100,
+ "w": 32,
+ "h": 32
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 32,
+ "h": 32
+ },
+ "sourceSize": {
+ "w": 32,
+ "h": 32
+ }
+ },
+ "tile_floor_carpet": {
+ "frame": {
+ "x": 86,
+ "y": 100,
+ "w": 32,
+ "h": 32
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 32,
+ "h": 32
+ },
+ "sourceSize": {
+ "w": 32,
+ "h": 32
+ }
+ },
+ "tile_wall_office": {
+ "frame": {
+ "x": 120,
+ "y": 100,
+ "w": 32,
+ "h": 32
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 32,
+ "h": 32
+ },
+ "sourceSize": {
+ "w": 32,
+ "h": 32
+ }
+ },
+ "tile_floor_sidewalk": {
+ "frame": {
+ "x": 154,
+ "y": 100,
+ "w": 32,
+ "h": 32
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 32,
+ "h": 32
+ },
+ "sourceSize": {
+ "w": 32,
+ "h": 32
+ }
+ },
+ "tile_floor_road": {
+ "frame": {
+ "x": 188,
+ "y": 100,
+ "w": 32,
+ "h": 32
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 32,
+ "h": 32
+ },
+ "sourceSize": {
+ "w": 32,
+ "h": 32
+ }
+ },
+ "tile_grass": {
+ "frame": {
+ "x": 222,
+ "y": 100,
+ "w": 32,
+ "h": 32
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 32,
+ "h": 32
+ },
+ "sourceSize": {
+ "w": 32,
+ "h": 32
+ }
+ },
+ "env_tree": {
+ "frame": {
+ "x": 256,
+ "y": 100,
+ "w": 48,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 48,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 48,
+ "h": 64
+ }
+ },
+ "env_streetlight": {
+ "frame": {
+ "x": 306,
+ "y": 100,
+ "w": 16,
+ "h": 64
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 16,
+ "h": 64
+ },
+ "sourceSize": {
+ "w": 16,
+ "h": 64
+ }
+ },
+ "env_bench": {
+ "frame": {
+ "x": 324,
+ "y": 100,
+ "w": 48,
+ "h": 32
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 48,
+ "h": 32
+ },
+ "sourceSize": {
+ "w": 48,
+ "h": 32
+ }
+ },
+ "env_trash_can": {
+ "frame": {
+ "x": 374,
+ "y": 100,
+ "w": 24,
+ "h": 32
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 24,
+ "h": 32
+ },
+ "sourceSize": {
+ "w": 24,
+ "h": 32
+ }
+ },
+ "env_mailbox": {
+ "frame": {
+ "x": 400,
+ "y": 100,
+ "w": 24,
+ "h": 32
+ },
+ "rotated": false,
+ "trimmed": false,
+ "spriteSourceSize": {
+ "x": 0,
+ "y": 0,
+ "w": 24,
+ "h": 32
+ },
+ "sourceSize": {
+ "w": 24,
+ "h": 32
+ }
+ }
+ },
+ "meta": {
+ "app": "asset-generator",
+ "version": "1.0",
+ "image": "atlas_rw_world.png",
+ "format": "RGBA8888",
+ "size": {
+ "w": 512,
+ "h": 256
+ },
+ "scale": "1"
+ }
+}
\ No newline at end of file
diff --git a/assets/atlas/rw/atlas_rw_world.png b/assets/atlas/rw/atlas_rw_world.png
new file mode 100644
index 0000000..716f170
Binary files /dev/null and b/assets/atlas/rw/atlas_rw_world.png differ
diff --git a/assets/spritesheets/gw/boss_ceo_attack.png b/assets/spritesheets/gw/boss_ceo_attack.png
new file mode 100644
index 0000000..e0e9acc
Binary files /dev/null and b/assets/spritesheets/gw/boss_ceo_attack.png differ
diff --git a/assets/spritesheets/gw/boss_ceo_idle.png b/assets/spritesheets/gw/boss_ceo_idle.png
new file mode 100644
index 0000000..848bf85
Binary files /dev/null and b/assets/spritesheets/gw/boss_ceo_idle.png differ
diff --git a/assets/spritesheets/gw/boss_ceo_run.png b/assets/spritesheets/gw/boss_ceo_run.png
new file mode 100644
index 0000000..dcbe679
Binary files /dev/null and b/assets/spritesheets/gw/boss_ceo_run.png differ
diff --git a/assets/spritesheets/gw/boss_ceo_walk.png b/assets/spritesheets/gw/boss_ceo_walk.png
new file mode 100644
index 0000000..792a6a7
Binary files /dev/null and b/assets/spritesheets/gw/boss_ceo_walk.png differ
diff --git a/assets/spritesheets/gw/enemy_accountant_attack.png b/assets/spritesheets/gw/enemy_accountant_attack.png
new file mode 100644
index 0000000..39ca270
Binary files /dev/null and b/assets/spritesheets/gw/enemy_accountant_attack.png differ
diff --git a/assets/spritesheets/gw/enemy_accountant_idle.png b/assets/spritesheets/gw/enemy_accountant_idle.png
new file mode 100644
index 0000000..f2e401e
Binary files /dev/null and b/assets/spritesheets/gw/enemy_accountant_idle.png differ
diff --git a/assets/spritesheets/gw/enemy_accountant_run.png b/assets/spritesheets/gw/enemy_accountant_run.png
new file mode 100644
index 0000000..06be109
Binary files /dev/null and b/assets/spritesheets/gw/enemy_accountant_run.png differ
diff --git a/assets/spritesheets/gw/enemy_accountant_walk.png b/assets/spritesheets/gw/enemy_accountant_walk.png
new file mode 100644
index 0000000..fe1ce73
Binary files /dev/null and b/assets/spritesheets/gw/enemy_accountant_walk.png differ
diff --git a/assets/spritesheets/gw/enemy_dev_attack.png b/assets/spritesheets/gw/enemy_dev_attack.png
new file mode 100644
index 0000000..fc91e4d
Binary files /dev/null and b/assets/spritesheets/gw/enemy_dev_attack.png differ
diff --git a/assets/spritesheets/gw/enemy_dev_idle.png b/assets/spritesheets/gw/enemy_dev_idle.png
new file mode 100644
index 0000000..b41cdf5
Binary files /dev/null and b/assets/spritesheets/gw/enemy_dev_idle.png differ
diff --git a/assets/spritesheets/gw/enemy_dev_run.png b/assets/spritesheets/gw/enemy_dev_run.png
new file mode 100644
index 0000000..a5d1f7b
Binary files /dev/null and b/assets/spritesheets/gw/enemy_dev_run.png differ
diff --git a/assets/spritesheets/gw/enemy_dev_walk.png b/assets/spritesheets/gw/enemy_dev_walk.png
new file mode 100644
index 0000000..e5c5881
Binary files /dev/null and b/assets/spritesheets/gw/enemy_dev_walk.png differ
diff --git a/assets/spritesheets/gw/enemy_hr_attack.png b/assets/spritesheets/gw/enemy_hr_attack.png
new file mode 100644
index 0000000..6b07140
Binary files /dev/null and b/assets/spritesheets/gw/enemy_hr_attack.png differ
diff --git a/assets/spritesheets/gw/enemy_hr_idle.png b/assets/spritesheets/gw/enemy_hr_idle.png
new file mode 100644
index 0000000..5b0d664
Binary files /dev/null and b/assets/spritesheets/gw/enemy_hr_idle.png differ
diff --git a/assets/spritesheets/gw/enemy_hr_run.png b/assets/spritesheets/gw/enemy_hr_run.png
new file mode 100644
index 0000000..c127497
Binary files /dev/null and b/assets/spritesheets/gw/enemy_hr_run.png differ
diff --git a/assets/spritesheets/gw/enemy_hr_walk.png b/assets/spritesheets/gw/enemy_hr_walk.png
new file mode 100644
index 0000000..0b86eec
Binary files /dev/null and b/assets/spritesheets/gw/enemy_hr_walk.png differ
diff --git a/assets/spritesheets/gw/enemy_intern_attack.png b/assets/spritesheets/gw/enemy_intern_attack.png
new file mode 100644
index 0000000..170a569
Binary files /dev/null and b/assets/spritesheets/gw/enemy_intern_attack.png differ
diff --git a/assets/spritesheets/gw/enemy_intern_idle.png b/assets/spritesheets/gw/enemy_intern_idle.png
new file mode 100644
index 0000000..ed1e911
Binary files /dev/null and b/assets/spritesheets/gw/enemy_intern_idle.png differ
diff --git a/assets/spritesheets/gw/enemy_intern_run.png b/assets/spritesheets/gw/enemy_intern_run.png
new file mode 100644
index 0000000..f6b9c0c
Binary files /dev/null and b/assets/spritesheets/gw/enemy_intern_run.png differ
diff --git a/assets/spritesheets/gw/enemy_intern_walk.png b/assets/spritesheets/gw/enemy_intern_walk.png
new file mode 100644
index 0000000..a007e3a
Binary files /dev/null and b/assets/spritesheets/gw/enemy_intern_walk.png differ
diff --git a/assets/spritesheets/gw/enemy_security_attack.png b/assets/spritesheets/gw/enemy_security_attack.png
new file mode 100644
index 0000000..59441c3
Binary files /dev/null and b/assets/spritesheets/gw/enemy_security_attack.png differ
diff --git a/assets/spritesheets/gw/enemy_security_idle.png b/assets/spritesheets/gw/enemy_security_idle.png
new file mode 100644
index 0000000..cb985e2
Binary files /dev/null and b/assets/spritesheets/gw/enemy_security_idle.png differ
diff --git a/assets/spritesheets/gw/enemy_security_run.png b/assets/spritesheets/gw/enemy_security_run.png
new file mode 100644
index 0000000..9538fc8
Binary files /dev/null and b/assets/spritesheets/gw/enemy_security_run.png differ
diff --git a/assets/spritesheets/gw/enemy_security_walk.png b/assets/spritesheets/gw/enemy_security_walk.png
new file mode 100644
index 0000000..27509a3
Binary files /dev/null and b/assets/spritesheets/gw/enemy_security_walk.png differ
diff --git a/assets/spritesheets/gw/fx_hitspark.png b/assets/spritesheets/gw/fx_hitspark.png
new file mode 100644
index 0000000..fbe2f4f
Binary files /dev/null and b/assets/spritesheets/gw/fx_hitspark.png differ
diff --git a/assets/spritesheets/gw/fx_pickup.png b/assets/spritesheets/gw/fx_pickup.png
new file mode 100644
index 0000000..e5aed81
Binary files /dev/null and b/assets/spritesheets/gw/fx_pickup.png differ
diff --git a/assets/spritesheets/gw/fx_projectile.png b/assets/spritesheets/gw/fx_projectile.png
new file mode 100644
index 0000000..260fb35
Binary files /dev/null and b/assets/spritesheets/gw/fx_projectile.png differ
diff --git a/assets/spritesheets/gw/fx_slasharc.png b/assets/spritesheets/gw/fx_slasharc.png
new file mode 100644
index 0000000..92cc719
Binary files /dev/null and b/assets/spritesheets/gw/fx_slasharc.png differ
diff --git a/assets/spritesheets/gw/miniboss_vp_attack.png b/assets/spritesheets/gw/miniboss_vp_attack.png
new file mode 100644
index 0000000..34aaf14
Binary files /dev/null and b/assets/spritesheets/gw/miniboss_vp_attack.png differ
diff --git a/assets/spritesheets/gw/miniboss_vp_idle.png b/assets/spritesheets/gw/miniboss_vp_idle.png
new file mode 100644
index 0000000..e9bcfb9
Binary files /dev/null and b/assets/spritesheets/gw/miniboss_vp_idle.png differ
diff --git a/assets/spritesheets/gw/miniboss_vp_run.png b/assets/spritesheets/gw/miniboss_vp_run.png
new file mode 100644
index 0000000..21c53dc
Binary files /dev/null and b/assets/spritesheets/gw/miniboss_vp_run.png differ
diff --git a/assets/spritesheets/gw/miniboss_vp_walk.png b/assets/spritesheets/gw/miniboss_vp_walk.png
new file mode 100644
index 0000000..cd7d64e
Binary files /dev/null and b/assets/spritesheets/gw/miniboss_vp_walk.png differ
diff --git a/assets/spritesheets/gw/player_gw_attack.png b/assets/spritesheets/gw/player_gw_attack.png
new file mode 100644
index 0000000..cf5a1d5
Binary files /dev/null and b/assets/spritesheets/gw/player_gw_attack.png differ
diff --git a/assets/spritesheets/gw/player_gw_idle.png b/assets/spritesheets/gw/player_gw_idle.png
new file mode 100644
index 0000000..3e42c88
Binary files /dev/null and b/assets/spritesheets/gw/player_gw_idle.png differ
diff --git a/assets/spritesheets/gw/player_gw_run.png b/assets/spritesheets/gw/player_gw_run.png
new file mode 100644
index 0000000..27aa3b0
Binary files /dev/null and b/assets/spritesheets/gw/player_gw_run.png differ
diff --git a/assets/spritesheets/gw/player_gw_walk.png b/assets/spritesheets/gw/player_gw_walk.png
new file mode 100644
index 0000000..5487772
Binary files /dev/null and b/assets/spritesheets/gw/player_gw_walk.png differ
diff --git a/assets/spritesheets/rw/player_rw_attack.png b/assets/spritesheets/rw/player_rw_attack.png
new file mode 100644
index 0000000..6ef2aa8
Binary files /dev/null and b/assets/spritesheets/rw/player_rw_attack.png differ
diff --git a/assets/spritesheets/rw/player_rw_idle.png b/assets/spritesheets/rw/player_rw_idle.png
new file mode 100644
index 0000000..8ddd665
Binary files /dev/null and b/assets/spritesheets/rw/player_rw_idle.png differ
diff --git a/assets/spritesheets/rw/player_rw_run.png b/assets/spritesheets/rw/player_rw_run.png
new file mode 100644
index 0000000..ad5966e
Binary files /dev/null and b/assets/spritesheets/rw/player_rw_run.png differ
diff --git a/assets/spritesheets/rw/player_rw_walk.png b/assets/spritesheets/rw/player_rw_walk.png
new file mode 100644
index 0000000..5b70be3
Binary files /dev/null and b/assets/spritesheets/rw/player_rw_walk.png differ
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..a91f0b7
--- /dev/null
+++ b/index.html
@@ -0,0 +1,63 @@
+
+
+
+
+
+ That One Game
+
+
+
+ That One Game
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..5ba1b43
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,458 @@
+{
+ "name": "that-one-game",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "that-one-game",
+ "version": "1.0.0",
+ "dependencies": {
+ "canvas": "^3.2.3"
+ }
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/canvas": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.3.tgz",
+ "integrity": "sha512-PzE5nJZPz72YUAfo8oTp0u3fqqY7IzlTubneAihqDYAUcBk7ryeCmBbdJBEdaH0bptSOe2VT2Zwcb3UaFyaSWw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "node-addon-api": "^7.0.0",
+ "prebuild-install": "^7.1.3"
+ },
+ "engines": {
+ "node": "^18.12.0 || >= 20.9.0"
+ }
+ },
+ "node_modules/chownr": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+ "license": "ISC"
+ },
+ "node_modules/decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+ "license": "MIT",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/expand-template": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
+ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
+ "license": "(MIT OR WTFPL)",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/fs-constants": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+ "license": "MIT"
+ },
+ "node_modules/github-from-package": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
+ "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
+ "license": "MIT"
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "license": "ISC"
+ },
+ "node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/mkdirp-classic": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
+ "license": "MIT"
+ },
+ "node_modules/napi-build-utils": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
+ "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
+ "license": "MIT"
+ },
+ "node_modules/node-abi": {
+ "version": "3.89.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz",
+ "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==",
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/node-addon-api": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
+ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
+ "license": "MIT"
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/prebuild-install": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
+ "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
+ "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
+ "license": "MIT",
+ "dependencies": {
+ "detect-libc": "^2.0.0",
+ "expand-template": "^2.0.3",
+ "github-from-package": "0.0.0",
+ "minimist": "^1.2.3",
+ "mkdirp-classic": "^0.5.3",
+ "napi-build-utils": "^2.0.0",
+ "node-abi": "^3.3.0",
+ "pump": "^3.0.0",
+ "rc": "^1.2.7",
+ "simple-get": "^4.0.0",
+ "tar-fs": "^2.0.0",
+ "tunnel-agent": "^0.6.0"
+ },
+ "bin": {
+ "prebuild-install": "bin.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/pump": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
+ "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
+ "dependencies": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "bin": {
+ "rc": "cli.js"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/simple-concat": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
+ "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/simple-get": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
+ "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "decompress-response": "^6.0.0",
+ "once": "^1.3.1",
+ "simple-concat": "^1.0.0"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tar-fs": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
+ "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "chownr": "^1.1.1",
+ "mkdirp-classic": "^0.5.2",
+ "pump": "^3.0.0",
+ "tar-stream": "^2.1.4"
+ }
+ },
+ "node_modules/tar-stream": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "bl": "^4.0.3",
+ "end-of-stream": "^1.4.1",
+ "fs-constants": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tunnel-agent": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+ "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..8b1e5b8
--- /dev/null
+++ b/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "that-one-game",
+ "version": "1.0.0",
+ "description": "That One Game – Phaser 3 placeholder asset pack generator and loader helpers",
+ "scripts": {
+ "generate": "node tools/generate-assets.js"
+ },
+ "dependencies": {
+ "canvas": "^3.2.3"
+ }
+}
diff --git a/src/AnimationHelper.js b/src/AnimationHelper.js
new file mode 100644
index 0000000..1cb1678
--- /dev/null
+++ b/src/AnimationHelper.js
@@ -0,0 +1,197 @@
+/**
+ * AnimationHelper.js
+ *
+ * Phaser 3 helper – creates all character and FX animations based on the
+ * sprite-sheet layout defined in the asset spec.
+ *
+ * Sprite-sheet layout (all characters):
+ * Row 0 = down | Row 1 = left | Row 2 = right | Row 3 = up
+ * Columns = animation frames (4 for idle, 8 for walk/run/attack)
+ *
+ * Phaser frame indices are left-to-right, top-to-bottom, so:
+ * idle (4 cols): down 0-3, left 4-7, right 8-11, up 12-15
+ * others (8 cols): down 0-7, left 8-15, right 16-23, up 24-31
+ *
+ * FX sheets are a single row of 8 frames (no direction).
+ *
+ * Animation key convention:
+ * Characters : "//" e.g. "player_rw/idle/down"
+ * FX : "" e.g. "fx_hitspark"
+ *
+ * Usage (plain ES module):
+ *
+ * import { createAllAnimations } from './AnimationHelper.js';
+ *
+ * // Inside a Phaser.Scene subclass:
+ * create() {
+ * createAllAnimations(this);
+ * }
+ */
+
+'use strict';
+
+// ─── SPEC (mirrors tools/generate-assets.js) ─────────────────────────────────
+
+/** Direction rows, in order. */
+const DIR_ROWS = ['down', 'left', 'right', 'up'];
+
+/** Animation name → number of frames per direction. */
+const ANIMS = { idle: 4, walk: 8, run: 8, attack: 8 };
+
+/** Suggested frame-rates per animation. */
+const FRAME_RATES = {
+ idle: 6,
+ walk: 12,
+ run: 18,
+ attack:14,
+};
+
+/** repeat: -1 = loop, 0 = play once */
+const ANIM_REPEAT = {
+ idle: -1,
+ walk: -1,
+ run: -1,
+ attack: 0,
+};
+
+/** All character spritesheet base IDs. */
+const CHAR_IDS = [
+ 'player_rw',
+ 'player_gw',
+ 'enemy_intern',
+ 'enemy_hr',
+ 'enemy_dev',
+ 'enemy_security',
+ 'enemy_accountant',
+ 'miniboss_vp',
+ 'boss_ceo',
+];
+
+/** FX spritesheet IDs (single-row, 8 frames, no direction). */
+const FX_IDS = [
+ 'fx_hitspark',
+ 'fx_slasharc',
+ 'fx_projectile',
+ 'fx_pickup',
+];
+
+// ─── HELPERS ─────────────────────────────────────────────────────────────────
+
+/**
+ * Create 4-directional animations for one character + one animation.
+ *
+ * @param {Phaser.Scene} scene
+ * @param {string} charId – e.g. "player_gw"
+ * @param {string} animName – e.g. "walk"
+ * @param {number} numFrames – frames per direction (4 or 8)
+ */
+function make4DirAnim(scene, charId, animName, numFrames) {
+ const sheetKey = `${charId}_${animName}`;
+ const frameRate = FRAME_RATES[animName] || 8;
+ const repeat = ANIM_REPEAT[animName] !== undefined ? ANIM_REPEAT[animName] : -1;
+
+ DIR_ROWS.forEach(function (dir, rowIdx) {
+ const key = `${charId}/${animName}/${dir}`;
+ const start = rowIdx * numFrames;
+ const end = start + numFrames - 1;
+
+ if (scene.anims.exists(key)) return; // idempotent
+
+ scene.anims.create({
+ key,
+ frames: scene.anims.generateFrameNumbers(sheetKey, { start, end }),
+ frameRate,
+ repeat,
+ });
+ });
+}
+
+/**
+ * Create a single-row FX animation.
+ *
+ * @param {Phaser.Scene} scene
+ * @param {string} fxId – e.g. "fx_hitspark"
+ */
+function makeFxAnim(scene, fxId) {
+ const key = fxId;
+ if (scene.anims.exists(key)) return;
+
+ scene.anims.create({
+ key,
+ frames: scene.anims.generateFrameNumbers(fxId, { start: 0, end: 7 }),
+ frameRate: 16,
+ repeat: 0,
+ });
+}
+
+// ─── PUBLIC API ──────────────────────────────────────────────────────────────
+
+/**
+ * Create every animation defined by the asset spec.
+ * Safe to call multiple times (existing keys are skipped).
+ *
+ * @param {Phaser.Scene} scene
+ */
+function createAllAnimations(scene) {
+ // Character animations
+ CHAR_IDS.forEach(function (charId) {
+ Object.entries(ANIMS).forEach(function ([animName, numFrames]) {
+ make4DirAnim(scene, charId, animName, numFrames);
+ });
+ });
+
+ // FX animations
+ FX_IDS.forEach(function (fxId) {
+ makeFxAnim(scene, fxId);
+ });
+}
+
+/**
+ * Create animations for a single character.
+ * Useful when loading characters lazily.
+ *
+ * @param {Phaser.Scene} scene
+ * @param {string} charId
+ */
+function createCharAnimations(scene, charId) {
+ Object.entries(ANIMS).forEach(function ([animName, numFrames]) {
+ make4DirAnim(scene, charId, animName, numFrames);
+ });
+}
+
+/**
+ * Return the animation key for a character, animation, and direction.
+ * Convenience wrapper to avoid typos in game code.
+ *
+ * @param {string} charId – e.g. "player_gw"
+ * @param {string} animName – "idle" | "walk" | "run" | "attack"
+ * @param {string} dir – "down" | "left" | "right" | "up"
+ * @returns {string}
+ */
+function animKey(charId, animName, dir) {
+ return `${charId}/${animName}/${dir}`;
+}
+
+// ─── EXPORTS ─────────────────────────────────────────────────────────────────
+
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = {
+ createAllAnimations,
+ createCharAnimations,
+ animKey,
+ CHAR_IDS,
+ FX_IDS,
+ ANIMS,
+ DIR_ROWS,
+ };
+} else if (typeof window !== 'undefined') {
+ window.AnimationHelper = {
+ createAllAnimations,
+ createCharAnimations,
+ animKey,
+ CHAR_IDS,
+ FX_IDS,
+ ANIMS,
+ DIR_ROWS,
+ };
+}
diff --git a/src/AssetLoader.js b/src/AssetLoader.js
new file mode 100644
index 0000000..f3f581d
--- /dev/null
+++ b/src/AssetLoader.js
@@ -0,0 +1,90 @@
+/**
+ * AssetLoader.js
+ *
+ * Phaser 3 helper – reads assets/asset-manifest.json and loads all
+ * sprite-sheets and atlases in a scene's preload() method.
+ *
+ * Usage (plain ES module):
+ *
+ * import { preloadAssets } from './AssetLoader.js';
+ *
+ * // Inside a Phaser.Scene subclass:
+ * preload() {
+ * preloadAssets(this);
+ * }
+ *
+ * The manifest path can be overridden via the second argument.
+ */
+
+'use strict';
+
+/** Default path to the asset manifest, relative to the Phaser base URL. */
+const DEFAULT_MANIFEST_PATH = 'assets/asset-manifest.json';
+
+/**
+ * Loads all assets declared in asset-manifest.json.
+ *
+ * @param {Phaser.Scene} scene – The Phaser scene (provides this.load).
+ * @param {string} [manifestPath] – Optional override for the manifest URL.
+ */
+function preloadAssets(scene, manifestPath) {
+ const url = manifestPath || DEFAULT_MANIFEST_PATH;
+
+ // Use Phaser's built-in JSON loader so the manifest is fetched
+ // asynchronously alongside other assets.
+ scene.load.json('__asset_manifest__', url);
+
+ // Once the manifest arrives we kick off the real asset loads.
+ scene.load.once('filecomplete-json-__asset_manifest__', function (key, type, data) {
+ _loadFromManifest(scene, data);
+ });
+}
+
+/**
+ * Loads all assets described in a manifest object.
+ * Can be called directly if you have the manifest data already.
+ *
+ * @param {Phaser.Scene} scene
+ * @param {object} manifest – { spritesheets: [...], atlases: [...] }
+ */
+function loadFromManifest(scene, manifest) {
+ _loadFromManifest(scene, manifest);
+}
+
+// ─── INTERNAL ────────────────────────────────────────────────────────────────
+
+function _loadFromManifest(scene, manifest) {
+ const loader = scene.load;
+
+ // Sprite-sheets
+ for (const ss of (manifest.spritesheets || [])) {
+ if (!loader.textureManager.exists(ss.key)) {
+ loader.spritesheet(ss.key, ss.path, {
+ frameWidth: ss.frameWidth,
+ frameHeight: ss.frameHeight,
+ });
+ }
+ }
+
+ // Atlases (TexturePacker hash format)
+ for (const atlas of (manifest.atlases || [])) {
+ if (!loader.textureManager.exists(atlas.key)) {
+ loader.atlas(atlas.key, atlas.png, atlas.json);
+ }
+ }
+
+ // Restart the loader if it is not already running so the new entries
+ // are fetched immediately (necessary when called from filecomplete).
+ if (!loader.isLoading()) {
+ loader.start();
+ }
+}
+
+// ─── EXPORTS ─────────────────────────────────────────────────────────────────
+
+// Support both CommonJS (Node / bundler) and plain browser ES modules.
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = { preloadAssets, loadFromManifest };
+} else if (typeof window !== 'undefined') {
+ window.AssetLoader = { preloadAssets, loadFromManifest };
+}
diff --git a/src/main.js b/src/main.js
new file mode 100644
index 0000000..97d49c2
--- /dev/null
+++ b/src/main.js
@@ -0,0 +1,193 @@
+/**
+ * main.js – That One Game – GitHub Spark entry point
+ *
+ * Scenes:
+ * BootScene – tiny loading screen while the asset manifest is fetched
+ * PreloadScene – loads every spritesheet and atlas via AssetLoader
+ * GameScene – registers all animations and shows a playable demo
+ *
+ * Keyboard controls (GameScene demo):
+ * Arrow keys / WASD – move the player
+ * SHIFT – hold to run
+ * SPACE – attack
+ * X key – toggle Real-World ↔ Game-World skin
+ */
+
+'use strict';
+
+// ─── CONSTANTS ────────────────────────────────────────────────────────────────
+
+const TILE = 32; // px per logical tile
+const SPEED = {walk: 80, run: 150};
+
+// ─── BOOT SCENE ──────────────────────────────────────────────────────────────
+
+class BootScene extends Phaser.Scene {
+ constructor() { super('BootScene'); }
+
+ preload() {
+ // Show a simple loading bar while the manifest JSON is fetched
+ const W = this.scale.width;
+ const H = this.scale.height;
+
+ this.add.text(W / 2, H / 2 - 20, 'Loading…', {
+ fontFamily: 'monospace', fontSize: '20px', color: '#ffffff',
+ }).setOrigin(0.5);
+
+ const bar = this.add.rectangle(W / 2, H / 2 + 10, 0, 10, 0x44ff88).setOrigin(0, 0.5);
+ const bg = this.add.rectangle(W / 2 - 150, H / 2 + 10, 300, 10, 0x336633).setOrigin(0, 0.5);
+ bg.setDepth(0); bar.setDepth(1);
+
+ this.load.on('progress', (v) => { bar.width = 300 * v; });
+
+ // Hand off to AssetLoader (attached to window by AssetLoader.js)
+ window.AssetLoader.preloadAssets(this);
+ }
+
+ create() {
+ this.scene.start('GameScene');
+ }
+}
+
+// ─── GAME SCENE ──────────────────────────────────────────────────────────────
+
+class GameScene extends Phaser.Scene {
+ constructor() { super('GameScene'); }
+
+ create() {
+ const { width: W, height: H } = this.scale;
+ const AH = window.AnimationHelper;
+
+ // Register all animations (idempotent)
+ AH.createAllAnimations(this);
+
+ // ── Background ────────────────────────────────────────────────────────────
+ this.add.rectangle(0, 0, W, H, 0x1a1a2e).setOrigin(0);
+
+ // Draw a simple tiled floor from the atlas
+ this._drawFloor(W, H);
+
+ // ── Player sprite ─────────────────────────────────────────────────────────
+ this._world = 'rw'; // 'rw' or 'gw'
+ this._dir = 'down';
+ this._moving = false;
+ this._running = false;
+ this._attacking = false;
+
+ this.player = this.add.sprite(W / 2, H / 2, 'player_rw_idle')
+ .setScale(3);
+
+ this.player.play(AH.animKey('player_rw', 'idle', 'down'));
+
+ // ── Input ─────────────────────────────────────────────────────────────────
+ this.cursors = this.input.keyboard.createCursorKeys();
+ this.wasd = this.input.keyboard.addKeys('W,A,S,D');
+ this.shiftKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SHIFT);
+ this.spaceKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);
+ this.swapKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.X);
+
+ // Attack fires once per keydown
+ this.input.keyboard.on('keydown-SPACE', () => this._startAttack());
+
+ // Swap world skin
+ this.input.keyboard.on('keydown-X', () => this._swapWorld());
+
+ // ── HUD ───────────────────────────────────────────────────────────────────
+ this.worldLabel = this.add.text(10, 10,
+ 'World: Real | X = swap world | Arrows/WASD = move | SHIFT = run | SPACE = attack', {
+ fontFamily: 'monospace', fontSize: '11px', color: '#aaffaa',
+ });
+ }
+
+ update() {
+ if (this._attacking) return; // lock movement during attack
+
+ const left = this.cursors.left.isDown || this.wasd.A.isDown;
+ const right = this.cursors.right.isDown || this.wasd.D.isDown;
+ const up = this.cursors.up.isDown || this.wasd.W.isDown;
+ const down = this.cursors.down.isDown || this.wasd.S.isDown;
+ const run = this.shiftKey.isDown;
+
+ const moving = left || right || up || down;
+ const speed = run ? SPEED.run : SPEED.walk;
+ const animName = moving ? (run ? 'run' : 'walk') : 'idle';
+ const AH = window.AnimationHelper;
+
+ // Direction
+ if (left) this._dir = 'left';
+ else if (right) this._dir = 'right';
+ else if (up) this._dir = 'up';
+ else if (down) this._dir = 'down';
+
+ // Position
+ const dt = this.game.loop.delta / 1000;
+ if (left) this.player.x -= speed * dt;
+ if (right) this.player.x += speed * dt;
+ if (up) this.player.y -= speed * dt;
+ if (down) this.player.y += speed * dt;
+
+ // Clamp to screen
+ this.player.x = Phaser.Math.Clamp(this.player.x, 16, this.scale.width - 16);
+ this.player.y = Phaser.Math.Clamp(this.player.y, 16, this.scale.height - 16);
+
+ // Play animation only when it changes
+ const newKey = AH.animKey(`player_${this._world}`, animName, this._dir);
+ if (this.player.anims.currentAnim?.key !== newKey) {
+ this.player.play(newKey);
+ }
+ }
+
+ // ── Private helpers ──────────────────────────────────────────────────────
+
+ _drawFloor(W, H) {
+ // Draw a checkerboard pattern as a stand-in for tiles
+ const cols = Math.ceil(W / TILE) + 1;
+ const rows = Math.ceil(H / TILE) + 1;
+ for (let r = 0; r < rows; r++) {
+ for (let c = 0; c < cols; c++) {
+ const col = (r + c) % 2 === 0 ? 0x111122 : 0x0d0d1a;
+ this.add.rectangle(c * TILE, r * TILE, TILE, TILE, col)
+ .setOrigin(0).setAlpha(0.8);
+ }
+ }
+ }
+
+ _startAttack() {
+ if (this._attacking) return;
+ this._attacking = true;
+ const key = window.AnimationHelper.animKey(`player_${this._world}`, 'attack', this._dir);
+ this.player.play(key);
+ this.player.once('animationcomplete', () => {
+ this._attacking = false;
+ });
+ }
+
+ _swapWorld() {
+ this._world = this._world === 'rw' ? 'gw' : 'rw';
+ const label = this._world === 'rw' ? 'Real' : 'Game';
+ this.worldLabel.setText(
+ `World: ${label} | X = swap world | Arrows/WASD = move | SHIFT = run | SPACE = attack`
+ );
+ // Reset to idle so the new texture loads cleanly
+ const key = window.AnimationHelper.animKey(`player_${this._world}`, 'idle', this._dir);
+ this.player.play(key);
+ this._attacking = false;
+ }
+}
+
+// ─── PHASER CONFIG ────────────────────────────────────────────────────────────
+
+window.addEventListener('load', function () {
+ new Phaser.Game({
+ type: Phaser.AUTO,
+ width: 800,
+ height: 500,
+ backgroundColor: '#1a1a2e',
+ scene: [BootScene, GameScene],
+ parent: 'game-container',
+ scale: {
+ mode: Phaser.Scale.FIT,
+ autoCenter: Phaser.Scale.CENTER_BOTH,
+ },
+ });
+});
diff --git a/tools/generate-assets.js b/tools/generate-assets.js
new file mode 100644
index 0000000..f23cf10
--- /dev/null
+++ b/tools/generate-assets.js
@@ -0,0 +1,1024 @@
+#!/usr/bin/env node
+'use strict';
+
+/**
+ * generate-assets.js
+ *
+ * Generates all placeholder PNG sprite-sheets, atlas PNGs + JSONs, and
+ * assets/asset-manifest.json for "That One Game".
+ *
+ * Run: node tools/generate-assets.js
+ *
+ * Outputs (deterministic, safe to re-run):
+ * assets/spritesheets/rw/*.png
+ * assets/spritesheets/gw/*.png
+ * assets/atlas/rw/atlas_rw_world.{png,json}
+ * assets/atlas/rw/atlas_rw_ui.{png,json}
+ * assets/atlas/gw/atlas_gw_world.{png,json}
+ * assets/atlas/gw/atlas_gw_ui.{png,json}
+ * assets/asset-manifest.json
+ */
+
+const { createCanvas } = require('canvas');
+const fs = require('fs');
+const path = require('path');
+
+// ─── PATHS ────────────────────────────────────────────────────────────────────
+const ROOT = path.join(__dirname, '..');
+const ASSETS_DIR = path.join(ROOT, 'assets');
+const SS_RW = path.join(ASSETS_DIR, 'spritesheets', 'rw');
+const SS_GW = path.join(ASSETS_DIR, 'spritesheets', 'gw');
+const ATLAS_RW = path.join(ASSETS_DIR, 'atlas', 'rw');
+const ATLAS_GW = path.join(ASSETS_DIR, 'atlas', 'gw');
+
+// ─── SPRITE-SHEET SPEC ───────────────────────────────────────────────────────
+const DIR_ROWS = ['down', 'left', 'right', 'up']; // Row 0-3
+const ANIMS = { idle: 4, walk: 8, run: 8, attack: 8 };
+const FX_FRAMES = 8;
+
+// ─── CHARACTER DEFINITIONS ───────────────────────────────────────────────────
+/**
+ * Each character definition:
+ * world – 'rw' | 'gw' (determines output subdir and style)
+ * style – 'cartoon' | 'pixel'
+ * frameSize – 32 | 64
+ * body – torso / suit colour
+ * head – skin colour
+ * hair – hair colour (also used as accent)
+ * eyes – eye colour (red for enemies)
+ * legs – leg / trouser colour
+ * tie – tie / detail colour (optional)
+ * boss – true if boss-tier (gets a slightly different silhouette)
+ */
+const CHARS = {
+ // ── Player ────────────────────────────────────────────────────────────────
+ player_rw: {
+ world: 'rw', style: 'cartoon', frameSize: 32,
+ body: '#2277DD', head: '#FFCC99', hair: '#FF6600',
+ eyes: '#333333', legs: '#4444AA', tie: null,
+ },
+ player_gw: {
+ world: 'gw', style: 'pixel', frameSize: 32,
+ body: '#2277DD', head: '#FFCC99', hair: '#FF6600',
+ eyes: '#333333', legs: '#4444AA', tie: null,
+ },
+ // ── Regular enemies ───────────────────────────────────────────────────────
+ enemy_intern: {
+ world: 'gw', style: 'pixel', frameSize: 32,
+ body: '#AAAAAA', head: '#FFCC99', hair: '#555555',
+ eyes: '#FF0000', legs: '#888888', tie: '#CC0000',
+ },
+ enemy_hr: {
+ world: 'gw', style: 'pixel', frameSize: 32,
+ body: '#CC4488', head: '#FFCC99', hair: '#881133',
+ eyes: '#FF0000', legs: '#AA2266', tie: '#FF88BB',
+ },
+ enemy_dev: {
+ world: 'gw', style: 'pixel', frameSize: 32,
+ body: '#33AA33', head: '#FFCC99', hair: '#115511',
+ eyes: '#FF0000', legs: '#226622', tie: '#88FF44',
+ },
+ enemy_security: {
+ world: 'gw', style: 'pixel', frameSize: 32,
+ body: '#111122', head: '#FFCC99', hair: '#000000',
+ eyes: '#FF0000', legs: '#080815', tie: '#FF0000',
+ },
+ enemy_accountant: {
+ world: 'gw', style: 'pixel', frameSize: 32,
+ body: '#774400', head: '#FFCC99', hair: '#442200',
+ eyes: '#FF0000', legs: '#553300', tie: '#FFCC00',
+ },
+ // ── Mini-boss VP (32×32) ──────────────────────────────────────────────────
+ miniboss_vp: {
+ world: 'gw', style: 'pixel', frameSize: 32,
+ body: '#111166', head: '#FFCC99', hair: '#000033',
+ eyes: '#FF0000', legs: '#0A0A44', tie: '#FFCC00', boss: true,
+ },
+ // ── Boss CEO (64×64) ──────────────────────────────────────────────────────
+ boss_ceo: {
+ world: 'gw', style: 'pixel', frameSize: 64,
+ body: '#0A0A0A', head: '#CCAA88', hair: '#000000',
+ eyes: '#FF0000', legs: '#050505', tie: '#CC0000', boss: true,
+ },
+};
+
+// ─── FX DEFINITIONS ──────────────────────────────────────────────────────────
+const FX_DEFS = {
+ fx_hitspark: { colors: ['#FFFF44','#FFAA00','#FF4400','#CC0000'], shape: 'spark' },
+ fx_slasharc: { colors: ['#FFFFFF','#CCDDFF','#88AAFF','#4466DD'], shape: 'arc' },
+ fx_projectile: { colors: ['#00FFFF','#00AAFF','#0055FF','#002299'], shape: 'orb' },
+ fx_pickup: { colors: ['#FFFF88','#FFCCFF','#FF88FF','#AA44FF'], shape: 'sparkle' },
+};
+
+// ─── ATLAS FRAME DEFINITIONS ─────────────────────────────────────────────────
+// Each entry: { name, w, h, color, shape, detail }
+// shape – hint for the placeholder drawing routine
+// detail – accent / line colour
+
+const ATLAS_RW_WORLD_FRAMES = [
+ // Buildings
+ { name: 'building_office', w: 64, h: 96, color: '#CCE0FF', shape: 'building', detail: '#3366AA' },
+ { name: 'building_shop', w: 64, h: 80, color: '#FFEEDD', shape: 'building', detail: '#CC6633' },
+ { name: 'building_house', w: 64, h: 64, color: '#EEDDCC', shape: 'building', detail: '#885533' },
+ { name: 'building_apartment', w: 48, h: 96, color: '#DDEECC', shape: 'building', detail: '#447733' },
+ // Props
+ { name: 'prop_desk', w: 48, h: 32, color: '#AA7755', shape: 'rect', detail: '#774433' },
+ { name: 'prop_chair', w: 32, h: 32, color: '#3355AA', shape: 'rect', detail: '#112266' },
+ { name: 'prop_computer', w: 32, h: 32, color: '#222244', shape: 'rect', detail: '#00AAFF' },
+ { name: 'prop_plant', w: 32, h: 48, color: '#33AA33', shape: 'rect', detail: '#226622' },
+ { name: 'prop_filing_cabinet', w: 32, h: 48, color: '#AAAACC', shape: 'rect', detail: '#666688' },
+ { name: 'prop_coffee_machine', w: 32, h: 48, color: '#333333', shape: 'rect', detail: '#FF6600' },
+ { name: 'prop_water_cooler', w: 24, h: 48, color: '#AADDFF', shape: 'rect', detail: '#3399FF' },
+ { name: 'prop_printer', w: 48, h: 32, color: '#CCCCCC', shape: 'rect', detail: '#666666' },
+ // Tiles
+ { name: 'tile_floor_office', w: 32, h: 32, color: '#EEEEFF', shape: 'tile', detail: '#CCCCEE' },
+ { name: 'tile_floor_carpet', w: 32, h: 32, color: '#5566AA', shape: 'tile', detail: '#3344AA' },
+ { name: 'tile_wall_office', w: 32, h: 32, color: '#DDDDEE', shape: 'tile', detail: '#BBBBCC' },
+ { name: 'tile_floor_sidewalk', w: 32, h: 32, color: '#CCCCBB', shape: 'tile', detail: '#AAAAAA' },
+ { name: 'tile_floor_road', w: 32, h: 32, color: '#444444', shape: 'tile', detail: '#666666' },
+ { name: 'tile_grass', w: 32, h: 32, color: '#44AA44', shape: 'tile', detail: '#338833' },
+ // Environmental
+ { name: 'env_tree', w: 48, h: 64, color: '#226622', shape: 'tree', detail: '#774422' },
+ { name: 'env_streetlight', w: 16, h: 64, color: '#888866', shape: 'rect', detail: '#FFFF88' },
+ { name: 'env_bench', w: 48, h: 32, color: '#AA7744', shape: 'rect', detail: '#774422' },
+ { name: 'env_trash_can', w: 24, h: 32, color: '#555555', shape: 'rect', detail: '#333333' },
+ { name: 'env_mailbox', w: 24, h: 32, color: '#1133AA', shape: 'rect', detail: '#0022AA' },
+];
+
+const ATLAS_RW_UI_FRAMES = [
+ { name: 'ui_button_normal', w: 120, h: 40, color: '#4477FF', shape: 'button', detail: '#FFFFFF' },
+ { name: 'ui_button_hover', w: 120, h: 40, color: '#5599FF', shape: 'button', detail: '#FFFFFF' },
+ { name: 'ui_button_pressed', w: 120, h: 40, color: '#2255CC', shape: 'button', detail: '#DDDDFF' },
+ { name: 'ui_panel', w: 200, h: 150, color: '#1A2244', shape: 'panel', detail: '#3355AA' },
+ { name: 'ui_panel_header', w: 200, h: 40, color: '#223366', shape: 'panel', detail: '#4477CC' },
+ { name: 'ui_health_bar_bg', w: 100, h: 16, color: '#442222', shape: 'bar', detail: '#661111' },
+ { name: 'ui_health_bar_fill', w: 100, h: 16, color: '#FF3333', shape: 'bar', detail: '#FF6666' },
+ { name: 'ui_icon_health', w: 24, h: 24, color: '#FF3333', shape: 'icon', detail: '#FF6666' },
+ { name: 'ui_icon_stamina', w: 24, h: 24, color: '#33FF33', shape: 'icon', detail: '#66FF66' },
+ { name: 'ui_icon_coin', w: 24, h: 24, color: '#FFCC00', shape: 'icon', detail: '#FFEE88' },
+ { name: 'ui_icon_key', w: 24, h: 24, color: '#FFAA44', shape: 'icon', detail: '#FFCC88' },
+ { name: 'ui_dialog_box', w: 300, h: 80, color: '#1A2244', shape: 'panel', detail: '#3355AA' },
+ { name: 'ui_cursor', w: 32, h: 32, color: '#FFFFFF', shape: 'cursor', detail: '#4477FF' },
+ { name: 'ui_minimap_bg', w: 128, h: 128, color: '#0A1122', shape: 'panel', detail: '#1A2244' },
+ { name: 'ui_minimap_player', w: 8, h: 8, color: '#00FF88', shape: 'rect', detail: '#00FF88' },
+];
+
+const ATLAS_GW_WORLD_FRAMES = [
+ // Buildings (pixel, game world)
+ { name: 'gw_building_server', w: 64, h: 96, color: '#112233', shape: 'building', detail: '#00AAFF' },
+ { name: 'gw_building_arena', w: 96, h: 96, color: '#221133', shape: 'building', detail: '#AA00FF' },
+ { name: 'gw_building_portal', w: 48, h: 64, color: '#003322', shape: 'building', detail: '#00FF88' },
+ // Props
+ { name: 'gw_prop_terminal', w: 32, h: 48, color: '#112244', shape: 'rect', detail: '#00AAFF' },
+ { name: 'gw_prop_chest', w: 32, h: 32, color: '#885500', shape: 'rect', detail: '#FFCC00' },
+ { name: 'gw_prop_barrel', w: 24, h: 32, color: '#553300', shape: 'rect', detail: '#886644' },
+ { name: 'gw_prop_crate', w: 32, h: 32, color: '#665533', shape: 'rect', detail: '#998866' },
+ { name: 'gw_prop_crystal', w: 24, h: 32, color: '#8800FF', shape: 'diamond', detail: '#CC88FF' },
+ // Items
+ { name: 'item_sword', w: 16, h: 32, color: '#AAAAAA', shape: 'sword', detail: '#CCCCCC' },
+ { name: 'item_shield', w: 24, h: 24, color: '#3355AA', shape: 'shield', detail: '#6688CC' },
+ { name: 'item_potion_red', w: 16, h: 24, color: '#FF3333', shape: 'potion', detail: '#FF6666' },
+ { name: 'item_potion_blue', w: 16, h: 24, color: '#3333FF', shape: 'potion', detail: '#6666FF' },
+ { name: 'item_coin', w: 16, h: 16, color: '#FFCC00', shape: 'circle', detail: '#FFEE88' },
+ { name: 'item_key', w: 16, h: 24, color: '#FFAA44', shape: 'rect', detail: '#FFCC88' },
+ { name: 'item_badge', w: 24, h: 24, color: '#334455', shape: 'rect', detail: '#FF0000' },
+ { name: 'item_power_chip', w: 16, h: 16, color: '#001133', shape: 'rect', detail: '#00AAFF' },
+ // Tiles
+ { name: 'gw_tile_floor_dungeon', w: 32, h: 32, color: '#221122', shape: 'tile', detail: '#332233' },
+ { name: 'gw_tile_floor_circuit', w: 32, h: 32, color: '#001122', shape: 'tile', detail: '#002244' },
+ { name: 'gw_tile_wall_dungeon', w: 32, h: 32, color: '#332233', shape: 'tile', detail: '#443344' },
+ { name: 'gw_tile_wall_circuit', w: 32, h: 32, color: '#112233', shape: 'tile', detail: '#223355' },
+ { name: 'gw_tile_floor_boss', w: 32, h: 32, color: '#110011', shape: 'tile', detail: '#330022' },
+ // Environmental
+ { name: 'gw_env_pillar', w: 32, h: 48, color: '#334455', shape: 'rect', detail: '#556677' },
+ { name: 'gw_env_spike', w: 32, h: 32, color: '#AA2222', shape: 'diamond', detail: '#FF3333' },
+ { name: 'gw_env_portal_ring', w: 48, h: 48, color: '#8800FF', shape: 'ring', detail: '#CC88FF' },
+ { name: 'gw_env_data_stream', w: 16, h: 64, color: '#00FF88', shape: 'rect', detail: '#00FFCC' },
+];
+
+const ATLAS_GW_UI_FRAMES = [
+ { name: 'gw_ui_button_normal', w: 120, h: 40, color: '#220033', shape: 'button', detail: '#AA00FF' },
+ { name: 'gw_ui_button_hover', w: 120, h: 40, color: '#330044', shape: 'button', detail: '#CC44FF' },
+ { name: 'gw_ui_button_pressed', w: 120, h: 40, color: '#110022', shape: 'button', detail: '#8800DD' },
+ { name: 'gw_ui_panel', w: 200, h: 150, color: '#0A0022', shape: 'panel', detail: '#220055' },
+ { name: 'gw_ui_health_bar_bg', w: 100, h: 16, color: '#330000', shape: 'bar', detail: '#440000' },
+ { name: 'gw_ui_health_bar_fill', w: 100, h: 16, color: '#FF0033', shape: 'bar', detail: '#FF4466' },
+ { name: 'gw_ui_energy_bar_bg', w: 100, h: 16, color: '#001133', shape: 'bar', detail: '#002244' },
+ { name: 'gw_ui_energy_bar_fill', w: 100, h: 16, color: '#0099FF', shape: 'bar', detail: '#44BBFF' },
+ { name: 'gw_ui_icon_hp', w: 24, h: 24, color: '#FF0033', shape: 'icon', detail: '#FF4466' },
+ { name: 'gw_ui_icon_mp', w: 24, h: 24, color: '#0099FF', shape: 'icon', detail: '#44BBFF' },
+ { name: 'gw_ui_icon_exp', w: 24, h: 24, color: '#FFCC00', shape: 'icon', detail: '#FFEE88' },
+ { name: 'gw_ui_icon_gold', w: 24, h: 24, color: '#FFAA00', shape: 'icon', detail: '#FFCC44' },
+ { name: 'gw_ui_dialog_box', w: 300, h: 80, color: '#0A0022', shape: 'panel', detail: '#220055' },
+ { name: 'gw_ui_cursor', w: 32, h: 32, color: '#AA00FF', shape: 'cursor', detail: '#CC88FF' },
+ { name: 'gw_ui_minimap_bg', w: 128, h: 128, color: '#050011', shape: 'panel', detail: '#110033' },
+ { name: 'gw_ui_hud_frame', w: 256, h: 64, color: '#0A0022', shape: 'panel', detail: '#330066' },
+ { name: 'gw_ui_level_up_banner', w: 200, h: 48, color: '#220044', shape: 'panel', detail: '#FF44FF' },
+];
+
+// ─── UTILITY ─────────────────────────────────────────────────────────────────
+
+function ensureDir(p) {
+ fs.mkdirSync(p, { recursive: true });
+}
+
+function nextPow2(n) {
+ let p = 1;
+ while (p < n) p <<= 1;
+ return p;
+}
+
+function savePNG(canvas, filePath) {
+ const buf = canvas.toBuffer('image/png');
+ fs.writeFileSync(filePath, buf);
+ console.log(' wrote', path.relative(path.join(__dirname, '..'), filePath));
+}
+
+// ─── CHARACTER DRAWING ───────────────────────────────────────────────────────
+
+/**
+ * Compute per-frame animation offsets.
+ * Returns { bodyDY, legToggle, strikePhase }
+ */
+function animOffsets(animName, frameIdx, p) {
+ const pat4 = [0, -p * 0.25, -p * 0.5, -p * 0.25];
+ if (animName === 'idle') {
+ return { bodyDY: pat4[frameIdx % 4], legToggle: false, strikePhase: false };
+ }
+ if (animName === 'walk') {
+ return { bodyDY: (frameIdx % 2) * (-p * 0.5), legToggle: frameIdx % 2 === 1, strikePhase: false };
+ }
+ if (animName === 'run') {
+ return { bodyDY: (frameIdx % 2) * (-p), legToggle: frameIdx % 2 === 1, strikePhase: false };
+ }
+ if (animName === 'attack') {
+ const strike = frameIdx >= 4;
+ return { bodyDY: strike ? -p * 0.25 : 0, legToggle: false, strikePhase: strike };
+ }
+ return { bodyDY: 0, legToggle: false, strikePhase: false };
+}
+
+/**
+ * Draw one pixel-art character frame at canvas pixel offset (ox, oy).
+ * sz = frameSize (32 or 64).
+ */
+function drawPixelChar(ctx, char, dir, animName, frameIdx, ox, oy, sz) {
+ const p = sz >= 64 ? 8 : 4; // one "logical pixel" in actual pixels
+ const { bodyDY, legToggle, strikePhase } = animOffsets(animName, frameIdx, p);
+ const snap = (v) => Math.round(v / p) * p;
+ const bdy = snap(bodyDY);
+
+ // Layout (in actual pixels within the sz×sz frame)
+ const headW = 2 * p, headH = 2 * p;
+ const bodyW = 3 * p, bodyH = 2 * p;
+ const legW = p, legH = 2 * p;
+
+ const headX = snap((sz - headW) / 2);
+ const headY = p + bdy;
+ const bodyX = snap((sz - bodyW) / 2);
+ const bodyY = headY + headH;
+ const legY = bodyY + bodyH;
+
+ const fill = (x, y, w, h, color) => {
+ if (w <= 0 || h <= 0) return;
+ ctx.fillStyle = color;
+ ctx.fillRect(ox + Math.round(x), oy + Math.round(y),
+ Math.max(1, Math.round(w)), Math.max(1, Math.round(h)));
+ };
+
+ // Shadow
+ ctx.save();
+ ctx.fillStyle = 'rgba(0,0,0,0.18)';
+ ctx.beginPath();
+ ctx.ellipse(ox + sz / 2, oy + sz - p * 0.4, sz * 0.28, p * 0.55, 0, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.restore();
+
+ // Legs
+ if (dir === 'left' || dir === 'right') {
+ fill(bodyX + p * 0.5, legY, bodyW - p, legH, char.legs);
+ } else {
+ const l1DY = legToggle ? p * 0.5 : 0;
+ const l2DY = legToggle ? 0 : p * 0.5;
+ fill(bodyX, legY + l1DY, legW, legH - l1DY, char.legs);
+ fill(bodyX + bodyW - legW, legY + l2DY, legW, legH - l2DY, char.legs);
+ }
+
+ // Body
+ if (dir === 'left' || dir === 'right') {
+ fill(bodyX + p * 0.25, bodyY, bodyW - p * 0.5, bodyH, char.body);
+ } else {
+ fill(bodyX, bodyY, bodyW, bodyH, char.body);
+ }
+ // Tie stripe
+ if (char.tie) {
+ fill(bodyX + snap(bodyW * 0.38), bodyY, p * 0.5, bodyH, char.tie);
+ }
+
+ // Boss crown / aura indicator (extra decoration for boss-tier characters)
+ if (char.boss && sz >= 32) {
+ const crownW = p, crownH = p * 0.5;
+ fill(headX + headW * 0.25, headY - crownH, crownW, crownH, char.tie || char.hair);
+ fill(headX + headW * 0.75 - crownW * 0.5, headY - crownH, crownW, crownH, char.tie || char.hair);
+ }
+
+ // Attack arm
+ if (animName === 'attack') {
+ const armW = p * 1.5, armH = p;
+ const armY = bodyY + p * 0.5;
+ if (strikePhase) {
+ let armX;
+ if (dir === 'left') armX = bodyX - armW;
+ else if (dir === 'right') armX = bodyX + bodyW;
+ else armX = bodyX + bodyW; // down / up
+ fill(armX, armY, armW, armH, char.head);
+ // Weapon tip (bright accent)
+ const wpX = dir === 'left' ? armX - p * 0.5 : armX + armW;
+ fill(wpX, armY - p * 0.5, p * 0.5, armH * 2, char.hair);
+ } else {
+ // Wind-up: arm on opposite side
+ let armX;
+ if (dir === 'left') armX = bodyX + bodyW;
+ else armX = bodyX - p * 1.5;
+ fill(armX, armY, armW, armH, char.head);
+ }
+ }
+
+ // Head
+ fill(headX, headY, headW, headH, char.head);
+ // Hair (top half)
+ fill(headX, headY, headW, Math.max(p * 0.5, Math.round(headH * 0.4)), char.hair);
+
+ // Eyes
+ const eyeSize = Math.max(1, Math.round(p * 0.5));
+ const eyeY = headY + Math.round(headH * 0.55);
+ if (dir === 'down') {
+ fill(headX + Math.round(p * 0.3), eyeY, eyeSize, eyeSize, char.eyes);
+ fill(headX + headW - Math.round(p * 0.3) - eyeSize, eyeY, eyeSize, eyeSize, char.eyes);
+ } else if (dir === 'left') {
+ fill(headX + Math.round(p * 0.3), eyeY, eyeSize, eyeSize, char.eyes);
+ } else if (dir === 'right') {
+ fill(headX + headW - Math.round(p * 0.3) - eyeSize, eyeY, eyeSize, eyeSize, char.eyes);
+ }
+ // dir === 'up': no eyes visible
+
+ // Direction indicator: small teal dot at edge of frame
+ ctx.save();
+ ctx.fillStyle = 'rgba(0,255,160,0.55)';
+ ctx.beginPath();
+ const dr = Math.max(1.5, p * 0.35);
+ let dx = ox + sz * 0.5, dy = oy + sz * 0.5;
+ if (dir === 'down') dy = oy + sz - dr - 1;
+ if (dir === 'up') dy = oy + dr + 1;
+ if (dir === 'left') dx = ox + dr + 1;
+ if (dir === 'right') dx = ox + sz - dr - 1;
+ ctx.arc(dx, dy, dr, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.restore();
+}
+
+/**
+ * Draw one cartoon-style character frame at (ox, oy).
+ * Uses smooth arcs instead of pixel blocks.
+ */
+function drawCartoonChar(ctx, char, dir, animName, frameIdx, ox, oy, sz) {
+ const u = sz / 32;
+ const p = 4 * u;
+ const { bodyDY, legToggle, strikePhase } = animOffsets(animName, frameIdx, p);
+
+ const bodyW = 12 * u, bodyH = 10 * u;
+ const bodyX = (sz - bodyW) / 2;
+ const bodyY = sz * 0.45 + bodyDY;
+ const headR = 6.5 * u;
+ const headCX = sz * 0.5;
+ const headCY = bodyY - headR;
+ const legW = 4.5 * u, legH = 8 * u;
+ const legY = bodyY + bodyH;
+
+ const fillRect = (x, y, w, h, color) => {
+ if (w <= 0 || h <= 0) return;
+ ctx.fillStyle = color;
+ ctx.fillRect(ox + x, oy + y, w, h);
+ };
+ const fillCircle = (cx, cy, r, color) => {
+ ctx.fillStyle = color;
+ ctx.beginPath();
+ ctx.arc(ox + cx, oy + cy, r, 0, Math.PI * 2);
+ ctx.fill();
+ };
+
+ // Shadow
+ ctx.save();
+ ctx.fillStyle = 'rgba(0,0,0,0.15)';
+ ctx.beginPath();
+ ctx.ellipse(ox + sz * 0.5, oy + sz - 2 * u, sz * 0.28, 2.5 * u, 0, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.restore();
+
+ // Legs
+ if (dir === 'left' || dir === 'right') {
+ fillRect(bodyX + legW * 0.5, legY, bodyW - legW, legH, char.legs);
+ } else {
+ const l1DY = legToggle ? p * 0.5 : 0;
+ const l2DY = legToggle ? 0 : p * 0.5;
+ fillRect(bodyX + u, legY + l1DY, legW, legH - l1DY, char.legs);
+ fillRect(bodyX + bodyW - legW - u, legY + l2DY, legW, legH - l2DY, char.legs);
+ }
+
+ // Body (rounded rect approximated as rect)
+ fillRect(bodyX, bodyY, bodyW, bodyH, char.body);
+ // Tie
+ if (char.tie) {
+ fillRect(bodyX + bodyW * 0.42, bodyY, 2 * u, bodyH, char.tie);
+ }
+
+ // Attack arm
+ if (animName === 'attack') {
+ const armW = 9 * u, armH = 4 * u;
+ const armY = bodyY + bodyH * 0.2;
+ if (strikePhase) {
+ let armX = dir === 'left' ? bodyX - armW : bodyX + bodyW;
+ fillRect(armX, armY, armW, armH, char.head);
+ const wpX = dir === 'left' ? armX - p * 0.5 : armX + armW;
+ fillRect(wpX, armY - p * 0.4, p * 0.4, armH * 2, char.hair);
+ } else {
+ let armX = dir === 'left' ? bodyX + bodyW : bodyX - 9 * u;
+ fillRect(armX, armY, armW, armH, char.head);
+ }
+ }
+
+ // Head
+ fillCircle(headCX, headCY, headR, char.head);
+ // Hair (upper half arc)
+ ctx.save();
+ ctx.fillStyle = char.hair;
+ ctx.beginPath();
+ ctx.arc(ox + headCX, oy + headCY, headR, Math.PI, 2 * Math.PI);
+ ctx.fill();
+ ctx.restore();
+
+ // Eyes
+ if (dir !== 'up') {
+ const eyeR = Math.max(1.5, 1.5 * u);
+ const eyeY = headCY + headR * 0.35;
+ if (dir === 'down') {
+ fillCircle(headCX - headR * 0.38, eyeY, eyeR, char.eyes);
+ fillCircle(headCX + headR * 0.38, eyeY, eyeR, char.eyes);
+ } else if (dir === 'left') {
+ fillCircle(headCX - headR * 0.3, eyeY, eyeR, char.eyes);
+ } else {
+ fillCircle(headCX + headR * 0.3, eyeY, eyeR, char.eyes);
+ }
+ }
+
+ // Direction dot
+ ctx.save();
+ ctx.fillStyle = 'rgba(0,255,160,0.55)';
+ ctx.beginPath();
+ const dr = Math.max(1.5, 2 * u);
+ let dx = ox + sz * 0.5, dy = oy + sz * 0.5;
+ if (dir === 'down') dy = oy + sz - dr - 1;
+ if (dir === 'up') dy = oy + dr + 1;
+ if (dir === 'left') dx = ox + dr + 1;
+ if (dir === 'right') dx = ox + sz - dr - 1;
+ ctx.arc(dx, dy, dr, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.restore();
+}
+
+/**
+ * Draw one character frame – dispatches to pixel or cartoon renderer.
+ */
+function drawCharFrame(ctx, char, dir, animName, frameIdx, ox, oy, sz) {
+ if (char.style === 'pixel') {
+ drawPixelChar(ctx, char, dir, animName, frameIdx, ox, oy, sz);
+ } else {
+ drawCartoonChar(ctx, char, dir, animName, frameIdx, ox, oy, sz);
+ }
+}
+
+// ─── FX DRAWING ──────────────────────────────────────────────────────────────
+
+/**
+ * Draw one FX frame at (ox, oy) on a 32×32 cell.
+ * frame 0 = start, 7 = end.
+ */
+function drawFxFrame(ctx, fxDef, frameIdx, ox, oy) {
+ const sz = 32;
+ const t = frameIdx / (FX_FRAMES - 1); // 0..1
+ const tinv = 1 - t;
+ const cx = ox + sz * 0.5, cy = oy + sz * 0.5;
+ const col = fxDef.colors[Math.min(frameIdx, fxDef.colors.length - 1)];
+
+ ctx.save();
+
+ switch (fxDef.shape) {
+ case 'spark': {
+ // Expanding burst of lines
+ const numRays = 6;
+ const maxR = sz * 0.45 * t;
+ ctx.strokeStyle = col;
+ ctx.lineWidth = Math.max(1, 2 * tinv);
+ ctx.globalAlpha = tinv * 0.9 + 0.1;
+ for (let i = 0; i < numRays; i++) {
+ const angle = (i / numRays) * Math.PI * 2;
+ const r0 = maxR * 0.2;
+ ctx.beginPath();
+ ctx.moveTo(cx + Math.cos(angle) * r0, cy + Math.sin(angle) * r0);
+ ctx.lineTo(cx + Math.cos(angle) * maxR, cy + Math.sin(angle) * maxR);
+ ctx.stroke();
+ }
+ // Center flash
+ ctx.fillStyle = fxDef.colors[0];
+ ctx.globalAlpha = tinv * 0.8;
+ ctx.beginPath();
+ ctx.arc(cx, cy, sz * 0.12 * tinv, 0, Math.PI * 2);
+ ctx.fill();
+ break;
+ }
+ case 'arc': {
+ // Growing + fading slash arc
+ const maxR = sz * 0.4;
+ const r = maxR * Math.min(1, t * 2);
+ ctx.strokeStyle = col;
+ ctx.lineWidth = Math.max(1, 4 * tinv);
+ ctx.globalAlpha = tinv;
+ ctx.beginPath();
+ ctx.arc(cx, cy, r, -Math.PI * 0.75, Math.PI * 0.25);
+ ctx.stroke();
+ break;
+ }
+ case 'orb': {
+ // Glowing projectile with trail
+ const posX = ox + sz * 0.1 + sz * 0.8 * t;
+ ctx.globalAlpha = 0.9;
+ // Trail
+ ctx.fillStyle = fxDef.colors[2];
+ ctx.globalAlpha = tinv * 0.5;
+ ctx.fillRect(ox + sz * 0.1, cy - 3, sz * 0.8 * t, 4);
+ // Core
+ ctx.globalAlpha = 0.95;
+ ctx.fillStyle = col;
+ ctx.beginPath();
+ ctx.arc(posX + 4, cy, 4, 0, Math.PI * 2);
+ ctx.fill();
+ break;
+ }
+ case 'sparkle': {
+ // Growing / fading star points
+ const numPts = 4;
+ const maxR = sz * 0.4 * Math.sin(t * Math.PI);
+ ctx.fillStyle = col;
+ ctx.globalAlpha = Math.sin(t * Math.PI);
+ for (let i = 0; i < numPts; i++) {
+ const a = (i / numPts) * Math.PI * 2;
+ const px = cx + Math.cos(a) * maxR;
+ const py = cy + Math.sin(a) * maxR;
+ ctx.beginPath();
+ ctx.arc(px, py, Math.max(1, 2 * Math.sin(t * Math.PI)), 0, Math.PI * 2);
+ ctx.fill();
+ }
+ // Center
+ ctx.fillStyle = fxDef.colors[0];
+ ctx.beginPath();
+ ctx.arc(cx, cy, Math.max(1, 3 * Math.sin(t * Math.PI)), 0, Math.PI * 2);
+ ctx.fill();
+ break;
+ }
+ default:
+ ctx.fillStyle = col;
+ ctx.globalAlpha = tinv;
+ ctx.fillRect(ox + 4, oy + 4, sz - 8, sz - 8);
+ }
+
+ ctx.restore();
+}
+
+// ─── ATLAS FRAME DRAWING ─────────────────────────────────────────────────────
+
+/**
+ * Draw one atlas placeholder frame at (ox, oy) with given w × h dimensions.
+ */
+function drawAtlasFrame(ctx, frame, ox, oy) {
+ const { w, h, color, detail, shape, name } = frame;
+
+ ctx.save();
+
+ switch (shape) {
+ case 'building': {
+ // Main structure
+ ctx.fillStyle = color;
+ ctx.fillRect(ox + 2, oy + h * 0.25, w - 4, h * 0.75 - 2);
+ // Roof
+ ctx.fillStyle = detail;
+ ctx.fillRect(ox + 2, oy + 2, w - 4, h * 0.25);
+ // Windows (2×2 grid)
+ const winColor = detail + 'CC';
+ const cols = 2, rows = 2;
+ const winW = Math.max(4, Math.floor((w - 8) / (cols * 1.5)));
+ const winH = Math.max(4, Math.floor((h * 0.5) / (rows * 1.5)));
+ for (let r = 0; r < rows; r++) {
+ for (let c = 0; c < cols; c++) {
+ const wx = ox + 4 + c * (winW + 4);
+ const wy = oy + h * 0.35 + r * (winH + 4);
+ ctx.fillStyle = winColor;
+ ctx.fillRect(wx, wy, winW, winH);
+ }
+ }
+ break;
+ }
+ case 'tile': {
+ ctx.fillStyle = color;
+ ctx.fillRect(ox, oy, w, h);
+ ctx.strokeStyle = detail;
+ ctx.lineWidth = 1;
+ ctx.strokeRect(ox + 1, oy + 1, w - 2, h - 2);
+ // Cross lines
+ ctx.beginPath();
+ ctx.moveTo(ox + w * 0.5, oy);
+ ctx.lineTo(ox + w * 0.5, oy + h);
+ ctx.moveTo(ox, oy + h * 0.5);
+ ctx.lineTo(ox + w, oy + h * 0.5);
+ ctx.stroke();
+ break;
+ }
+ case 'tree': {
+ // Trunk
+ ctx.fillStyle = detail;
+ ctx.fillRect(ox + w * 0.38, oy + h * 0.6, w * 0.24, h * 0.4);
+ // Canopy (layered circles)
+ ctx.fillStyle = color;
+ ctx.beginPath();
+ ctx.arc(ox + w * 0.5, oy + h * 0.55, w * 0.4, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.beginPath();
+ ctx.arc(ox + w * 0.5, oy + h * 0.38, w * 0.35, 0, Math.PI * 2);
+ ctx.fill();
+ break;
+ }
+ case 'button': {
+ // Background fill
+ ctx.fillStyle = color;
+ ctx.fillRect(ox + 2, oy + 2, w - 4, h - 4);
+ // Border
+ ctx.strokeStyle = detail;
+ ctx.lineWidth = 2;
+ ctx.strokeRect(ox + 2, oy + 2, w - 4, h - 4);
+ // Highlight line
+ ctx.strokeStyle = detail + '88';
+ ctx.lineWidth = 1;
+ ctx.beginPath();
+ ctx.moveTo(ox + 4, oy + 4);
+ ctx.lineTo(ox + w - 4, oy + 4);
+ ctx.stroke();
+ break;
+ }
+ case 'panel': {
+ ctx.fillStyle = color;
+ ctx.fillRect(ox + 1, oy + 1, w - 2, h - 2);
+ ctx.strokeStyle = detail;
+ ctx.lineWidth = 2;
+ ctx.strokeRect(ox + 1, oy + 1, w - 2, h - 2);
+ // Inner border
+ ctx.strokeStyle = detail + '66';
+ ctx.lineWidth = 1;
+ ctx.strokeRect(ox + 4, oy + 4, w - 8, h - 8);
+ break;
+ }
+ case 'bar': {
+ ctx.fillStyle = color;
+ ctx.fillRect(ox, oy, w, h);
+ ctx.fillStyle = detail;
+ ctx.fillRect(ox, oy, w * 0.7, h);
+ ctx.strokeStyle = detail;
+ ctx.lineWidth = 1;
+ ctx.strokeRect(ox + 0.5, oy + 0.5, w - 1, h - 1);
+ break;
+ }
+ case 'icon': {
+ ctx.fillStyle = color;
+ ctx.beginPath();
+ ctx.arc(ox + w * 0.5, oy + h * 0.5, Math.min(w, h) * 0.42, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.strokeStyle = detail;
+ ctx.lineWidth = 1.5;
+ ctx.stroke();
+ break;
+ }
+ case 'cursor': {
+ ctx.fillStyle = color;
+ ctx.beginPath();
+ ctx.moveTo(ox + 4, oy + 4);
+ ctx.lineTo(ox + 4, oy + h - 4);
+ ctx.lineTo(ox + w * 0.5, oy + h * 0.65);
+ ctx.lineTo(ox + w - 4, oy + h - 4);
+ ctx.lineTo(ox + w * 0.6, oy + h * 0.6);
+ ctx.lineTo(ox + w - 4, oy + 4);
+ ctx.closePath();
+ ctx.fill();
+ ctx.strokeStyle = detail;
+ ctx.lineWidth = 1;
+ ctx.stroke();
+ break;
+ }
+ case 'diamond': {
+ ctx.fillStyle = color;
+ ctx.beginPath();
+ ctx.moveTo(ox + w * 0.5, oy + 2);
+ ctx.lineTo(ox + w - 2, oy + h * 0.5);
+ ctx.lineTo(ox + w * 0.5, oy + h - 2);
+ ctx.lineTo(ox + 2, oy + h * 0.5);
+ ctx.closePath();
+ ctx.fill();
+ ctx.strokeStyle = detail;
+ ctx.lineWidth = 1;
+ ctx.stroke();
+ break;
+ }
+ case 'ring': {
+ ctx.strokeStyle = color;
+ ctx.lineWidth = 5;
+ ctx.beginPath();
+ ctx.arc(ox + w * 0.5, oy + h * 0.5, Math.min(w, h) * 0.4, 0, Math.PI * 2);
+ ctx.stroke();
+ ctx.strokeStyle = detail;
+ ctx.lineWidth = 2;
+ ctx.stroke();
+ break;
+ }
+ case 'sword': {
+ // Blade
+ ctx.fillStyle = color;
+ ctx.fillRect(ox + w * 0.38, oy + 2, w * 0.24, h * 0.75);
+ // Guard
+ ctx.fillStyle = detail;
+ ctx.fillRect(ox + 2, oy + h * 0.7, w - 4, h * 0.08);
+ // Handle
+ ctx.fillStyle = '#AA7744';
+ ctx.fillRect(ox + w * 0.38, oy + h * 0.78, w * 0.24, h * 0.2);
+ break;
+ }
+ case 'shield': {
+ ctx.fillStyle = color;
+ ctx.beginPath();
+ ctx.moveTo(ox + w * 0.5, oy + 2);
+ ctx.lineTo(ox + w - 2, oy + h * 0.45);
+ ctx.quadraticCurveTo(ox + w - 2, oy + h - 2, ox + w * 0.5, oy + h - 2);
+ ctx.quadraticCurveTo(ox + 2, oy + h - 2, ox + 2, oy + h * 0.45);
+ ctx.closePath();
+ ctx.fill();
+ ctx.strokeStyle = detail;
+ ctx.lineWidth = 1.5;
+ ctx.stroke();
+ break;
+ }
+ case 'potion': {
+ // Bottle body
+ ctx.fillStyle = color + 'CC';
+ ctx.beginPath();
+ ctx.arc(ox + w * 0.5, oy + h * 0.65, w * 0.38, 0, Math.PI * 2);
+ ctx.fill();
+ // Neck
+ ctx.fillStyle = detail + '99';
+ ctx.fillRect(ox + w * 0.3, oy + h * 0.1, w * 0.4, h * 0.3);
+ // Cork
+ ctx.fillStyle = '#AA8844';
+ ctx.fillRect(ox + w * 0.3, oy + 2, w * 0.4, h * 0.12);
+ // Shine
+ ctx.fillStyle = 'rgba(255,255,255,0.4)';
+ ctx.beginPath();
+ ctx.arc(ox + w * 0.38, oy + h * 0.55, w * 0.12, 0, Math.PI * 2);
+ ctx.fill();
+ break;
+ }
+ case 'circle': {
+ ctx.fillStyle = color;
+ ctx.beginPath();
+ ctx.arc(ox + w * 0.5, oy + h * 0.5, Math.min(w, h) * 0.45, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.strokeStyle = detail;
+ ctx.lineWidth = 1.5;
+ ctx.stroke();
+ break;
+ }
+ default: { // 'rect' and fallback
+ ctx.fillStyle = color;
+ ctx.fillRect(ox + 1, oy + 1, w - 2, h - 2);
+ ctx.strokeStyle = detail;
+ ctx.lineWidth = 1;
+ ctx.strokeRect(ox + 1, oy + 1, w - 2, h - 2);
+ break;
+ }
+ }
+
+ ctx.restore();
+}
+
+// ─── SPRITESHEET GENERATION ──────────────────────────────────────────────────
+
+/**
+ * Generate one spritesheet PNG for a given character + animation combo.
+ * Returns { key, path, frameWidth, frameHeight, cols, rows }
+ */
+function generateCharSheet(charId, char, animName, numFrames, outputDir) {
+ const sz = char.frameSize;
+ const cols = numFrames;
+ const rows = DIR_ROWS.length;
+ const canvas = createCanvas(cols * sz, rows * sz);
+ const ctx = canvas.getContext('2d');
+
+ // Transparent background (default for canvas)
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ // Optional: light checkerboard to confirm transparency visually (skip for real output)
+
+ for (let rowIdx = 0; rowIdx < rows; rowIdx++) {
+ const dir = DIR_ROWS[rowIdx];
+ for (let frameIdx = 0; frameIdx < numFrames; frameIdx++) {
+ const ox = frameIdx * sz;
+ const oy = rowIdx * sz;
+ drawCharFrame(ctx, char, dir, animName, frameIdx, ox, oy, sz);
+ }
+ }
+
+ const key = `${charId}_${animName}`;
+ const fileName = `${key}.png`;
+ const filePath = path.join(outputDir, fileName);
+ savePNG(canvas, filePath);
+
+ return {
+ key,
+ path: `assets/spritesheets/${char.world}/${fileName}`,
+ frameWidth: sz,
+ frameHeight: sz,
+ };
+}
+
+/**
+ * Generate one FX spritesheet (single row, 8 frames, 32×32 each).
+ */
+function generateFxSheet(fxId, fxDef, outputDir) {
+ const sz = 32;
+ const canvas = createCanvas(FX_FRAMES * sz, sz);
+ const ctx = canvas.getContext('2d');
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+ for (let f = 0; f < FX_FRAMES; f++) {
+ drawFxFrame(ctx, fxDef, f, f * sz, 0);
+ }
+
+ const fileName = `${fxId}.png`;
+ const filePath = path.join(outputDir, fileName);
+ savePNG(canvas, filePath);
+
+ return {
+ key: fxId,
+ path: `assets/spritesheets/gw/${fileName}`,
+ frameWidth: sz,
+ frameHeight: sz,
+ };
+}
+
+// ─── ATLAS GENERATION ────────────────────────────────────────────────────────
+
+const ATLAS_PADDING = 2;
+
+/**
+ * Simple greedy row packer.
+ * Returns array of { name, w, h, x, y } and atlas { width, height }.
+ */
+function packFrames(frames, maxWidth) {
+ let x = ATLAS_PADDING, y = ATLAS_PADDING, rowH = 0;
+ const packed = [];
+
+ for (const f of frames) {
+ if (x + f.w + ATLAS_PADDING > maxWidth && x > ATLAS_PADDING) {
+ x = ATLAS_PADDING;
+ y += rowH + ATLAS_PADDING;
+ rowH = 0;
+ }
+ packed.push({ ...f, x, y });
+ x += f.w + ATLAS_PADDING;
+ rowH = Math.max(rowH, f.h);
+ }
+
+ const totalH = y + rowH + ATLAS_PADDING;
+ return {
+ frames: packed,
+ atlasWidth: nextPow2(maxWidth),
+ atlasHeight: nextPow2(totalH),
+ };
+}
+
+/**
+ * Build Phaser-compatible TexturePacker "hash" JSON for an atlas.
+ */
+function buildAtlasJson(packedFrames, atlasWidth, atlasHeight, imageName) {
+ const frames = {};
+ for (const f of packedFrames) {
+ frames[f.name] = {
+ frame: { x: f.x, y: f.y, w: f.w, h: f.h },
+ rotated: false,
+ trimmed: false,
+ spriteSourceSize:{ x: 0, y: 0, w: f.w, h: f.h },
+ sourceSize: { w: f.w, h: f.h },
+ };
+ }
+ return {
+ frames,
+ meta: {
+ app: 'asset-generator',
+ version: '1.0',
+ image: imageName,
+ format: 'RGBA8888',
+ size: { w: atlasWidth, h: atlasHeight },
+ scale: '1',
+ },
+ };
+}
+
+/**
+ * Generate one atlas (PNG + JSON) and return the manifest entry.
+ */
+function generateAtlas(atlasId, framesDefs, outputDir, relDir) {
+ const maxWidth = 512;
+ const { frames: packed, atlasWidth, atlasHeight } = packFrames(framesDefs, maxWidth);
+
+ const canvas = createCanvas(atlasWidth, atlasHeight);
+ const ctx = canvas.getContext('2d');
+ ctx.clearRect(0, 0, atlasWidth, atlasHeight);
+
+ for (const f of packed) {
+ drawAtlasFrame(ctx, f, f.x, f.y);
+ }
+
+ const pngFile = `${atlasId}.png`;
+ const jsonFile = `${atlasId}.json`;
+ const pngPath = path.join(outputDir, pngFile);
+ const jsonPath = path.join(outputDir, jsonFile);
+
+ savePNG(canvas, pngPath);
+
+ const json = buildAtlasJson(packed, atlasWidth, atlasHeight, pngFile);
+ fs.writeFileSync(jsonPath, JSON.stringify(json, null, 2));
+ console.log(' wrote', path.relative(path.join(__dirname, '..'), jsonPath));
+
+ return {
+ key: atlasId,
+ png: `assets/atlas/${relDir}/${pngFile}`,
+ json: `assets/atlas/${relDir}/${jsonFile}`,
+ };
+}
+
+// ─── ASSET MANIFEST ──────────────────────────────────────────────────────────
+
+function buildManifest(spritesheets, atlases) {
+ return { spritesheets, atlases };
+}
+
+// ─── MAIN ────────────────────────────────────────────────────────────────────
+
+function main() {
+ console.log('=== That One Game – Asset Generator ===\n');
+
+ ensureDir(SS_RW);
+ ensureDir(SS_GW);
+ ensureDir(ATLAS_RW);
+ ensureDir(ATLAS_GW);
+
+ const spritesheets = [];
+
+ // ── Character sprite-sheets ──────────────────────────────────────────────
+ console.log('Generating character sprite-sheets…');
+ for (const [charId, char] of Object.entries(CHARS)) {
+ const outputDir = char.world === 'rw' ? SS_RW : SS_GW;
+ for (const [animName, numFrames] of Object.entries(ANIMS)) {
+ const entry = generateCharSheet(charId, char, animName, numFrames, outputDir);
+ spritesheets.push(entry);
+ }
+ }
+
+ // ── FX sprite-sheets ─────────────────────────────────────────────────────
+ console.log('\nGenerating FX sprite-sheets…');
+ for (const [fxId, fxDef] of Object.entries(FX_DEFS)) {
+ const entry = generateFxSheet(fxId, fxDef, SS_GW);
+ spritesheets.push(entry);
+ }
+
+ // ── Atlases ──────────────────────────────────────────────────────────────
+ console.log('\nGenerating atlases…');
+ const atlases = [];
+ atlases.push(generateAtlas('atlas_rw_world', ATLAS_RW_WORLD_FRAMES, ATLAS_RW, 'rw'));
+ atlases.push(generateAtlas('atlas_rw_ui', ATLAS_RW_UI_FRAMES, ATLAS_RW, 'rw'));
+ atlases.push(generateAtlas('atlas_gw_world', ATLAS_GW_WORLD_FRAMES, ATLAS_GW, 'gw'));
+ atlases.push(generateAtlas('atlas_gw_ui', ATLAS_GW_UI_FRAMES, ATLAS_GW, 'gw'));
+
+ // ── Asset manifest ───────────────────────────────────────────────────────
+ const manifest = buildManifest(spritesheets, atlases);
+ const manifestPath = path.join(ASSETS_DIR, 'asset-manifest.json');
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
+ console.log('\n wrote', path.relative(path.join(__dirname, '..'), manifestPath));
+
+ console.log('\n✓ Done – generated', spritesheets.length, 'sprite-sheets and', atlases.length, 'atlases.');
+}
+
+main();