From a9e999b49e33a3dec7d7a1611d3c59377347fb24 Mon Sep 17 00:00:00 2001 From: Halica Date: Tue, 29 Sep 2026 18:55:29 +0200 Subject: [PATCH] room overhaul more editor changes, new rooms --- light_and_dark/README.md | 2 +- light_and_dark/data/objects/objects.json | 3 + light_and_dark/data/world/rooms/room_01.json | 322 ++++++------ light_and_dark/data/world/rooms/room_02.json | 434 ++++++++-------- light_and_dark/data/world/rooms/room_03.json | 135 +++++ light_and_dark/data/world/rooms/room_07.json | 468 ------------------ .../data/world/rooms/room_boss_01.json | 81 --- .../world/rooms/room_boss_refight_01.json | 48 -- .../data/world/rooms/room_reward_01.json | 132 ----- .../world/rooms/room_town_gloomreach.json | 143 ------ light_and_dark/level_editor_v43.html | 353 +++++++++++-- light_and_dark/main.py | 232 ++++++++- light_and_dark/saves/new rooms.json | 39 ++ light_and_dark/saves/save 1.json | 6 +- 14 files changed, 1110 insertions(+), 1288 deletions(-) create mode 100644 light_and_dark/data/world/rooms/room_03.json delete mode 100644 light_and_dark/data/world/rooms/room_07.json delete mode 100644 light_and_dark/data/world/rooms/room_boss_01.json delete mode 100644 light_and_dark/data/world/rooms/room_boss_refight_01.json delete mode 100644 light_and_dark/data/world/rooms/room_reward_01.json delete mode 100644 light_and_dark/data/world/rooms/room_town_gloomreach.json create mode 100644 light_and_dark/saves/new rooms.json diff --git a/light_and_dark/README.md b/light_and_dark/README.md index 5ddcc8c..78dcdda 100644 --- a/light_and_dark/README.md +++ b/light_and_dark/README.md @@ -51,7 +51,7 @@ Requires Python 3.10+ and `pygame`. Game data (rooms, items, sets, enemies, drop ## Saving & lanterns -**Lanterns are save points.** Walk up to a lantern and press **F** to save — a "Press F - Save at lantern" prompt appears when you're in range. Your respawn point is set to that lantern, so dying (or reloading the save) puts you back at your latest lantern. Saving via a lantern in a *different room* than where you last died also respawns butterfly jars in the world. +**Lanterns are save points.** Walk up to a lantern and press **F** to save — a "Press F - Save at lantern" prompt appears when you're in range. Saving **fully restores your health and Charge** (and clears poison), and sets your respawn point to that lantern, so dying (or reloading the save) puts you back at your latest lantern. Saving via a lantern in a *different room* than where you last died also respawns butterfly jars in the world. You can also save anytime from the pause menu (**Esc → Save Game**). diff --git a/light_and_dark/data/objects/objects.json b/light_and_dark/data/objects/objects.json index ab2224c..ddbb1d5 100644 --- a/light_and_dark/data/objects/objects.json +++ b/light_and_dark/data/objects/objects.json @@ -19,6 +19,9 @@ }, "player": { "type": "player" + }, + "quest_point": { + "type": "quest_giver" } } } \ No newline at end of file diff --git a/light_and_dark/data/world/rooms/room_01.json b/light_and_dark/data/world/rooms/room_01.json index f3e0a0f..6d5e38e 100644 --- a/light_and_dark/data/world/rooms/room_01.json +++ b/light_and_dark/data/world/rooms/room_01.json @@ -1,158 +1,202 @@ { + "name": "room_01", + "display_name": "room_01", "meta": { "id": "room_01", - "name": "Room 01 - Tutorial Steps" + "name": "room_01" }, + "bounds": {}, "player_spawn": [ - -260, - 500 + 100, + 180 ], - "bounds": { - "kill_y": 2000 - }, - "platforms": { - "ground": [ - [ - -500, - 640, - 2060, - 460 - ], - [ - -500, - 560, - 520, - 80 - ], - [ - 1560, - 220, - 120, - 880 - ], - [ - -1100, - -520, - 600, - 1620 - ], - [ - 1680, - -100, - 920, - 1200 - ], - [ - -500, - -520, - 940, - 840 - ], - [ - 440, - -520, - 2160, - 420 - ] - ], - "floating": [ - [ - 800, - 220, - 320, - 40 - ], - [ - 880, - 520, - 240, - 40 - ], - [ - 500, - 400, - 320, - 40 - ], - [ - 440, - 280, - 200, - 40 - ], - [ - 1220, - 220, - 340, - 40 - ] + "entry_spawns": { + "room1_right": [ + 1040, + 680 ] }, - "enemies": [], - "exits": [ + "platforms": [ { - "rect": [ - -420, - 480, - 90, - 80 - ], - "to": "room_reward_01", - "spawn": [ - 120, - 300 - ], - "hint": "Reward" + "x": 20, + "y": 260, + "w": 1000, + "h": 20, + "group": "ground", + "layer": 0 }, { - "rect": [ - 1560, - 100, - 120, - 120 - ], + "x": 0, + "y": 0, + "w": 20, + "h": 780, + "group": "ground", + "layer": 0 + }, + { + "x": 20, + "y": 0, + "w": 1580, + "h": 20, + "group": "ground", + "layer": 0 + }, + { + "x": 540, + "y": 140, + "w": 160, + "h": 20, + "group": "ground", + "layer": 0 + }, + { + "x": 900, + "y": 400, + "w": 700, + "h": 100, + "group": "ground", + "layer": 0 + }, + { + "x": 1580, + "y": 20, + "w": 20, + "h": 380, + "group": "ground", + "layer": 0 + }, + { + "x": 1120, + "y": 280, + "w": 40, + "h": 20, + "group": "ground", + "layer": 0 + }, + { + "x": 840, + "y": 460, + "w": 60, + "h": 20, + "group": "ground", + "layer": 0 + }, + { + "x": 600, + "y": 400, + "w": 140, + "h": 100, + "group": "ground", + "layer": 0 + }, + { + "x": 380, + "y": 400, + "w": 140, + "h": 100, + "group": "ground", + "layer": 0 + }, + { + "x": 160, + "y": 400, + "w": 140, + "h": 100, + "group": "ground", + "layer": 0 + }, + { + "x": 160, + "y": 620, + "w": 760, + "h": 20, + "group": "ground", + "layer": 0 + }, + { + "x": 160, + "y": 500, + "w": 20, + "h": 120, + "group": "ground", + "layer": 0 + }, + { + "x": 900, + "y": 500, + "w": 20, + "h": 120, + "group": "ground", + "layer": 0 + }, + { + "x": 800, + "y": 520, + "w": 60, + "h": 20, + "group": "ground", + "layer": 0 + }, + { + "x": 20, + "y": 760, + "w": 1140, + "h": 20, + "group": "ground", + "layer": 0 + }, + { + "x": 1140, + "y": 500, + "w": 20, + "h": 260, + "group": "ground", + "layer": 0 + } + ], + "enemies": [ + { + "x": 1480, + "y": 360, + "type": "enemy", + "layer": 0 + }, + { + "x": 340, + "y": 580, + "type": "enemy", + "layer": 0 + } + ], + "npcs": [], + "props": [ + { + "x": 700, + "y": 760, + "type": "butterfly_jar", + "radius": 180, + "object_id": "flow_butterfly_jar", + "layer": 0, + "quest_id": "" + } + ], + "exits": [ + { + "x": 1060, + "y": 660, + "w": 80, + "h": 100, "to": "room_02", + "spawn_id": "room2_left", "spawn": [ - -420, - 300 + 120, + 240 ], "hint": "", - "spawn_id": "entry_1" + "layer": 0 } ], "ability_doors": [], - "entry_spawns": {}, - "props": [ - { - "type": "light_source", - "pos": [ - 0, - 560 - ], - "radius": 180 - }, - { - "type": "save_point", - "pos": [ - -60, - 560 - ], - "radius": 110 - }, - { - "type": "butterfly_jar", - "pos": [ - 320, - 560 - ] - }, - { - "type": "quest_giver", - "quest_id": "quest_first_kills", - "pos": [ - 300, - 600 - ] - } - ] + "room_w": 1600, + "room_h": 900 } \ No newline at end of file diff --git a/light_and_dark/data/world/rooms/room_02.json b/light_and_dark/data/world/rooms/room_02.json index 8d2cbc7..a4d830c 100644 --- a/light_and_dark/data/world/rooms/room_02.json +++ b/light_and_dark/data/world/rooms/room_02.json @@ -1,238 +1,258 @@ { + "name": "room_02", + "display_name": "room_02", "meta": { "id": "room_02", - "name": "Room 02 - First Enemies" + "name": "room_02" }, + "bounds": {}, "player_spawn": [ - -320, - 300 + 820, + 440 ], - "bounds": { - "kill_y": 2000 - }, - "platforms": { - "ground": [ - [ - -500, - 360, - 2440, - 660 - ], - [ - -760, - -580, - 260, - 1600 - ], - [ - 1940, - -580, - 240, - 1600 - ], - [ - -760, - -820, - 2940, - 240 - ] + "entry_spawns": { + "room2_bottom_left": [ + 120, + 460 ], - "floating": [ - [ - 260, - 260, - 140, - 18 - ], - [ - 520, - 220, - 140, - 18 - ], - [ - 760, - 180, - 140, - 18 - ], - [ - 940, - 120, - 170, - 18 - ], - [ - 1200, - 160, - 170, - 18 - ], - [ - 1460, - 100, - 170, - 18 - ], - [ - 1200, - -20, - 280, - 40 - ], - [ - 40, - -140, - 1100, - 60 - ], - [ - -500, - -140, - 460, - 60 - ], - [ - -500, - -260, - 260, - 40 - ], - [ - -500, - -380, - 160, - 40 - ], - [ - -320, - -500, - 1380, - 40 - ], - [ - 1140, - -440, - 340, - 40 - ], - [ - 1560, - -380, - 380, - 40 - ] + "room2_top_left": [ + 120, + 240 + ], + "hazard_respawn_1": [ + 840, + 440 ] }, + "platforms": [ + { + "x": 0, + "y": 320, + "w": 1040, + "h": 20, + "group": "ground", + "layer": 0 + }, + { + "x": 0, + "y": 120, + "w": 20, + "h": 200, + "group": "ground", + "layer": 0 + }, + { + "x": 420, + "y": 220, + "w": 220, + "h": 20, + "group": "ground", + "layer": 0 + }, + { + "x": 20, + "y": 120, + "w": 1240, + "h": 20, + "group": "ground", + "layer": 0 + }, + { + "x": 1240, + "y": 140, + "w": 20, + "h": 60, + "group": "ground", + "layer": 0 + }, + { + "x": 1060, + "y": 360, + "w": 180, + "h": 20, + "group": "ground", + "layer": 0 + }, + { + "x": 1260, + "y": 320, + "w": 140, + "h": 20, + "group": "ground", + "layer": 0 + }, + { + "x": 1260, + "y": 180, + "w": 260, + "h": 20, + "group": "ground", + "layer": 0 + }, + { + "x": 1500, + "y": 200, + "w": 20, + "h": 440, + "group": "ground", + "layer": 0 + }, + { + "x": 860, + "y": 620, + "w": 640, + "h": 20, + "group": "ground", + "layer": 0 + }, + { + "x": 800, + "y": 500, + "w": 60, + "h": 140, + "group": "ground", + "layer": 0 + }, + { + "x": 1400, + "y": 500, + "w": 100, + "h": 20, + "group": "ground", + "layer": 0 + }, + { + "x": 1380, + "y": 380, + "w": 40, + "h": 40, + "group": "ground", + "layer": 0 + }, + { + "x": 380, + "y": 620, + "w": 420, + "h": 20, + "group": "ground", + "layer": 0 + }, + { + "x": 260, + "y": 580, + "w": 120, + "h": 60, + "group": "ground", + "layer": 0 + }, + { + "x": 640, + "y": 500, + "w": 80, + "h": 20, + "group": "ground", + "layer": 0 + }, + { + "x": 480, + "y": 500, + "w": 80, + "h": 20, + "group": "ground", + "layer": 0 + }, + { + "x": 20, + "y": 540, + "w": 220, + "h": 20, + "group": "ground", + "layer": 0 + }, + { + "x": 0, + "y": 340, + "w": 20, + "h": 220, + "group": "ground", + "layer": 0 + }, + { + "x": 240, + "y": 560, + "w": 20, + "h": 20, + "group": "ground", + "layer": 0 + }, + { + "x": 380, + "y": 600, + "w": 420, + "h": 20, + "group": "hazard", + "layer": 0, + "respawn": [ + 840, + 440 + ] + } + ], "enemies": [ { + "x": 1160, + "y": 320, "type": "enemy", - "pos": [ - 240, - -200 - ] + "layer": 0 }, { + "x": 1160, + "y": 580, "type": "enemy", - "pos": [ - 620, - 140 - ] - }, + "layer": 0 + } + ], + "npcs": [], + "props": [ { - "type": "enemy", - "pos": [ - 860, - -200 - ] - }, - { - "type": "enemy", - "pos": [ - 480, - -530 - ] - }, - { - "type": "enemy", - "pos": [ - 1740, - 280 - ] + "x": 720, + "y": 320, + "type": "save_point", + "radius": 90, + "object_id": "lamp" } ], "exits": [ { - "rect": [ - -500, - 300, - 80, - 60 - ], + "x": 20, + "y": 220, + "w": 80, + "h": 100, "to": "room_01", + "spawn_id": "room1_right", "spawn": [ - 1560, - 140 + 1040, + 680 ], - "hint": "Back" + "hint": "", + "layer": 0 }, { - "rect": [ - 1860, - -500, - 80, - 120 - ], - "to": "room_07", + "x": 20, + "y": 440, + "w": 80, + "h": 100, + "to": "room_03", + "spawn_id": "room3_top_right", "spawn": [ - 180, - 240 + 1480, + 220 ], - "hint": "" + "hint": "", + "layer": 0 } ], "ability_doors": [], - "props": [ - { - "type": "save_point", - "pos": [ - -300, - 360 - ], - "radius": 110 - }, - { - "type": "butterfly_jar", - "pos": [ - 800, - 360 - ] - }, - { - "type": "light_source", - "pos": [ - 300, - 360 - ], - "radius": 180 - }, - { - "type": "light_source", - "pos": [ - 1100, - 360 - ], - "radius": 180 - } - ], - "entry_spawns": { - "entry_1": [ - -420, - 300 - ], - "entry_3": [ - 1840, - -440 - ] - } + "room_w": 1600, + "room_h": 900 } \ No newline at end of file diff --git a/light_and_dark/data/world/rooms/room_03.json b/light_and_dark/data/world/rooms/room_03.json new file mode 100644 index 0000000..51f8eec --- /dev/null +++ b/light_and_dark/data/world/rooms/room_03.json @@ -0,0 +1,135 @@ +{ + "name": "room_03", + "display_name": "room_03", + "meta": { + "id": "room_03", + "name": "room_03" + }, + "bounds": {}, + "player_spawn": [ + 1460, + 220 + ], + "entry_spawns": { + "room3_top_right": [ + 1480, + 220 + ] + }, + "platforms": [ + { + "x": 1080, + "y": 300, + "w": 520, + "h": 20, + "group": "ground", + "layer": 0 + }, + { + "x": 1580, + "y": 60, + "w": 20, + "h": 240, + "group": "ground", + "layer": 0 + }, + { + "x": 20, + "y": 40, + "w": 1580, + "h": 20, + "group": "ground", + "layer": 0 + }, + { + "x": 1080, + "y": 320, + "w": 20, + "h": 140, + "group": "ground", + "layer": 0 + }, + { + "x": 20, + "y": 620, + "w": 1080, + "h": 20, + "group": "ground", + "layer": 0 + }, + { + "x": 20, + "y": 60, + "w": 20, + "h": 560, + "group": "ground", + "layer": 0 + }, + { + "x": 620, + "y": 520, + "w": 80, + "h": 20, + "group": "moving", + "layer": 0, + "path": [ + [ + 620, + 520 + ], + [ + 620, + 340 + ] + ] + }, + { + "x": 1080, + "y": 460, + "w": 20, + "h": 160, + "group": "trigger" + } + ], + "enemies": [], + "npcs": [], + "props": [ + { + "x": 1300, + "y": 220, + "type": "player", + "radius": 180, + "object_id": "player", + "layer": 0, + "quest_id": "" + }, + { + "x": 960, + "y": 540, + "type": "quest_giver", + "radius": 180, + "object_id": "quest_point", + "layer": 0, + "quest_id": "first_kills" + } + ], + "exits": [ + { + "x": 1500, + "y": 200, + "w": 80, + "h": 100, + "to": "room_02", + "spawn_id": "room2_bottom_left", + "spawn": [ + 120, + 460 + ], + "hint": "", + "layer": 0 + } + ], + "ability_doors": [], + "room_w": 1600, + "room_h": 900 +} \ No newline at end of file diff --git a/light_and_dark/data/world/rooms/room_07.json b/light_and_dark/data/world/rooms/room_07.json deleted file mode 100644 index 52c3fdc..0000000 --- a/light_and_dark/data/world/rooms/room_07.json +++ /dev/null @@ -1,468 +0,0 @@ -{ - "name": "room_07", - "display_name": "room_07", - "meta": { - "id": "room_07", - "name": "room_07" - }, - "bounds": { - "kill_y": 3200 - }, - "player_spawn": [ - 200, - 220 - ], - "entry_spawns": { - "entry_1": [ - 180, - 240 - ], - "entry_2": [ - 4200, - 2720 - ], - "entry_3": [ - -320, - 2720 - ] - }, - "platforms": [ - { - "x": 60, - "y": 320, - "w": 1460, - "h": 40, - "group": "ground" - }, - { - "x": -1160, - "y": 2780, - "w": 5500, - "h": 120, - "group": "ground" - }, - { - "x": -1160, - "y": -300, - "w": 1220, - "h": 2480, - "group": "ground" - }, - { - "x": -1280, - "y": -300, - "w": 120, - "h": 3200, - "group": "ground" - }, - { - "x": 2300, - "y": 1860, - "w": 2040, - "h": 320, - "group": "ground" - }, - { - "x": 4340, - "y": -300, - "w": 100, - "h": 3200, - "group": "ground" - }, - { - "x": 60, - "y": -300, - "w": 4280, - "h": 80, - "group": "ground" - }, - { - "x": 60, - "y": 1860, - "w": 1600, - "h": 320, - "group": "ground" - }, - { - "x": 1600, - "y": 440, - "w": 1060, - "h": 40, - "group": "ground" - }, - { - "x": 2780, - "y": 320, - "w": 580, - "h": 40, - "group": "ground" - }, - { - "x": 3280, - "y": 800, - "w": 1060, - "h": 60, - "group": "ground" - }, - { - "x": 1660, - "y": 1920, - "w": 360, - "h": 20, - "group": "ground" - }, - { - "x": 1940, - "y": 2040, - "w": 360, - "h": 20, - "group": "ground" - }, - { - "x": 1660, - "y": 2160, - "w": 360, - "h": 20, - "group": "ground" - }, - { - "x": 1940, - "y": 2280, - "w": 360, - "h": 20, - "group": "ground" - }, - { - "x": 1660, - "y": 2400, - "w": 360, - "h": 20, - "group": "ground" - }, - { - "x": 1940, - "y": 2520, - "w": 360, - "h": 20, - "group": "ground" - }, - { - "x": 1660, - "y": 2640, - "w": 360, - "h": 20, - "group": "ground" - }, - { - "x": 2660, - "y": 880, - "w": 520, - "h": 40, - "group": "ground" - }, - { - "x": 2140, - "y": 880, - "w": 380, - "h": 40, - "group": "ground" - }, - { - "x": 1940, - "y": 880, - "w": 120, - "h": 40, - "group": "ground" - }, - { - "x": 1680, - "y": 880, - "w": 100, - "h": 40, - "group": "ground" - }, - { - "x": 1440, - "y": 880, - "w": 80, - "h": 40, - "group": "ground" - }, - { - "x": 1220, - "y": 880, - "w": 80, - "h": 40, - "group": "ground" - }, - { - "x": 940, - "y": 880, - "w": 180, - "h": 40, - "group": "ground" - }, - { - "x": 660, - "y": 820, - "w": 180, - "h": 40, - "group": "ground" - }, - { - "x": 300, - "y": 740, - "w": 260, - "h": 60, - "group": "ground" - }, - { - "x": 260, - "y": 640, - "w": 40, - "h": 1100, - "group": "ground" - }, - { - "x": 300, - "y": 1260, - "w": 4040, - "h": 40, - "group": "ground" - }, - { - "x": 3240, - "y": 980, - "w": 60, - "h": 40, - "group": "ground" - }, - { - "x": 3100, - "y": 1060, - "w": 60, - "h": 40, - "group": "ground" - }, - { - "x": 3240, - "y": 1140, - "w": 60, - "h": 40, - "group": "ground" - }, - { - "x": 3380, - "y": 440, - "w": 300, - "h": 40, - "group": "ground" - }, - { - "x": 3700, - "y": 560, - "w": 300, - "h": 40, - "group": "ground" - }, - { - "x": 4020, - "y": 680, - "w": 320, - "h": 40, - "group": "ground" - }, - { - "x": 2100, - "y": 2720, - "w": 60, - "h": 20 - } - ], - "enemies": [ - { - "x": 2080, - "y": 380, - "type": "enemy" - }, - { - "x": 280, - "y": 2680, - "type": "miniboss" - }, - { - "x": 520, - "y": 2700, - "type": "enemy" - }, - { - "x": 680, - "y": 2700, - "type": "enemy" - }, - { - "x": 980, - "y": 2700, - "type": "enemy" - }, - { - "x": 840, - "y": 2700, - "type": "enemy" - }, - { - "x": 820, - "y": 1160, - "type": "enemy" - }, - { - "x": 1040, - "y": 1160, - "type": "enemy" - }, - { - "x": 1340, - "y": 1160, - "type": "enemy" - }, - { - "x": 1700, - "y": 1160, - "type": "enemy" - }, - { - "x": 2140, - "y": 1160, - "type": "enemy" - }, - { - "x": 2480, - "y": 1160, - "type": "enemy" - }, - { - "x": 2700, - "y": 1160, - "type": "enemy" - }, - { - "x": 2740, - "y": 1720, - "type": "miniboss" - } - ], - "npcs": [], - "props": [ - { - "x": 200, - "y": 320, - "type": "save_point", - "radius": 110, - "object_id": null - }, - { - "x": 820, - "y": 320, - "type": "butterfly_jar", - "radius": 180, - "object_id": null - }, - { - "x": 500, - "y": 320, - "type": "light_source", - "radius": 180, - "object_id": null - }, - { - "x": 900, - "y": 2780, - "type": "light_source", - "radius": 180, - "object_id": null - }, - { - "x": 2600, - "y": 1860, - "type": "light_source", - "radius": 180, - "object_id": null - } - ], - "exits": [ - { - "x": -1160, - "y": 2640, - "w": 100, - "h": 140, - "to": "room_town_gloomreach", - "spawn_id": "", - "spawn": [ - 120, - 300 - ], - "hint": "" - }, - { - "x": -420, - "y": 2720, - "w": 80, - "h": 60, - "to": "room_boss_01", - "spawn_id": "entry_2", - "spawn": [ - -360, - 300 - ], - "hint": "" - }, - { - "x": 4240, - "y": 2700, - "w": 100, - "h": 80, - "to": "room_reward_01", - "spawn_id": "", - "spawn": [ - 1760, - 300 - ], - "hint": "" - }, - { - "x": 60, - "y": 180, - "w": 100, - "h": 140, - "to": "room_02", - "spawn_id": "", - "spawn": [ - 1840, - -440 - ], - "hint": "" - } - ], - "ability_doors": [ - { - "x": -980, - "y": 2180, - "w": 80, - "h": 600 - }, - { - "x": 4100, - "y": 2180, - "w": 40, - "h": 600 - }, - { - "x": 1940, - "y": 2720, - "w": 360, - "h": 20 - } - ], - "room_w": 1600, - "room_h": 900 -} \ No newline at end of file diff --git a/light_and_dark/data/world/rooms/room_boss_01.json b/light_and_dark/data/world/rooms/room_boss_01.json deleted file mode 100644 index 1ca42cf..0000000 --- a/light_and_dark/data/world/rooms/room_boss_01.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "meta": { - "id": "room_boss_01", - "name": "room_boss_01" - }, - "player_spawn": [ - -240, - 300 - ], - "bounds": { - "kill_y": 1400 - }, - "platforms": { - "ground": [ - [ - -500, - 360, - 1500, - 240 - ] - ] - }, - "enemies": [ - { - "type": "boss", - "pos": [ - 280, - 200 - ] - } - ], - "exits": [ - { - "rect": [ - -460, - 300, - 80, - 60 - ], - "to": "room_07", - "spawn": [ - -320, - 2720 - ], - "hint": "Retreat", - "spawn_id": "entry_3" - }, - { - "rect": [ - 880, - 260, - 120, - 100 - ], - "to": "room_reward_01", - "spawn": [ - 100, - 300 - ], - "hint": "Reward", - "spawn_id": "entry_2" - } - ], - "ability_doors": [], - "props": [ - { - "type": "light_source", - "pos": [ - -100, - 360 - ], - "radius": 180 - } - ], - "entry_spawns": { - "entry_2": [ - -360, - 300 - ] - } -} \ No newline at end of file diff --git a/light_and_dark/data/world/rooms/room_boss_refight_01.json b/light_and_dark/data/world/rooms/room_boss_refight_01.json deleted file mode 100644 index 75e2524..0000000 --- a/light_and_dark/data/world/rooms/room_boss_refight_01.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "meta": { - "id": "room_boss_refight_01", - "name": "room_boss_refight_01" - }, - "player_spawn": [ - -240, - 300 - ], - "bounds": { - "kill_y": 1400 - }, - "platforms": { - "ground": [ - [ - -500, - 360, - 1500, - 240 - ] - ] - }, - "enemies": [ - { - "type": "boss", - "pos": [ - 280, - 200 - ] - } - ], - "exits": [ - { - "rect": [ - -460, - 300, - 80, - 60 - ], - "to": "room_town_gloomreach", - "spawn_id": "entry_diary", - "hint": "Leave", - "requires_boss_dead": true - } - ], - "ability_doors": [], - "entry_spawns": {} -} \ No newline at end of file diff --git a/light_and_dark/data/world/rooms/room_reward_01.json b/light_and_dark/data/world/rooms/room_reward_01.json deleted file mode 100644 index 3fd684b..0000000 --- a/light_and_dark/data/world/rooms/room_reward_01.json +++ /dev/null @@ -1,132 +0,0 @@ -{ - "meta": { - "id": "room_reward_01", - "name": "room_reward_01" - }, - "player_spawn": [ - 140, - 300 - ], - "bounds": { - "kill_y": 2000 - }, - "platforms": { - "ground": [ - [ - -500, - 360, - 2400, - 240 - ] - ], - "floating": [ - [ - 650, - 260, - 220, - 18 - ], - [ - 1100, - 260, - 220, - 18 - ] - ] - }, - "enemies": [], - "exits": [ - { - "rect": [ - 0, - 300, - 80, - 60 - ], - "to": "room_boss_01", - "spawn_id": "entry_2", - "spawn": [ - 110, - 260 - ], - "hint": "Back" - }, - { - "rect": [ - 1840, - 300, - 60, - 60 - ], - "to": "room_07", - "spawn_id": "entry_2", - "spawn": [ - 4200, - 2720 - ], - "hint": "Forward" - } - ], - "ability_doors": [ - { - "rect": [ - 1700, - 60, - 60, - 300 - ] - }, - { - "rect": [ - 1580, - 60, - 40, - 300 - ] - } - ], - "entry_spawns": { - "entry_2": [ - 100, - 300 - ], - "entry_1": [ - 1800, - 300 - ] - }, - "props": [ - { - "type": "save_point", - "pos": [ - 140, - 360 - ], - "radius": 110 - }, - { - "type": "light_source", - "pos": [ - 1100, - 360 - ], - "radius": 180 - }, - { - "type": "reward_chest", - "pos": [ - 520, - 330 - ], - "radius": 180 - }, - { - "type": "ability_pickup", - "pos": [ - 820, - 210 - ], - "radius": 180 - } - ] -} diff --git a/light_and_dark/data/world/rooms/room_town_gloomreach.json b/light_and_dark/data/world/rooms/room_town_gloomreach.json deleted file mode 100644 index 1abff1b..0000000 --- a/light_and_dark/data/world/rooms/room_town_gloomreach.json +++ /dev/null @@ -1,143 +0,0 @@ -{ - "meta": { - "id": "room_town_gloomreach", - "name": "room_town_gloomreach" - }, - "player_spawn": [ - 80, - 260 - ], - "bounds": { - "kill_y": 2000 - }, - "platforms": { - "ground": [ - [ - -500, - 360, - 2400, - 240 - ] - ] - }, - "enemies": [], - "npcs": [ - { - "type": "vendor", - "pos": [ - 700, - 306 - ] - }, - { - "type": "upgrader", - "pos": [ - 860, - 306 - ] - }, - { - "type": "necklace_vendor", - "pos": [ - 1020, - 306 - ] - }, - { - "type": "combat_master", - "pos": [ - 479, - 306 - ] - } - ], - "exits": [ - { - "rect": [ - 10, - 300, - 80, - 60 - ], - "to": "room_07", - "spawn": [ - -1000, - 2720 - ], - "hint": "Back" - } - ], - "ability_doors": [], - "entry_spawns": { - "entry_1": [ - 120, - 300 - ], - "entry_diary": [ - 300, - 300 - ] - }, - "props": [ - { - "type": "house", - "pos": [ - 720, - 360 - ], - "radius": 180, - "object_id": "house" - }, - { - "type": "save_point", - "pos": [ - 1300, - 360 - ], - "radius": 110 - }, - { - "type": "light_source", - "pos": [ - 1360, - 360 - ], - "radius": 180 - }, - { - "type": "diary", - "pos": [ - 239, - 310 - ], - "radius": 180 - }, - { - "type": "butterfly_jar", - "pos": [ - 1480, - 360 - ], - "radius": 180, - "object_id": "flow_butterfly_jar" - }, - { - "type": "butterfly_jar", - "pos": [ - 1520, - 360 - ], - "radius": 180, - "object_id": "flow_butterfly_jar" - }, - { - "type": "butterfly_jar", - "pos": [ - 1560, - 360 - ], - "radius": 180, - "object_id": "flow_butterfly_jar" - } - ] -} \ No newline at end of file diff --git a/light_and_dark/level_editor_v43.html b/light_and_dark/level_editor_v43.html index e99f605..b8e235a 100644 --- a/light_and_dark/level_editor_v43.html +++ b/light_and_dark/level_editor_v43.html @@ -9,9 +9,9 @@ --bg:#0b0f14; --panel:#111827; --panel2:#0f172a; --fg:#e5e7eb; --muted:#9ca3af; --accent:#60a5fa; --accent2:#34d399; --warn:#f59e0b; --danger:#ef4444; --border:#1f2937; } - html,body{height:100%; margin:0; background:var(--bg); color:var(--fg); font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;} - .wrap{display:grid; grid-template-columns: 360px 1fr; height:100vh; min-height:100vh;} - .left{background:var(--panel); border-right:1px solid var(--border); padding:12px; overflow:auto;} + html,body{height:100%; margin:0; overflow:hidden; background:var(--bg); color:var(--fg); font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;} + .wrap{display:grid; grid-template-columns: 360px 1fr; height:100vh; min-height:0; overflow:hidden;} + .left{background:var(--panel); border-right:1px solid var(--border); padding:12px; overflow-y:auto; height:100vh; box-sizing:border-box;} .section{background:rgba(255,255,255,0.03); border:1px solid var(--border); border-radius:12px; padding:10px; margin-bottom:12px;} .section h3{margin:0 0 8px 0; font-size:14px; color:var(--fg); display:flex; align-items:center; gap:8px;} .row{display:flex; gap:8px; flex-wrap:wrap; align-items:center;} @@ -32,7 +32,7 @@ .badge{font-size:11px; padding:2px 6px; border-radius:999px; border:1px solid var(--border); color:var(--muted);} .badge.dirty{border-color:rgba(245,158,11,0.6); color:#fbbf24;} .kbd{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:11px; padding:2px 6px; border:1px solid var(--border); border-radius:6px; color:var(--muted);} - .right{position:relative; display:flex; flex-direction:column; min-width:0; height:100%;} + .right{position:relative; display:flex; flex-direction:column; min-width:0; height:100%; overflow:hidden;} /* Floating tool palette pinned to the right edge of the canvas area */ #toolbar{ position:absolute; right:10px; top:70px; z-index:60; @@ -105,6 +105,16 @@ Grid snap (20px) on placement / drag +
+ + +
Layers: every object (platforms, houses, NPCs, enemies, props, doors, exits) has a layer in its Properties — lower = further back, higher = further forward. The canvas and game draw back-to-front by (layer, category), and the select tool respects layers.
@@ -258,6 +268,7 @@ house: { type: "house" }, diary: { type: "diary" }, player: { type: "player" }, + quest_point: { type: "quest_giver" }, }; // Type fallbacks used when no object def provides a radius. const TYPE_DEFAULT_RADII = { light_source: 180, save_point: 90 }; @@ -328,7 +339,22 @@ }; } - function normalizeRoom(json, fallbackName, filename){ + function normPlatformEntry(rect, group){ + if(Array.isArray(rect)){ + return { x:+rect[0]||0, y:+rect[1]||0, w:+rect[2]||0, h:+rect[3]||0, group, layer: rect.length>=5 ? (+rect[4]||0) : 0 }; + } + if(rect && typeof rect === "object"){ + const out = { x:+rect.x||0, y:+rect.y||0, w:+rect.w||0, h:+rect.h||0, group: rect.group || group || "ground", layer: +(rect.layer||0) }; + if(Array.isArray(rect.path)) out.path = deepClone(rect.path); + if(rect.speed !== undefined) out.speed = +rect.speed; + if(rect.name) out.name = String(rect.name); + if(Array.isArray(rect.respawn)) out.respawn = deepClone(rect.respawn); + return out; + } + return null; + } + + function normalizeRoom(json, fallbackName, filename){ // Supports both: // A) editor-native rooms (name, platforms:[{x,y,w,h}], exits:[{x,y,w,h}...]) // B) your game rooms (meta, platforms:{ground:[[x,y,w,h],...], ...}, exits:[{rect:[...]}], enemies:[{pos:[x,y]}]) @@ -367,27 +393,23 @@ const arr = platObj[group]; if(!Array.isArray(arr)) continue; for(const rect of arr){ - if(!Array.isArray(rect) || rect.length < 4) continue; - r.platforms.push({ x:+rect[0]||0, y:+rect[1]||0, w:+rect[2]||0, h:+rect[3]||0, group, layer: rect.length>=5 ? (+rect[4]||0) : 0 }); + const np = normPlatformEntry(rect, group); + if(np) r.platforms.push(np); } } // if nothing came through, try also accepting editor-native arrays inside game rooms if(Array.isArray(src.platforms)){ for(const p of src.platforms){ - if(p && typeof p === "object"){ - r.platforms.push({ x:+p.x||0, y:+p.y||0, w:+p.w||0, h:+p.h||0, group: p.group || "ground", layer: +(p.layer||0) }); - } + const np = normPlatformEntry(p, p && p.group ? p.group : "ground"); + if(np) r.platforms.push(np); } } } else { // editor-native if(Array.isArray(src.platforms)){ for(const p of src.platforms){ - if(p && typeof p === "object"){ - r.platforms.push({ x:+p.x||0, y:+p.y||0, w:+p.w||0, h:+p.h||0, group: p.group || "ground", layer: +(p.layer||0) }); - } else if(Array.isArray(p) && p.length>=4){ - r.platforms.push({ x:+p[0]||0, y:+p[1]||0, w:+p[2]||0, h:+p[3]||0, group:"ground", layer: p.length>=5 ? (+p[4]||0) : 0 }); - } + const np = normPlatformEntry(p, p && p.group ? p.group : "ground"); + if(np) r.platforms.push(np); } } else if(src.platforms && typeof src.platforms === "object"){ // allow grouped platforms even in editor rooms @@ -395,8 +417,8 @@ const arr = src.platforms[group]; if(!Array.isArray(arr)) continue; for(const rect of arr){ - if(!Array.isArray(rect) || rect.length < 4) continue; - r.platforms.push({ x:+rect[0]||0, y:+rect[1]||0, w:+rect[2]||0, h:+rect[3]||0, group, layer: rect.length>=5 ? (+rect[4]||0) : 0 }); + const np = normPlatformEntry(rect, group); + if(np) r.platforms.push(np); } } } @@ -444,7 +466,7 @@ const type = p.type || p.kind || "light_source"; const object_id = p.object_id || p.objectId || null; const radius = +p.radius || getDefaultRadiusForType(type, object_id); - r.props.push({ x, y, type, radius, object_id, layer: +(p.layer||0) }); + r.props.push({ x, y, type, radius, object_id, layer: +(p.layer||0), quest_id: p.quest_id || p.questId || "" }); } } @@ -535,7 +557,8 @@ enemyPlaceType: "enemy", npcPlaceType: "vendor", showAggro: false, - objectPlaceId: null + objectPlaceId: null, + waitingPathPt: null }; const els = { @@ -957,6 +980,24 @@ Cancel = Choose whether to remove without saving`); const room = roomWrap.data; + // Adding a path point to a selected moving platform? + if(state.waitingPathPt){ + if(ev.button === 0){ + ev.preventDefault(); + const wp = state.waitingPathPt; + const wrap = state.rooms.get(wp.roomId); + const plat = wrap && wrap.data.platforms && wrap.data.platforms[wp.index]; + if(plat){ + if(!Array.isArray(plat.path)) plat.path = [[plat.x, plat.y]]; + plat.path.push(snapPt(wx, wy)); + } + state.waitingPathPt = null; + refreshUI(); + setStatus("Path point added."); + return; + } + } + if(ev.button === 1){ // middle mouse pan handled by mousedown return; @@ -1075,7 +1116,8 @@ Cancel = Choose whether to remove without saving`); const [sx1,sy1]=snapPt(wx,wy); // Start as a 1x1 rect at the anchor; resizing can go in any direction. - const p = {x: sx1, y: sy1, w: 1, h: 1}; + const p = {x: sx1, y: sy1, w: 1, h: 1, group: state.platformGroup || "ground"}; + if(p.group === "moving") p.path = [[sx1, sy1]]; room.platforms.push(p); state.selection = {kind:"platform", index: room.platforms.length-1, roomId: roomWrap.id}; @@ -1491,7 +1533,12 @@ Cancel = Choose whether to remove without saving`); }; const layerable = []; for(const p of (room.props || [])) if(p) layerable.push({ z: +(p.layer||0), cat: 0, kind: "prop", p }); - for(const p of (room.platforms || [])) layerable.push({ z: +(p.layer||0), cat: 1, kind: "platform", p }); + for(const p of (room.platforms || [])){ + if(p && (p.group === "moving")) layerable.push({ z: +(p.layer||0), cat: 1, kind: "moving", p }); + else if(p && (p.group === "trigger")) layerable.push({ z: +(p.layer||0), cat: 1, kind: "trigger", p }); + else if(p && (p.group === "hazard")) layerable.push({ z: +(p.layer||0), cat: 1, kind: "hazard", p }); + else layerable.push({ z: +(p.layer||0), cat: 1, kind: "platform", p }); + } for(const d of (room.ability_doors || [])) if(d) layerable.push({ z: +(d.layer||0), cat: 2, kind: "door", d }); for(const ex of (room.exits || [])) if(ex) layerable.push({ z: +(ex.layer||0), cat: 3, kind: "exit", ex }); for(const e of (room.enemies || [])) if(e) layerable.push({ z: +(e.layer||0), cat: 4, kind: "enemy", e }); @@ -1509,6 +1556,89 @@ Cancel = Choose whether to remove without saving`); ctx.strokeStyle = "rgba(96,165,250,0.75)"; ctx.lineWidth = 2*dpr; ctx.strokeRect(sx,sy, sx2-sx, sy2-sy); + } else if(it.kind === "moving"){ + const p = it.p; + const [sx,sy] = worldToScreen(p.x, p.y, canvas); + const [sx2,sy2] = worldToScreen(p.x+p.w, p.y+p.h, canvas); + ctx.fillStyle = "rgba(80,180,220,0.30)"; + ctx.fillRect(sx,sy, sx2-sx, sy2-sy); + ctx.strokeStyle = "rgba(150,220,255,0.9)"; + ctx.lineWidth = 2*dpr; + ctx.strokeRect(sx,sy, sx2-sx, sy2-sy); + // draw the movement path (dashed line + waypoint markers) + const path = p.path || []; + if(path.length >= 2){ + ctx.strokeStyle = "rgba(150,220,255,0.5)"; + ctx.lineWidth = 1.5*dpr; + ctx.setLineDash([4*dpr, 4*dpr]); + ctx.beginPath(); + path.forEach((pt, pi)=>{ + const [xx,yy] = worldToScreen(pt[0], pt[1], canvas); + if(pi===0) ctx.moveTo(xx,yy); else ctx.lineTo(xx,yy); + }); + // loop back to start + const [exx,eyy] = worldToScreen(path[0][0], path[0][1], canvas); + ctx.lineTo(exx, eyy); + ctx.stroke(); + ctx.setLineDash([]); + for(const pt of path){ + const [xx,yy] = worldToScreen(pt[0], pt[1], canvas); + ctx.fillStyle = "rgba(150,220,255,0.9)"; + ctx.beginPath(); ctx.arc(xx, yy, 3*dpr, 0, Math.PI*2); ctx.fill(); + } + // ghost outline of the platform at its final waypoint + const lastPt = path[path.length-1]; + if(lastPt){ + const [gx,gy] = worldToScreen(lastPt[0], lastPt[1], canvas); + const gw = sx2 - sx, gh = sy2 - sy; + ctx.strokeStyle = "rgba(150,220,255,0.45)"; + ctx.lineWidth = 2*dpr; + ctx.setLineDash([5*dpr, 4*dpr]); + ctx.strokeRect(gx, gy, gw, gh); + ctx.setLineDash([]); + } + } + } else if(it.kind === "trigger"){ + const p = it.p; + const [sx,sy] = worldToScreen(p.x, p.y, canvas); + const [sx2,sy2] = worldToScreen(p.x+p.w, p.y+p.h, canvas); + ctx.fillStyle = "rgba(220,190,60,0.30)"; + ctx.fillRect(sx,sy, sx2-sx, sy2-sy); + ctx.strokeStyle = "rgba(255,230,120,0.9)"; + ctx.lineWidth = 2*dpr; + ctx.strokeRect(sx,sy, sx2-sx, sy2-sy); + const label = p.name ? ("Trigger: " + p.name) : "Trigger"; + ctx.fillStyle = "rgba(229,231,235,0.9)"; + ctx.font = `${11*dpr}px system-ui`; + ctx.fillText(label, sx+4*dpr, sy-4*dpr); + } else if(it.kind === "hazard"){ + const p = it.p; + const [sx,sy] = worldToScreen(p.x, p.y, canvas); + const [sx2,sy2] = worldToScreen(p.x+p.w, p.y+p.h, canvas); + ctx.fillStyle = "rgba(239,68,68,0.35)"; + ctx.fillRect(sx,sy, sx2-sx, sy2-sy); + ctx.strokeStyle = "rgba(239,68,68,0.9)"; + ctx.lineWidth = 2*dpr; + ctx.strokeRect(sx,sy, sx2-sx, sy2-sy); + ctx.fillStyle = "rgba(229,231,235,0.9)"; + ctx.font = `${11*dpr}px system-ui`; + ctx.fillText("Hazard", sx+4*dpr, sy-4*dpr); + // linked respawn marker + if(Array.isArray(p.respawn) && p.respawn.length>=2){ + const [rx,ry] = worldToScreen(p.respawn[0], p.respawn[1], canvas); + ctx.strokeStyle = "rgba(52,211,153,0.9)"; + ctx.lineWidth = 2*dpr; + ctx.beginPath(); ctx.arc(rx, ry, 6*dpr, 0, Math.PI*2); ctx.stroke(); + ctx.fillStyle = "rgba(52,211,153,0.9)"; + ctx.beginPath(); ctx.arc(rx, ry, 3*dpr, 0, Math.PI*2); ctx.fill(); + ctx.strokeStyle = "rgba(52,211,153,0.5)"; + ctx.setLineDash([3*dpr,3*dpr]); + ctx.beginPath(); + ctx.moveTo((sx+sx2)/2, (sy+sy2)/2); + ctx.lineTo(rx, ry); + ctx.stroke(); + ctx.setLineDash([]); + } } else if(it.kind === "door"){ const d = it.d; const [sx,sy] = worldToScreen(d.x, d.y, canvas); @@ -1807,26 +1937,17 @@ Cancel = Choose whether to remove without saving`); const ex = exits[i]; if(!ex || ex.to !== dstWrap.data.name) continue; - // End the line at the corresponding DOOR in the compare room (the exit that - // leads back to this room), so it lands accurately on the doorway. + // End the line at the ENTRY point in the compare room (spawn / entry spawn), + // i.e. exit -> entry, not exit -> exit. + const sid = ex.spawn_id || null; let pos = null; - for(const dex of (dstWrap.data.exits || [])){ - if(dex && dex.to === srcWrap.data.name){ - pos = [dex.x + dex.w/2, dex.y + dex.h/2]; - break; - } + if(sid === "__player_spawn__"){ + pos = dstWrap.data.player_spawn || null; + } else if(sid){ + pos = (dstWrap.data.entry_spawns && dstWrap.data.entry_spawns[sid]) ? dstWrap.data.entry_spawns[sid] : null; } - // Fallback: the exit's spawn / entry spawn in the compare room. - if(!pos){ - const sid = ex.spawn_id || null; - if(sid === "__player_spawn__"){ - pos = dstWrap.data.player_spawn || null; - } else if(sid){ - pos = (dstWrap.data.entry_spawns && dstWrap.data.entry_spawns[sid]) ? dstWrap.data.entry_spawns[sid] : null; - } - if(!pos && Array.isArray(ex.spawn) && ex.spawn.length >= 2){ - pos = ex.spawn; - } + if(!pos && Array.isArray(ex.spawn) && ex.spawn.length >= 2){ + pos = ex.spawn; } if(!pos) continue; @@ -1998,6 +2119,39 @@ Cancel = Choose whether to remove without saving`);
h
layer (lower = back)
+
+
type
+ +
+ ${(p.group||"ground") === "trigger" ? ` +
+
name
+ +
` : ""} + ${(p.group||"ground") === "hazard" ? ` +
+
respawn point (hazard teleports here)
+ +
` : ""} + ${(p.group||"ground") === "moving" ? ` +
+
path (waypoints - click Add, then the canvas)
+ +
+
+
+
speed (px/s)
+ +
` : ""}
@@ -2079,9 +2233,10 @@ Cancel = Choose whether to remove without saving`);
y
-
type
+
type / name
+
Any name works (e.g. quest_giver_gwen). vendor / upgrader / necklace_vendor / combat_master have game behavior; custom names are inert until coded.
layer (lower = back)
@@ -2096,7 +2251,8 @@ Cancel = Choose whether to remove without saving`); if(!p) return "
Selection missing.
"; const prDefault = getDefaultRadiusForType(p.type || "light_source", p.object_id || p.objectId || null); const objId = p.object_id || p.objectId || ""; - const hint = ({light_source:"Butterfly (charge regen 2x)", save_point:"Lantern (save point - press F to save)", butterfly_jar:"Butterfly-in-jar (collectible)", diary:"Boss diary (interact)", house:"Standalone house", player:"Player size reference (no in-game effect)"})[p.type||"light_source"] || ""; + const hint = ({light_source:"Butterfly (charge regen 2x)", save_point:"Lantern (save point - press F to save)", butterfly_jar:"Butterfly-in-jar (collectible)", diary:"Boss diary (interact)", house:"Standalone house", player:"Player size reference (no in-game effect)", quest_giver:"Quest point (name it below)"})[p.type||"light_source"] || ""; + const isQuest = (p.type||"") === "quest_giver"; return `
Prop #${sel.index}${hint ? ` · ${hint}` : ""}
@@ -2108,6 +2264,11 @@ Cancel = Choose whether to remove without saving`);
type
+ ${isQuest ? ` +
+
quest id (name)
+ +
` : ""}
object id
@@ -2188,6 +2349,67 @@ Cancel = Choose whether to remove without saving`); })); }); const pl=q("pl"); if(pl) pl.addEventListener("change", ()=>pushAnd(()=>{ p.layer = Math.round(num(pl.value)); })); + const pg=q("pg"); if(pg) pg.addEventListener("change", ()=>pushAnd(()=>{ + p.group = pg.value || "ground"; + if(p.group === "moving" && !Array.isArray(p.path)) p.path = [[p.x, p.y]]; + if(p.group !== "trigger" && p.name !== undefined && p.name !== "") delete p.name; + })); + const ptname=q("ptname"); if(ptname) ptname.addEventListener("change", ()=>pushAnd(()=>{ + const v = ptname.value.trim(); + if(v){ p.name = v; } else { delete p.name; } + })); + const ptspeed=q("ptspeed"); if(ptspeed) ptspeed.addEventListener("change", ()=>pushAnd(()=>{ p.speed = Math.max(1, num(ptspeed.value)); })); + const hzrespawn=q("hzrespawn"); + if(hzrespawn){ + // preselect current respawn if it matches an entry spawn + let cur = ""; + if(Array.isArray(p.respawn)){ + const es = room.entry_spawns || {}; + for(const k of Object.keys(es)){ + const pos = es[k]; + if(Array.isArray(pos) && pos.length>=2 && Math.round(pos[0])===Math.round(p.respawn[0]) && Math.round(pos[1])===Math.round(p.respawn[1])){ + cur = k; break; + } + } + } + hzrespawn.value = cur; + hzrespawn.addEventListener("change", ()=>pushAnd(()=>{ + const v = hzrespawn.value; + if(v === ""){ delete p.respawn; } + else { + const pos = (room.entry_spawns||{})[v]; + if(Array.isArray(pos) && pos.length>=2) p.respawn = [Math.round(pos[0]), Math.round(pos[1])]; + } + })); + } + const addPathPt=q("addPathPt"); if(addPathPt) addPathPt.addEventListener("click", ()=>{ + state.waitingPathPt = { roomId: roomWrap.id, index: sel.index }; + setStatus("Click on the canvas to add a path point to this moving platform."); + refreshProps(); + }); + const pathList=q("pathList"); + if(pathList && Array.isArray(p.path)){ + pathList.innerHTML = ""; + p.path.forEach((pt, pi)=>{ + const row = document.createElement("div"); + row.style.cssText = "display:flex; align-items:center; gap:6px; margin-bottom:4px;"; + row.innerHTML = `${pi+1}. (${Math.round(pt[0])}, ${Math.round(pt[1])})`; + const delBtn = document.createElement("button"); + delBtn.textContent = "x"; + delBtn.style.cssText = "padding:2px 8px;"; + delBtn.addEventListener("click", ()=>pushAnd(()=>{ + p.path.splice(pi,1); + })); + row.appendChild(delBtn); + pathList.appendChild(row); + }); + if(state.waitingPathPt && state.waitingPathPt.index === sel.index){ + const hint = document.createElement("div"); + hint.className = "muted"; + hint.textContent = "Click the canvas to add point " + (p.path.length + 1); + pathList.appendChild(hint); + } + } const del=q("delSel"); if(del) del.addEventListener("click", ()=>pushAnd(()=>{ room.platforms.splice(sel.index,1); state.selection=null; @@ -2250,10 +2472,14 @@ Cancel = Choose whether to remove without saving`); })); } else if(sel.kind==="prop"){ const p = room.props && room.props[sel.index]; if(!p) return; - const prx=q("prx"), pry=q("pry"), prtype=q("prtype"), probjid=q("probjid"), prrad=q("prrad"), prlayer=q("prlayer"); + const prx=q("prx"), pry=q("pry"), prtype=q("prtype"), probjid=q("probjid"), prrad=q("prrad"), prlayer=q("prlayer"), prqid=q("prqid"); if(prx) prx.addEventListener("change", ()=>pushAnd(()=>{ p.x=num(prx.value); })); if(pry) pry.addEventListener("change", ()=>pushAnd(()=>{ p.y=num(pry.value); })); if(prlayer) prlayer.addEventListener("change", ()=>pushAnd(()=>{ p.layer = Math.round(num(prlayer.value)); })); + if(prqid) prqid.addEventListener("change", ()=>pushAnd(()=>{ + const v = prqid.value.trim(); + if(v){ p.quest_id = v; } else { delete p.quest_id; } + })); if(probjid) probjid.addEventListener("change", ()=>pushAnd(()=>{ const v = probjid.value.trim(); if(v){ p.object_id = v; } else { delete p.object_id; } @@ -2309,6 +2535,24 @@ Cancel = Choose whether to remove without saving`); const room = roomWrap.data; + // Adding a path point to a selected moving platform? + if(state.waitingPathPt){ + if(ev.button === 0){ + ev.preventDefault(); + const wp = state.waitingPathPt; + const wrap = state.rooms.get(wp.roomId); + const plat = wrap && wrap.data.platforms && wrap.data.platforms[wp.index]; + if(plat){ + if(!Array.isArray(plat.path)) plat.path = [[plat.x, plat.y]]; + plat.path.push(snapPt(wx, wy)); + } + state.waitingPathPt = null; + refreshUI(); + setStatus("Path point added."); + return; + } + } + if(ev.button === 1){ // middle mouse pan is handled by the mousedown listener (better cross-browser) return; @@ -2390,7 +2634,8 @@ Cancel = Choose whether to remove without saving`); const [sx1,sy1]=snapPt(wx,wy); // Start as a 1x1 rect at the anchor; resizing can go in any direction. - const p = {x: sx1, y: sy1, w: 1, h: 1}; + const p = {x: sx1, y: sy1, w: 1, h: 1, group: state.platformGroup || "ground"}; + if(p.group === "moving") p.path = [[sx1, sy1]]; room.platforms.push(p); state.selection = {kind:"platform", index: room.platforms.length-1, roomId: roomWrap.id}; @@ -3000,14 +3245,27 @@ Cancel = Choose whether to remove without saving`); const fmt = d._format || "editor_v1"; if(fmt === "game_v1"){ - // rebuild grouped platforms (layer appended as 5th element when non-zero) + // rebuild grouped platforms (layer appended as 5th element when non-zero; + // moving/trigger platforms serialize as objects to keep their path/name/speed) const platforms = {}; for(const p of (d.platforms || [])){ const group = p.group || "ground"; if(!platforms[group]) platforms[group] = []; - const rect = [p.x, p.y, p.w, p.h]; - if(p.layer) rect.push(p.layer); - platforms[group].push(rect); + if(group === "moving" || group === "trigger" || group === "hazard"){ + const obj = { x: p.x, y: p.y, w: p.w, h: p.h }; + if(p.layer) obj.layer = p.layer; + if(group === "moving"){ + if(Array.isArray(p.path)) obj.path = p.path; + if(p.speed !== undefined) obj.speed = p.speed; + } + if(p.name) obj.name = p.name; + if(Array.isArray(p.respawn)) obj.respawn = p.respawn; + platforms[group].push(obj); + } else { + const rect = [p.x, p.y, p.w, p.h]; + if(p.layer) rect.push(p.layer); + platforms[group].push(rect); + } } // enemies back to {type, pos:[x,y]} (+ layer when set) @@ -3052,6 +3310,7 @@ Cancel = Choose whether to remove without saving`); }; if(p.object_id) out.object_id = p.object_id; if(p.layer) out.layer = p.layer; + if(p.quest_id) out.quest_id = p.quest_id; return out; }); diff --git a/light_and_dark/main.py b/light_and_dark/main.py index 8b41923..125f27f 100644 --- a/light_and_dark/main.py +++ b/light_and_dark/main.py @@ -21,6 +21,10 @@ SHOCKWAVE_DAMAGE = 2 BOSS_POISON_DURATION = 4.0 # boss attacks poison the player for this many seconds ATTACK_COOLDOWN_BASE = 0.35 # recovery between melee swings; attack speed reduces it +# Horizontal deadzone: enemies only turn toward the player when the player is this far +# to one side, so a player standing directly above/below no longer makes them wiggle. +CHASE_DEADZONE = 6 + # --- Ranged ability / projectile settings --- RANGED_CHARGE_COST = 5.0 # costs more than melee (melee costs 1) RANGED_DAMAGE_MULT = 0.5 # 50% of melee damage @@ -915,13 +919,32 @@ def normalize_room_json(room_json: dict) -> dict: if isinstance(platforms, list): grouped = {"ground": [], "floating": []} for p in platforms: - if isinstance(p, (list, tuple)) and len(p) == 4: - grouped["ground"].append([p[0], p[1], p[2], p[3]]) + if isinstance(p, (list, tuple)) and len(p) >= 4: + arr = [p[0], p[1], p[2], p[3]] + if len(p) >= 5: + arr.append(p[4]) + grouped["ground"].append(arr) elif isinstance(p, dict): - rect = [p.get("x", 0), p.get("y", 0), p.get("w", 0), p.get("h", 0)] group = p.get("group", "ground") - grouped.setdefault(group, []) - grouped[group].append(rect) + if group in ("moving", "trigger", "hazard"): + obj = {"x": p.get("x", 0), "y": p.get("y", 0), "w": p.get("w", 0), "h": p.get("h", 0)} + if "layer" in p and p.get("layer"): + obj["layer"] = p["layer"] + if group == "moving": + if isinstance(p.get("path"), list): + obj["path"] = p["path"] + if "speed" in p: + obj["speed"] = p["speed"] + if "name" in p: + obj["name"] = p["name"] + if isinstance(p.get("respawn"), (list, tuple)) and len(p["respawn"]) >= 2: + obj["respawn"] = p["respawn"] + grouped.setdefault(group, []).append(obj) + else: + rect = [p.get("x", 0), p.get("y", 0), p.get("w", 0), p.get("h", 0)] + if p.get("layer"): + rect.append(p["layer"]) + grouped.setdefault(group, []).append(rect) out["platforms"] = grouped elif not isinstance(platforms, dict): out["platforms"] = {"ground": [], "floating": []} @@ -954,6 +977,20 @@ def normalize_room_json(room_json: dict) -> dict: norm_npcs.append(n) out["npcs"] = norm_npcs + # props -> ensure pos:[x,y] + props = out.get("props", []) + if isinstance(props, list): + norm_props = [] + for p in props: + if not isinstance(p, dict): + continue + if not (isinstance(p.get("pos"), (list, tuple)) and len(p["pos"]) == 2): + if "x" in p and "y" in p: + p = dict(p) + p["pos"] = [p.get("x", 0), p.get("y", 0)] + norm_props.append(p) + out["props"] = norm_props + # exits -> ensure rect:[x,y,w,h] exits = out.get("exits", []) if isinstance(exits, list): @@ -998,18 +1035,78 @@ class Room: self.kill_y = int((room_json.get("bounds", {}) or {}).get("kill_y", 1400)) self.platform_layer: dict[int, int] = {} + self.platform_meta: dict[int, dict] = {} # id(rect) -> {"name"|"path"|"speed", ...} platforms = room_json.get("platforms", {}) if isinstance(room_json, dict) else {} if isinstance(platforms, list): self.platforms_ground = rects_from_list(platforms, self.platform_layer) self.platforms_floating = [] + self.platforms_hazard = [] + self.platforms_trigger = [] + self.platforms_moving = [] elif isinstance(platforms, dict): self.platforms_ground = rects_from_list(platforms.get("ground", []), self.platform_layer) self.platforms_floating = rects_from_list(platforms.get("floating", []), self.platform_layer) + self.platforms_hazard = [] + for hp in platforms.get("hazard", []): + if isinstance(hp, dict): + rect = pygame.Rect(int(hp.get("x", 0)), int(hp.get("y", 0)), int(hp.get("w", 0)), int(hp.get("h", 0))) + self.platform_layer[id(rect)] = int(hp.get("layer", 0)) + respawn = hp.get("respawn") + if isinstance(respawn, (list, tuple)) and len(respawn) >= 2: + self.platform_meta[id(rect)] = {"respawn": [int(respawn[0]), int(respawn[1])]} + self.platforms_hazard.append(rect) + elif isinstance(hp, (list, tuple)) and len(hp) >= 4: + rect = pygame.Rect(int(hp[0]), int(hp[1]), int(hp[2]), int(hp[3])) + if len(hp) >= 5: + try: + self.platform_layer[id(rect)] = int(hp[4]) + except (TypeError, ValueError): + pass + self.platforms_hazard.append(rect) + self.platforms_trigger = [] + for tp in platforms.get("trigger", []): + if isinstance(tp, dict): + rect = pygame.Rect(int(tp.get("x", 0)), int(tp.get("y", 0)), int(tp.get("w", 0)), int(tp.get("h", 0))) + self.platform_layer[id(rect)] = int(tp.get("layer", 0)) + self.platform_meta[id(rect)] = {"name": str(tp.get("name", ""))} + self.platforms_trigger.append(rect) + elif isinstance(tp, (list, tuple)) and len(tp) >= 4: + rect = pygame.Rect(int(tp[0]), int(tp[1]), int(tp[2]), int(tp[3])) + if len(tp) >= 5: + try: + self.platform_layer[id(rect)] = int(tp[4]) + except (TypeError, ValueError): + pass + self.platforms_trigger.append(rect) + self.platforms_moving = [] + for mp in platforms.get("moving", []): + if isinstance(mp, dict): + rect = pygame.Rect(int(mp.get("x", 0)), int(mp.get("y", 0)), int(mp.get("w", 0)), int(mp.get("h", 0))) + self.platform_layer[id(rect)] = int(mp.get("layer", 0)) + path = mp.get("path") or [] + if isinstance(path, list) and all(isinstance(w, (list, tuple)) and len(w) >= 2 for w in path): + path = [[int(w[0]), int(w[1])] for w in path] + else: + path = [[rect.x, rect.y]] + speed = float(mp.get("speed", 120)) + self.platform_meta[id(rect)] = {"path": path, "speed": speed, "i": 0, "t": 0.0} + self.platforms_moving.append(rect) + elif isinstance(mp, (list, tuple)) and len(mp) >= 4: + rect = pygame.Rect(int(mp[0]), int(mp[1]), int(mp[2]), int(mp[3])) + if len(mp) >= 5: + try: + self.platform_layer[id(rect)] = int(mp[4]) + except (TypeError, ValueError): + pass + self.platforms_moving.append(rect) else: self.platforms_ground = [] self.platforms_floating = [] + self.platforms_hazard = [] + self.platforms_trigger = [] + self.platforms_moving = [] self.ground_platforms = self.platforms_ground[:] - self.platforms = self.platforms_ground + self.platforms_floating + self.platforms = self.platforms_ground + self.platforms_floating + self.platforms_moving self.enemies = room_json.get("enemies", []) if isinstance(room_json, dict) else [] if not isinstance(self.enemies, list): @@ -4222,7 +4319,10 @@ class Enemy: self.direction = -1 if dx > 0 else 1 if chasing: - self.direction = 1 if dx > 0 else -1 + if dx < -CHASE_DEADZONE: + self.direction = -1 + elif dx > CHASE_DEADZONE: + self.direction = 1 self.last_chase_dir = self.direction # remember chase intent # aggro just broke (shade OR distance) @@ -4431,7 +4531,11 @@ class Enemy: # --- PATROL / CHASE INTENT --- if chasing: - self.direction = 1 if player.rect.centerx > self.rect.centerx else -1 + cdx = player.rect.centerx - self.rect.centerx + if cdx < -CHASE_DEADZONE: + self.direction = -1 + elif cdx > CHASE_DEADZONE: + self.direction = 1 elif self.stepoff_timer > 0.0: # aggro just broke near an edge: keep walking off naturally self.direction = self.stepoff_dir @@ -4818,6 +4922,45 @@ class HouseObject: draw_house_shape(surface, cx, cy, self.x, self.y) +def update_moving_platforms(dt: float): + """Advance moving platforms along their waypoint paths (top-left follows the path).""" + for rect in platforms_moving: + mdata = platform_meta.get(id(rect)) + if not mdata: + continue + path = mdata.get("path") or [] + if len(path) < 2: + continue + n = len(path) + speed = float(mdata.get("speed", 120)) + i = mdata.get("i", 0) + t = mdata.get("t", 0.0) + remaining = speed * dt + while remaining > 0: + x0, y0 = path[i] + x1, y1 = path[(i + 1) % n] + dx = x1 - x0 + dy = y1 - y0 + seg_len = math.hypot(dx, dy) + if seg_len <= 0: + i = (i + 1) % n + continue + to_end = (1.0 - t) * seg_len + if remaining < to_end: + t += remaining / seg_len + remaining = 0.0 + else: + remaining -= to_end + t = 0.0 + i = (i + 1) % n + mdata["i"] = i + mdata["t"] = t + x0, y0 = path[i] + x1, y1 = path[(i + 1) % n] + rect.x = int(round(x0 + (x1 - x0) * t)) + rect.y = int(round(y0 + (y1 - y0) * t)) + + def draw_layerable(surface, cx, cy): """Draw all static room objects back-to-front by (layer, category). @@ -4847,6 +4990,15 @@ def draw_layerable(surface, cx, cy): # platforms for p in platforms: items.append((platform_layer.get(id(p), 0), 1, "platform", p)) + # moving platforms (solid, follow a path) layer alongside platforms + for p in platforms_moving: + items.append((platform_layer.get(id(p), 0), 1, "moving", p)) + # hazard zones (red) layer alongside platforms + for p in platforms_hazard: + items.append((platform_layer.get(id(p), 0), 1, "hazard", p)) + # trigger placeholders (yellow, non-solid, no behavior yet) + for p in platforms_trigger: + items.append((platform_layer.get(id(p), 0), 1, "trigger", p)) # ability doors for d in ability_doors: items.append((getattr(d, "layer", 0), 2, "door", d)) @@ -4874,6 +5026,18 @@ def draw_layerable(surface, cx, cy): PLATFORM_COLOR, pygame.Rect(obj.x - cx, obj.y - cy, obj.width, obj.height), ) + elif kind == "moving": + mr = pygame.Rect(obj.x - cx, obj.y - cy, obj.width, obj.height) + pygame.draw.rect(surface, (80, 180, 220), mr) + pygame.draw.rect(surface, (180, 230, 255), mr, 2) + elif kind == "hazard": + hz = pygame.Rect(obj.x - cx, obj.y - cy, obj.width, obj.height) + pygame.draw.rect(surface, (200, 40, 40), hz) + pygame.draw.rect(surface, (255, 120, 120), hz, 2) + elif kind == "trigger": + tr = pygame.Rect(obj.x - cx, obj.y - cy, obj.width, obj.height) + pygame.draw.rect(surface, (220, 190, 60), tr) + pygame.draw.rect(surface, (255, 230, 120), tr, 2) elif kind == "exit": rr = obj["rect"] pygame.draw.rect( @@ -5114,6 +5278,10 @@ ROOM = room_manager.current_room KILL_Y = ROOM.kill_y platforms_ground = ROOM.platforms_ground platforms_floating = ROOM.platforms_floating +platforms_hazard = ROOM.platforms_hazard +platforms_trigger = ROOM.platforms_trigger +platforms_moving = ROOM.platforms_moving +platform_meta = ROOM.platform_meta ground_platforms = ROOM.ground_platforms platforms = ROOM.platforms platform_layer = ROOM.platform_layer @@ -5155,10 +5323,8 @@ class QuestGiver: self.bob_t = random.random() * 10.0 def draw(self, surface, cx, cy): - bob_y = int(math.sin(self.bob_t) * 2) - self.bob_t += 0.05 x = int(self.rect.x - cx) - y = int(self.rect.y - cy + bob_y) + y = int(self.rect.y - cy) pygame.draw.rect(surface, (255, 130, 180), (x, y, self.rect.width, self.rect.height)) pygame.draw.rect(surface, (160, 40, 90), (x, y, self.rect.width, self.rect.height), 2) @@ -5342,7 +5508,8 @@ def grant_five_tempered() -> dict | None: def apply_room(room: Room, set_player_spawn=True): """Updates room-dependent lists + respawns enemies. Keeps inventory/equipment.""" - global ROOM, KILL_Y, platforms_ground, platforms_floating, ground_platforms, platforms + global ROOM, KILL_Y, platforms_ground, platforms_floating, platforms_hazard, ground_platforms, platforms + global platforms_trigger, platforms_moving, platform_meta global platform_layer global enemies, npcs, loot_drops, shockwaves, coin_drops, projectiles, ability_doors global shatter_platforms, shatter_aoes, tether_shots @@ -5359,6 +5526,10 @@ def apply_room(room: Room, set_player_spawn=True): KILL_Y = ROOM.kill_y platforms_ground = ROOM.platforms_ground platforms_floating = ROOM.platforms_floating + platforms_hazard = ROOM.platforms_hazard + platforms_trigger = ROOM.platforms_trigger + platforms_moving = ROOM.platforms_moving + platform_meta = ROOM.platform_meta ground_platforms = ROOM.ground_platforms platforms = ROOM.platforms platform_layer = ROOM.platform_layer @@ -5941,10 +6112,8 @@ class DiaryObject: self.bob_t = random.random() * 10.0 def draw(self, surface, cx, cy): - bob_y = int(math.sin(self.bob_t) * 2) - self.bob_t += 0.05 x = int(self.rect.x - cx) - y = int(self.rect.y - cy + bob_y) + y = int(self.rect.y - cy) pygame.draw.rect(surface, (150, 110, 70), (x, y, self.rect.width, self.rect.height)) pygame.draw.rect(surface, (90, 60, 30), (x, y, self.rect.width, self.rect.height), 2) pygame.draw.line(surface, (90, 60, 30), @@ -5980,13 +6149,19 @@ def get_near_lantern(player_obj, save_point_list, range_px: int | None = None) - def save_at_lantern(sp: SavePoint): - """Set the lantern as the respawn point and save the game.""" + """Set the lantern as the respawn point, restore HP/Charge, and save the game.""" global LANTERN_RESPAWN_ROOM, LANTERN_RESPAWN_POS LANTERN_RESPAWN_ROOM = ROOM.id LANTERN_RESPAWN_POS = (sp.x, sp.y) + # Resting at a lantern fully restores the player. + player._cancel_heal() + player.health = player.max_health + player.charge = player.max_charge + player.poison_timer = 0.0 + player.poison_acc = 0.0 + player._clamp_resources() _do_save_game() - pickup_feed.push_center("Lantern - game saved", duration=2.0) - # Butterfly jars lost on death respawn after saving at a lantern in a different room. + pickup_feed.push_center("Lantern - game saved, fully restored", duration=2.0) if respawn_jars_if_elsewhere(): pickup_feed.push_center("Butterfly jars have respawned!", duration=2.5) @@ -7231,6 +7406,9 @@ while running: near_save_point = True break + # Move moving platforms before collision is computed so the player collides correctly. + update_moving_platforms(dt) + collision_platforms = platforms + [d.rect for d in ability_doors if not d.open] player_collision_platforms = collision_platforms + [p.rect for p in shatter_platforms if p.alive] @@ -7242,6 +7420,22 @@ while running: player.update(keys, player_collision_platforms, dt) + # Hazard zones: touching one deals 1 damage and teleports the player to its linked respawn + # (defaults to the room's player spawn). A lethal hit falls through to the normal death respawn. + for hz in platforms_hazard: + if player.rect.colliderect(hz): + player.take_damage(1) + mdata = platform_meta.get(id(hz), {}) + respawn = mdata.get("respawn") + if respawn: + spx, spy = respawn + else: + spx, spy = ROOM.player_spawn + player.rect.midbottom = (int(spx), int(spy)) + player.vel_y = 0.0 + player.on_ground = False + break + if player.is_shatterstep_active() and (not player.shade) and (not player.on_ground): if (not player.shatterstep_platform_spawned) and player.shatterstep_platform_cd <= 0.0: jump_v0 = abs(JUMP_LIGHT) @@ -7301,7 +7495,7 @@ while running: player.shatterstep_landing_pending = False player.shatterstep_landing_damage = 0.0 else: - # Respawn at the latest lantern save point if one has been touched. + # Normal death respawn: latest lantern save point, else the room spawn. if LANTERN_RESPAWN_ROOM and LANTERN_RESPAWN_POS: lx, ly = LANTERN_RESPAWN_POS if LANTERN_RESPAWN_ROOM != ROOM.id: diff --git a/light_and_dark/saves/new rooms.json b/light_and_dark/saves/new rooms.json new file mode 100644 index 0000000..bab9da9 --- /dev/null +++ b/light_and_dark/saves/new rooms.json @@ -0,0 +1,39 @@ +{ + "version": 1, + "saved_at": "2026-09-29T18:54:05", + "player": { + "room_id": "room_02.json", + "x": 774, + "y": 320, + "health": 10, + "charge": 100.0, + "shade": false, + "coins_light": 4, + "coins_dark": 0, + "butterfly_jars": 1, + "has_ranged": false, + "reward_chest_opened": false, + "equipment": { + "helmet": null, + "chest": null, + "pants": null, + "necklace": null + } + }, + "inventory": [], + "quests": { + "active": {}, + "completed_one_time": [], + "tokens": 0 + }, + "boss_diary": { + "slotted": {} + }, + "defeated_bosses": [], + "broken_doors": [], + "lantern_respawn": { + "room_id": "room_02.json", + "x": 720, + "y": 320 + } +} \ No newline at end of file diff --git a/light_and_dark/saves/save 1.json b/light_and_dark/saves/save 1.json index cb732a4..455da7d 100644 --- a/light_and_dark/saves/save 1.json +++ b/light_and_dark/saves/save 1.json @@ -1,11 +1,11 @@ { "version": 1, - "saved_at": "2026-09-29T12:12:10", + "saved_at": "2026-09-29T12:14:42", "player": { "room_id": "room_town_gloomreach.json", - "x": 1315, + "x": 1250, "y": 360, - "health": 10, + "health": 12, "charge": 125.0, "shade": false, "coins_light": 0,