room overhaul
more editor changes, new rooms
This commit is contained in:
@@ -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**).
|
||||
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
},
|
||||
"player": {
|
||||
"type": "player"
|
||||
},
|
||||
"quest_point": {
|
||||
"type": "quest_giver"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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": {}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
</label>
|
||||
</div>
|
||||
<div class="row" style="margin-top:8px;">
|
||||
<label class="muted" style="flex:1;">New platform type:</label>
|
||||
<select id="platformGroup" style="flex:1; min-width:120px;">
|
||||
<option value="ground">Ground</option>
|
||||
<option value="floating">Floating</option>
|
||||
<option value="hazard">Hazard (red - damage)</option>
|
||||
<option value="moving">Moving (path)</option>
|
||||
<option value="trigger">Trigger (name)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="muted" style="margin-top:8px;">
|
||||
<b>Layers:</b> every object (platforms, houses, NPCs, enemies, props, doors, exits) has a <b>layer</b> 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.
|
||||
</div>
|
||||
@@ -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`);
|
||||
<div><div class="mini">h</div><input id="ph" type="number" value="${p.h}"/></div>
|
||||
<div><div class="mini">layer (lower = back)</div><input id="pl" type="number" value="${p.layer||0}"/></div>
|
||||
</div>
|
||||
<div style="margin-top:8px;">
|
||||
<div class="mini">type</div>
|
||||
<select id="pg">
|
||||
<option value="ground" ${p.group==="ground"||!p.group?"selected":""}>Ground</option>
|
||||
<option value="floating" ${p.group==="floating"?"selected":""}>Floating</option>
|
||||
<option value="hazard" ${p.group==="hazard"?"selected":""}>Hazard (red - damage)</option>
|
||||
<option value="moving" ${p.group==="moving"?"selected":""}>Moving (path)</option>
|
||||
<option value="trigger" ${p.group==="trigger"?"selected":""}>Trigger (name)</option>
|
||||
</select>
|
||||
</div>
|
||||
${(p.group||"ground") === "trigger" ? `
|
||||
<div style="margin-top:8px;">
|
||||
<div class="mini">name</div>
|
||||
<input id="ptname" type="text" value="${p.name||""}" placeholder="e.g. opens_bridge"/>
|
||||
</div>` : ""}
|
||||
${(p.group||"ground") === "hazard" ? `
|
||||
<div style="margin-top:8px;">
|
||||
<div class="mini">respawn point (hazard teleports here)</div>
|
||||
<select id="hzrespawn">
|
||||
<option value="">(none - room default)</option>
|
||||
${Object.keys(room.entry_spawns||{}).map(k=>`<option value="${k}">Entry: ${k}</option>`).join("")}
|
||||
</select>
|
||||
</div>` : ""}
|
||||
${(p.group||"ground") === "moving" ? `
|
||||
<div style="margin-top:8px;">
|
||||
<div class="mini">path (waypoints - click Add, then the canvas)</div>
|
||||
<button id="addPathPt" class="primary" style="width:100%;">Add path point</button>
|
||||
<div id="pathList" style="margin-top:6px; max-height:120px; overflow:auto;"></div>
|
||||
</div>
|
||||
<div style="margin-top:8px;">
|
||||
<div class="mini">speed (px/s)</div>
|
||||
<input id="ptspeed" type="number" value="${p.speed||120}"/>
|
||||
</div>` : ""}
|
||||
<div class="row" style="margin-top:8px;">
|
||||
<button id="delSel" class="danger">Delete</button>
|
||||
</div>
|
||||
@@ -2079,9 +2233,10 @@ Cancel = Choose whether to remove without saving`);
|
||||
<div><div class="mini">y</div><input id="npy" type="number" value="${n.y}"/></div>
|
||||
</div>
|
||||
<div style="margin-top:8px;">
|
||||
<div class="mini">type</div>
|
||||
<div class="mini">type / name</div>
|
||||
<input id="npt" type="text" value="${n.type||"vendor"}"/>
|
||||
</div>
|
||||
<div class="muted" style="margin-top:6px;">Any name works (e.g. quest_giver_gwen). vendor / upgrader / necklace_vendor / combat_master have game behavior; custom names are inert until coded.</div>
|
||||
<div style="margin-top:8px;">
|
||||
<div class="mini">layer (lower = back)</div>
|
||||
<input id="npl" type="number" value="${n.layer||0}"/>
|
||||
@@ -2096,7 +2251,8 @@ Cancel = Choose whether to remove without saving`);
|
||||
if(!p) return "<div class='muted'>Selection missing.</div>";
|
||||
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 `
|
||||
<div><b>Prop</b> #${sel.index}${hint ? ` <span class="mini">· ${hint}</span>` : ""}</div>
|
||||
<div class="hr"></div>
|
||||
@@ -2108,6 +2264,11 @@ Cancel = Choose whether to remove without saving`);
|
||||
<div class="mini">type</div>
|
||||
<input id="prtype" type="text" value="${p.type||"light_source"}"/>
|
||||
</div>
|
||||
${isQuest ? `
|
||||
<div style="margin-top:8px;">
|
||||
<div class="mini">quest id (name)</div>
|
||||
<input id="prqid" type="text" value="${p.quest_id||""}" placeholder="e.g. quest_save_the_well"/>
|
||||
</div>` : ""}
|
||||
<div style="margin-top:8px;">
|
||||
<div class="mini">object id</div>
|
||||
<input id="probjid" type="text" value="${objId}" placeholder="(e.g. flow_butterfly, lamp, flow_butterfly_jar)"/>
|
||||
@@ -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 = `<span style="flex:1; font-size:12px;">${pi+1}. (${Math.round(pt[0])}, ${Math.round(pt[1])})</span>`;
|
||||
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;
|
||||
});
|
||||
|
||||
|
||||
+213
-19
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user