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();