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
|
## 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**).
|
You can also save anytime from the pause menu (**Esc → Save Game**).
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,9 @@
|
|||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
"type": "player"
|
"type": "player"
|
||||||
|
},
|
||||||
|
"quest_point": {
|
||||||
|
"type": "quest_giver"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,158 +1,202 @@
|
|||||||
{
|
{
|
||||||
|
"name": "room_01",
|
||||||
|
"display_name": "room_01",
|
||||||
"meta": {
|
"meta": {
|
||||||
"id": "room_01",
|
"id": "room_01",
|
||||||
"name": "Room 01 - Tutorial Steps"
|
"name": "room_01"
|
||||||
},
|
},
|
||||||
|
"bounds": {},
|
||||||
"player_spawn": [
|
"player_spawn": [
|
||||||
-260,
|
100,
|
||||||
500
|
180
|
||||||
],
|
],
|
||||||
"bounds": {
|
"entry_spawns": {
|
||||||
"kill_y": 2000
|
"room1_right": [
|
||||||
},
|
1040,
|
||||||
"platforms": {
|
680
|
||||||
"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
|
|
||||||
]
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"enemies": [],
|
"platforms": [
|
||||||
|
{
|
||||||
|
"x": 20,
|
||||||
|
"y": 260,
|
||||||
|
"w": 1000,
|
||||||
|
"h": 20,
|
||||||
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": [
|
"exits": [
|
||||||
{
|
{
|
||||||
"rect": [
|
"x": 1060,
|
||||||
-420,
|
"y": 660,
|
||||||
480,
|
"w": 80,
|
||||||
90,
|
"h": 100,
|
||||||
80
|
|
||||||
],
|
|
||||||
"to": "room_reward_01",
|
|
||||||
"spawn": [
|
|
||||||
120,
|
|
||||||
300
|
|
||||||
],
|
|
||||||
"hint": "Reward"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rect": [
|
|
||||||
1560,
|
|
||||||
100,
|
|
||||||
120,
|
|
||||||
120
|
|
||||||
],
|
|
||||||
"to": "room_02",
|
"to": "room_02",
|
||||||
|
"spawn_id": "room2_left",
|
||||||
"spawn": [
|
"spawn": [
|
||||||
-420,
|
120,
|
||||||
300
|
240
|
||||||
],
|
],
|
||||||
"hint": "",
|
"hint": "",
|
||||||
"spawn_id": "entry_1"
|
"layer": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"ability_doors": [],
|
"ability_doors": [],
|
||||||
"entry_spawns": {},
|
"room_w": 1600,
|
||||||
"props": [
|
"room_h": 900
|
||||||
{
|
|
||||||
"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
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
@@ -1,238 +1,258 @@
|
|||||||
{
|
{
|
||||||
|
"name": "room_02",
|
||||||
|
"display_name": "room_02",
|
||||||
"meta": {
|
"meta": {
|
||||||
"id": "room_02",
|
"id": "room_02",
|
||||||
"name": "Room 02 - First Enemies"
|
"name": "room_02"
|
||||||
},
|
},
|
||||||
|
"bounds": {},
|
||||||
"player_spawn": [
|
"player_spawn": [
|
||||||
-320,
|
820,
|
||||||
300
|
440
|
||||||
],
|
],
|
||||||
"bounds": {
|
"entry_spawns": {
|
||||||
"kill_y": 2000
|
"room2_bottom_left": [
|
||||||
},
|
|
||||||
"platforms": {
|
|
||||||
"ground": [
|
|
||||||
[
|
|
||||||
-500,
|
|
||||||
360,
|
|
||||||
2440,
|
|
||||||
660
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-760,
|
|
||||||
-580,
|
|
||||||
260,
|
|
||||||
1600
|
|
||||||
],
|
|
||||||
[
|
|
||||||
1940,
|
|
||||||
-580,
|
|
||||||
240,
|
|
||||||
1600
|
|
||||||
],
|
|
||||||
[
|
|
||||||
-760,
|
|
||||||
-820,
|
|
||||||
2940,
|
|
||||||
240
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"floating": [
|
|
||||||
[
|
|
||||||
260,
|
|
||||||
260,
|
|
||||||
140,
|
|
||||||
18
|
|
||||||
],
|
|
||||||
[
|
|
||||||
520,
|
|
||||||
220,
|
|
||||||
140,
|
|
||||||
18
|
|
||||||
],
|
|
||||||
[
|
|
||||||
760,
|
|
||||||
180,
|
|
||||||
140,
|
|
||||||
18
|
|
||||||
],
|
|
||||||
[
|
|
||||||
940,
|
|
||||||
120,
|
120,
|
||||||
170,
|
460
|
||||||
18
|
|
||||||
],
|
],
|
||||||
[
|
"room2_top_left": [
|
||||||
1200,
|
120,
|
||||||
160,
|
240
|
||||||
170,
|
|
||||||
18
|
|
||||||
],
|
],
|
||||||
[
|
"hazard_respawn_1": [
|
||||||
1460,
|
840,
|
||||||
100,
|
440
|
||||||
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
|
|
||||||
]
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"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": [
|
"enemies": [
|
||||||
{
|
{
|
||||||
|
"x": 1160,
|
||||||
|
"y": 320,
|
||||||
"type": "enemy",
|
"type": "enemy",
|
||||||
"pos": [
|
"layer": 0
|
||||||
240,
|
|
||||||
-200
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"x": 1160,
|
||||||
|
"y": 580,
|
||||||
"type": "enemy",
|
"type": "enemy",
|
||||||
"pos": [
|
"layer": 0
|
||||||
620,
|
}
|
||||||
140
|
],
|
||||||
]
|
"npcs": [],
|
||||||
},
|
"props": [
|
||||||
{
|
{
|
||||||
"type": "enemy",
|
"x": 720,
|
||||||
"pos": [
|
"y": 320,
|
||||||
860,
|
"type": "save_point",
|
||||||
-200
|
"radius": 90,
|
||||||
]
|
"object_id": "lamp"
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "enemy",
|
|
||||||
"pos": [
|
|
||||||
480,
|
|
||||||
-530
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "enemy",
|
|
||||||
"pos": [
|
|
||||||
1740,
|
|
||||||
280
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"exits": [
|
"exits": [
|
||||||
{
|
{
|
||||||
"rect": [
|
"x": 20,
|
||||||
-500,
|
"y": 220,
|
||||||
300,
|
"w": 80,
|
||||||
80,
|
"h": 100,
|
||||||
60
|
|
||||||
],
|
|
||||||
"to": "room_01",
|
"to": "room_01",
|
||||||
|
"spawn_id": "room1_right",
|
||||||
"spawn": [
|
"spawn": [
|
||||||
1560,
|
1040,
|
||||||
140
|
680
|
||||||
],
|
],
|
||||||
"hint": "Back"
|
"hint": "",
|
||||||
|
"layer": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rect": [
|
"x": 20,
|
||||||
1860,
|
"y": 440,
|
||||||
-500,
|
"w": 80,
|
||||||
80,
|
"h": 100,
|
||||||
120
|
"to": "room_03",
|
||||||
],
|
"spawn_id": "room3_top_right",
|
||||||
"to": "room_07",
|
|
||||||
"spawn": [
|
"spawn": [
|
||||||
180,
|
1480,
|
||||||
240
|
220
|
||||||
],
|
],
|
||||||
"hint": ""
|
"hint": "",
|
||||||
|
"layer": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"ability_doors": [],
|
"ability_doors": [],
|
||||||
"props": [
|
"room_w": 1600,
|
||||||
{
|
"room_h": 900
|
||||||
"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
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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;
|
--bg:#0b0f14; --panel:#111827; --panel2:#0f172a; --fg:#e5e7eb; --muted:#9ca3af;
|
||||||
--accent:#60a5fa; --accent2:#34d399; --warn:#f59e0b; --danger:#ef4444; --border:#1f2937;
|
--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;}
|
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:100vh;}
|
.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:auto;}
|
.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{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;}
|
.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;}
|
.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{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;}
|
.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);}
|
.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 */
|
/* Floating tool palette pinned to the right edge of the canvas area */
|
||||||
#toolbar{
|
#toolbar{
|
||||||
position:absolute; right:10px; top:70px; z-index:60;
|
position:absolute; right:10px; top:70px; z-index:60;
|
||||||
@@ -105,6 +105,16 @@
|
|||||||
Grid snap (20px) on placement / drag
|
Grid snap (20px) on placement / drag
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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;">
|
<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.
|
<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>
|
</div>
|
||||||
@@ -258,6 +268,7 @@
|
|||||||
house: { type: "house" },
|
house: { type: "house" },
|
||||||
diary: { type: "diary" },
|
diary: { type: "diary" },
|
||||||
player: { type: "player" },
|
player: { type: "player" },
|
||||||
|
quest_point: { type: "quest_giver" },
|
||||||
};
|
};
|
||||||
// Type fallbacks used when no object def provides a radius.
|
// Type fallbacks used when no object def provides a radius.
|
||||||
const TYPE_DEFAULT_RADII = { light_source: 180, save_point: 90 };
|
const TYPE_DEFAULT_RADII = { light_source: 180, save_point: 90 };
|
||||||
@@ -328,6 +339,21 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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){
|
function normalizeRoom(json, fallbackName, filename){
|
||||||
// Supports both:
|
// Supports both:
|
||||||
// A) editor-native rooms (name, platforms:[{x,y,w,h}], exits:[{x,y,w,h}...])
|
// A) editor-native rooms (name, platforms:[{x,y,w,h}], exits:[{x,y,w,h}...])
|
||||||
@@ -367,27 +393,23 @@
|
|||||||
const arr = platObj[group];
|
const arr = platObj[group];
|
||||||
if(!Array.isArray(arr)) continue;
|
if(!Array.isArray(arr)) continue;
|
||||||
for(const rect of arr){
|
for(const rect of arr){
|
||||||
if(!Array.isArray(rect) || rect.length < 4) continue;
|
const np = normPlatformEntry(rect, group);
|
||||||
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 });
|
if(np) r.platforms.push(np);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// if nothing came through, try also accepting editor-native arrays inside game rooms
|
// if nothing came through, try also accepting editor-native arrays inside game rooms
|
||||||
if(Array.isArray(src.platforms)){
|
if(Array.isArray(src.platforms)){
|
||||||
for(const p of src.platforms){
|
for(const p of src.platforms){
|
||||||
if(p && typeof p === "object"){
|
const np = normPlatformEntry(p, p && p.group ? p.group : "ground");
|
||||||
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) });
|
if(np) r.platforms.push(np);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// editor-native
|
// editor-native
|
||||||
if(Array.isArray(src.platforms)){
|
if(Array.isArray(src.platforms)){
|
||||||
for(const p of src.platforms){
|
for(const p of src.platforms){
|
||||||
if(p && typeof p === "object"){
|
const np = normPlatformEntry(p, p && p.group ? p.group : "ground");
|
||||||
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) });
|
if(np) r.platforms.push(np);
|
||||||
} 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 });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if(src.platforms && typeof src.platforms === "object"){
|
} else if(src.platforms && typeof src.platforms === "object"){
|
||||||
// allow grouped platforms even in editor rooms
|
// allow grouped platforms even in editor rooms
|
||||||
@@ -395,8 +417,8 @@
|
|||||||
const arr = src.platforms[group];
|
const arr = src.platforms[group];
|
||||||
if(!Array.isArray(arr)) continue;
|
if(!Array.isArray(arr)) continue;
|
||||||
for(const rect of arr){
|
for(const rect of arr){
|
||||||
if(!Array.isArray(rect) || rect.length < 4) continue;
|
const np = normPlatformEntry(rect, group);
|
||||||
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 });
|
if(np) r.platforms.push(np);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -444,7 +466,7 @@
|
|||||||
const type = p.type || p.kind || "light_source";
|
const type = p.type || p.kind || "light_source";
|
||||||
const object_id = p.object_id || p.objectId || null;
|
const object_id = p.object_id || p.objectId || null;
|
||||||
const radius = +p.radius || getDefaultRadiusForType(type, object_id);
|
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",
|
enemyPlaceType: "enemy",
|
||||||
npcPlaceType: "vendor",
|
npcPlaceType: "vendor",
|
||||||
showAggro: false,
|
showAggro: false,
|
||||||
objectPlaceId: null
|
objectPlaceId: null,
|
||||||
|
waitingPathPt: null
|
||||||
};
|
};
|
||||||
|
|
||||||
const els = {
|
const els = {
|
||||||
@@ -957,6 +980,24 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
|
|
||||||
const room = roomWrap.data;
|
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){
|
if(ev.button === 1){
|
||||||
// middle mouse pan handled by mousedown
|
// middle mouse pan handled by mousedown
|
||||||
return;
|
return;
|
||||||
@@ -1075,7 +1116,8 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
const [sx1,sy1]=snapPt(wx,wy);
|
const [sx1,sy1]=snapPt(wx,wy);
|
||||||
|
|
||||||
// Start as a 1x1 rect at the anchor; resizing can go in any direction.
|
// 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);
|
room.platforms.push(p);
|
||||||
|
|
||||||
state.selection = {kind:"platform", index: room.platforms.length-1, roomId: roomWrap.id};
|
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 = [];
|
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.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 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 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 });
|
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.strokeStyle = "rgba(96,165,250,0.75)";
|
||||||
ctx.lineWidth = 2*dpr;
|
ctx.lineWidth = 2*dpr;
|
||||||
ctx.strokeRect(sx,sy, sx2-sx, sy2-sy);
|
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"){
|
} else if(it.kind === "door"){
|
||||||
const d = it.d;
|
const d = it.d;
|
||||||
const [sx,sy] = worldToScreen(d.x, d.y, canvas);
|
const [sx,sy] = worldToScreen(d.x, d.y, canvas);
|
||||||
@@ -1807,18 +1937,10 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
const ex = exits[i];
|
const ex = exits[i];
|
||||||
if(!ex || ex.to !== dstWrap.data.name) continue;
|
if(!ex || ex.to !== dstWrap.data.name) continue;
|
||||||
|
|
||||||
// End the line at the corresponding DOOR in the compare room (the exit that
|
// End the line at the ENTRY point in the compare room (spawn / entry spawn),
|
||||||
// leads back to this room), so it lands accurately on the doorway.
|
// i.e. exit -> entry, not exit -> exit.
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Fallback: the exit's spawn / entry spawn in the compare room.
|
|
||||||
if(!pos){
|
|
||||||
const sid = ex.spawn_id || null;
|
const sid = ex.spawn_id || null;
|
||||||
|
let pos = null;
|
||||||
if(sid === "__player_spawn__"){
|
if(sid === "__player_spawn__"){
|
||||||
pos = dstWrap.data.player_spawn || null;
|
pos = dstWrap.data.player_spawn || null;
|
||||||
} else if(sid){
|
} else if(sid){
|
||||||
@@ -1827,7 +1949,6 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
if(!pos && Array.isArray(ex.spawn) && ex.spawn.length >= 2){
|
if(!pos && Array.isArray(ex.spawn) && ex.spawn.length >= 2){
|
||||||
pos = ex.spawn;
|
pos = ex.spawn;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if(!pos) continue;
|
if(!pos) continue;
|
||||||
|
|
||||||
// viewport device coords of both endpoints
|
// viewport device coords of both endpoints
|
||||||
@@ -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">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 class="mini">layer (lower = back)</div><input id="pl" type="number" value="${p.layer||0}"/></div>
|
||||||
</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;">
|
<div class="row" style="margin-top:8px;">
|
||||||
<button id="delSel" class="danger">Delete</button>
|
<button id="delSel" class="danger">Delete</button>
|
||||||
</div>
|
</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 class="mini">y</div><input id="npy" type="number" value="${n.y}"/></div>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top:8px;">
|
<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"}"/>
|
<input id="npt" type="text" value="${n.type||"vendor"}"/>
|
||||||
</div>
|
</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 style="margin-top:8px;">
|
||||||
<div class="mini">layer (lower = back)</div>
|
<div class="mini">layer (lower = back)</div>
|
||||||
<input id="npl" type="number" value="${n.layer||0}"/>
|
<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>";
|
if(!p) return "<div class='muted'>Selection missing.</div>";
|
||||||
const prDefault = getDefaultRadiusForType(p.type || "light_source", p.object_id || p.objectId || null);
|
const prDefault = getDefaultRadiusForType(p.type || "light_source", p.object_id || p.objectId || null);
|
||||||
const objId = p.object_id || p.objectId || "";
|
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 `
|
return `
|
||||||
<div><b>Prop</b> #${sel.index}${hint ? ` <span class="mini">· ${hint}</span>` : ""}</div>
|
<div><b>Prop</b> #${sel.index}${hint ? ` <span class="mini">· ${hint}</span>` : ""}</div>
|
||||||
<div class="hr"></div>
|
<div class="hr"></div>
|
||||||
@@ -2108,6 +2264,11 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
<div class="mini">type</div>
|
<div class="mini">type</div>
|
||||||
<input id="prtype" type="text" value="${p.type||"light_source"}"/>
|
<input id="prtype" type="text" value="${p.type||"light_source"}"/>
|
||||||
</div>
|
</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 style="margin-top:8px;">
|
||||||
<div class="mini">object id</div>
|
<div class="mini">object id</div>
|
||||||
<input id="probjid" type="text" value="${objId}" placeholder="(e.g. flow_butterfly, lamp, flow_butterfly_jar)"/>
|
<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 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(()=>{
|
const del=q("delSel"); if(del) del.addEventListener("click", ()=>pushAnd(()=>{
|
||||||
room.platforms.splice(sel.index,1);
|
room.platforms.splice(sel.index,1);
|
||||||
state.selection=null;
|
state.selection=null;
|
||||||
@@ -2250,10 +2472,14 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
}));
|
}));
|
||||||
} else if(sel.kind==="prop"){
|
} else if(sel.kind==="prop"){
|
||||||
const p = room.props && room.props[sel.index]; if(!p) return;
|
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(prx) prx.addEventListener("change", ()=>pushAnd(()=>{ p.x=num(prx.value); }));
|
||||||
if(pry) pry.addEventListener("change", ()=>pushAnd(()=>{ p.y=num(pry.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(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(()=>{
|
if(probjid) probjid.addEventListener("change", ()=>pushAnd(()=>{
|
||||||
const v = probjid.value.trim();
|
const v = probjid.value.trim();
|
||||||
if(v){ p.object_id = v; } else { delete p.object_id; }
|
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;
|
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){
|
if(ev.button === 1){
|
||||||
// middle mouse pan is handled by the mousedown listener (better cross-browser)
|
// middle mouse pan is handled by the mousedown listener (better cross-browser)
|
||||||
return;
|
return;
|
||||||
@@ -2390,7 +2634,8 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
const [sx1,sy1]=snapPt(wx,wy);
|
const [sx1,sy1]=snapPt(wx,wy);
|
||||||
|
|
||||||
// Start as a 1x1 rect at the anchor; resizing can go in any direction.
|
// 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);
|
room.platforms.push(p);
|
||||||
|
|
||||||
state.selection = {kind:"platform", index: room.platforms.length-1, roomId: roomWrap.id};
|
state.selection = {kind:"platform", index: room.platforms.length-1, roomId: roomWrap.id};
|
||||||
@@ -3000,15 +3245,28 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
const fmt = d._format || "editor_v1";
|
const fmt = d._format || "editor_v1";
|
||||||
|
|
||||||
if(fmt === "game_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 = {};
|
const platforms = {};
|
||||||
for(const p of (d.platforms || [])){
|
for(const p of (d.platforms || [])){
|
||||||
const group = p.group || "ground";
|
const group = p.group || "ground";
|
||||||
if(!platforms[group]) platforms[group] = [];
|
if(!platforms[group]) platforms[group] = [];
|
||||||
|
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];
|
const rect = [p.x, p.y, p.w, p.h];
|
||||||
if(p.layer) rect.push(p.layer);
|
if(p.layer) rect.push(p.layer);
|
||||||
platforms[group].push(rect);
|
platforms[group].push(rect);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// enemies back to {type, pos:[x,y]} (+ layer when set)
|
// enemies back to {type, pos:[x,y]} (+ layer when set)
|
||||||
const enemies = (d.enemies || []).map(e => {
|
const enemies = (d.enemies || []).map(e => {
|
||||||
@@ -3052,6 +3310,7 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
};
|
};
|
||||||
if(p.object_id) out.object_id = p.object_id;
|
if(p.object_id) out.object_id = p.object_id;
|
||||||
if(p.layer) out.layer = p.layer;
|
if(p.layer) out.layer = p.layer;
|
||||||
|
if(p.quest_id) out.quest_id = p.quest_id;
|
||||||
return out;
|
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
|
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
|
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 ability / projectile settings ---
|
||||||
RANGED_CHARGE_COST = 5.0 # costs more than melee (melee costs 1)
|
RANGED_CHARGE_COST = 5.0 # costs more than melee (melee costs 1)
|
||||||
RANGED_DAMAGE_MULT = 0.5 # 50% of melee damage
|
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):
|
if isinstance(platforms, list):
|
||||||
grouped = {"ground": [], "floating": []}
|
grouped = {"ground": [], "floating": []}
|
||||||
for p in platforms:
|
for p in platforms:
|
||||||
if isinstance(p, (list, tuple)) and len(p) == 4:
|
if isinstance(p, (list, tuple)) and len(p) >= 4:
|
||||||
grouped["ground"].append([p[0], p[1], p[2], p[3]])
|
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):
|
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")
|
group = p.get("group", "ground")
|
||||||
grouped.setdefault(group, [])
|
if group in ("moving", "trigger", "hazard"):
|
||||||
grouped[group].append(rect)
|
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
|
out["platforms"] = grouped
|
||||||
elif not isinstance(platforms, dict):
|
elif not isinstance(platforms, dict):
|
||||||
out["platforms"] = {"ground": [], "floating": []}
|
out["platforms"] = {"ground": [], "floating": []}
|
||||||
@@ -954,6 +977,20 @@ def normalize_room_json(room_json: dict) -> dict:
|
|||||||
norm_npcs.append(n)
|
norm_npcs.append(n)
|
||||||
out["npcs"] = norm_npcs
|
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 -> ensure rect:[x,y,w,h]
|
||||||
exits = out.get("exits", [])
|
exits = out.get("exits", [])
|
||||||
if isinstance(exits, list):
|
if isinstance(exits, list):
|
||||||
@@ -998,18 +1035,78 @@ class Room:
|
|||||||
self.kill_y = int((room_json.get("bounds", {}) or {}).get("kill_y", 1400))
|
self.kill_y = int((room_json.get("bounds", {}) or {}).get("kill_y", 1400))
|
||||||
|
|
||||||
self.platform_layer: dict[int, int] = {}
|
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 {}
|
platforms = room_json.get("platforms", {}) if isinstance(room_json, dict) else {}
|
||||||
if isinstance(platforms, list):
|
if isinstance(platforms, list):
|
||||||
self.platforms_ground = rects_from_list(platforms, self.platform_layer)
|
self.platforms_ground = rects_from_list(platforms, self.platform_layer)
|
||||||
self.platforms_floating = []
|
self.platforms_floating = []
|
||||||
|
self.platforms_hazard = []
|
||||||
|
self.platforms_trigger = []
|
||||||
|
self.platforms_moving = []
|
||||||
elif isinstance(platforms, dict):
|
elif isinstance(platforms, dict):
|
||||||
self.platforms_ground = rects_from_list(platforms.get("ground", []), self.platform_layer)
|
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_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:
|
else:
|
||||||
self.platforms_ground = []
|
self.platforms_ground = []
|
||||||
self.platforms_floating = []
|
self.platforms_floating = []
|
||||||
|
self.platforms_hazard = []
|
||||||
|
self.platforms_trigger = []
|
||||||
|
self.platforms_moving = []
|
||||||
self.ground_platforms = self.platforms_ground[:]
|
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 []
|
self.enemies = room_json.get("enemies", []) if isinstance(room_json, dict) else []
|
||||||
if not isinstance(self.enemies, list):
|
if not isinstance(self.enemies, list):
|
||||||
@@ -4222,7 +4319,10 @@ class Enemy:
|
|||||||
self.direction = -1 if dx > 0 else 1
|
self.direction = -1 if dx > 0 else 1
|
||||||
|
|
||||||
if chasing:
|
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
|
self.last_chase_dir = self.direction # remember chase intent
|
||||||
|
|
||||||
# aggro just broke (shade OR distance)
|
# aggro just broke (shade OR distance)
|
||||||
@@ -4431,7 +4531,11 @@ class Enemy:
|
|||||||
|
|
||||||
# --- PATROL / CHASE INTENT ---
|
# --- PATROL / CHASE INTENT ---
|
||||||
if chasing:
|
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:
|
elif self.stepoff_timer > 0.0:
|
||||||
# aggro just broke near an edge: keep walking off naturally
|
# aggro just broke near an edge: keep walking off naturally
|
||||||
self.direction = self.stepoff_dir
|
self.direction = self.stepoff_dir
|
||||||
@@ -4818,6 +4922,45 @@ class HouseObject:
|
|||||||
draw_house_shape(surface, cx, cy, self.x, self.y)
|
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):
|
def draw_layerable(surface, cx, cy):
|
||||||
"""Draw all static room objects back-to-front by (layer, category).
|
"""Draw all static room objects back-to-front by (layer, category).
|
||||||
|
|
||||||
@@ -4847,6 +4990,15 @@ def draw_layerable(surface, cx, cy):
|
|||||||
# platforms
|
# platforms
|
||||||
for p in platforms:
|
for p in platforms:
|
||||||
items.append((platform_layer.get(id(p), 0), 1, "platform", p))
|
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
|
# ability doors
|
||||||
for d in ability_doors:
|
for d in ability_doors:
|
||||||
items.append((getattr(d, "layer", 0), 2, "door", d))
|
items.append((getattr(d, "layer", 0), 2, "door", d))
|
||||||
@@ -4874,6 +5026,18 @@ def draw_layerable(surface, cx, cy):
|
|||||||
PLATFORM_COLOR,
|
PLATFORM_COLOR,
|
||||||
pygame.Rect(obj.x - cx, obj.y - cy, obj.width, obj.height),
|
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":
|
elif kind == "exit":
|
||||||
rr = obj["rect"]
|
rr = obj["rect"]
|
||||||
pygame.draw.rect(
|
pygame.draw.rect(
|
||||||
@@ -5114,6 +5278,10 @@ ROOM = room_manager.current_room
|
|||||||
KILL_Y = ROOM.kill_y
|
KILL_Y = ROOM.kill_y
|
||||||
platforms_ground = ROOM.platforms_ground
|
platforms_ground = ROOM.platforms_ground
|
||||||
platforms_floating = ROOM.platforms_floating
|
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
|
ground_platforms = ROOM.ground_platforms
|
||||||
platforms = ROOM.platforms
|
platforms = ROOM.platforms
|
||||||
platform_layer = ROOM.platform_layer
|
platform_layer = ROOM.platform_layer
|
||||||
@@ -5155,10 +5323,8 @@ class QuestGiver:
|
|||||||
self.bob_t = random.random() * 10.0
|
self.bob_t = random.random() * 10.0
|
||||||
|
|
||||||
def draw(self, surface, cx, cy):
|
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)
|
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, (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)
|
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):
|
def apply_room(room: Room, set_player_spawn=True):
|
||||||
"""Updates room-dependent lists + respawns enemies. Keeps inventory/equipment."""
|
"""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 platform_layer
|
||||||
global enemies, npcs, loot_drops, shockwaves, coin_drops, projectiles, ability_doors
|
global enemies, npcs, loot_drops, shockwaves, coin_drops, projectiles, ability_doors
|
||||||
global shatter_platforms, shatter_aoes, tether_shots
|
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
|
KILL_Y = ROOM.kill_y
|
||||||
platforms_ground = ROOM.platforms_ground
|
platforms_ground = ROOM.platforms_ground
|
||||||
platforms_floating = ROOM.platforms_floating
|
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
|
ground_platforms = ROOM.ground_platforms
|
||||||
platforms = ROOM.platforms
|
platforms = ROOM.platforms
|
||||||
platform_layer = ROOM.platform_layer
|
platform_layer = ROOM.platform_layer
|
||||||
@@ -5941,10 +6112,8 @@ class DiaryObject:
|
|||||||
self.bob_t = random.random() * 10.0
|
self.bob_t = random.random() * 10.0
|
||||||
|
|
||||||
def draw(self, surface, cx, cy):
|
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)
|
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, (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.rect(surface, (90, 60, 30), (x, y, self.rect.width, self.rect.height), 2)
|
||||||
pygame.draw.line(surface, (90, 60, 30),
|
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):
|
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
|
global LANTERN_RESPAWN_ROOM, LANTERN_RESPAWN_POS
|
||||||
LANTERN_RESPAWN_ROOM = ROOM.id
|
LANTERN_RESPAWN_ROOM = ROOM.id
|
||||||
LANTERN_RESPAWN_POS = (sp.x, sp.y)
|
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()
|
_do_save_game()
|
||||||
pickup_feed.push_center("Lantern - game saved", duration=2.0)
|
pickup_feed.push_center("Lantern - game saved, fully restored", duration=2.0)
|
||||||
# Butterfly jars lost on death respawn after saving at a lantern in a different room.
|
|
||||||
if respawn_jars_if_elsewhere():
|
if respawn_jars_if_elsewhere():
|
||||||
pickup_feed.push_center("Butterfly jars have respawned!", duration=2.5)
|
pickup_feed.push_center("Butterfly jars have respawned!", duration=2.5)
|
||||||
|
|
||||||
@@ -7231,6 +7406,9 @@ while running:
|
|||||||
near_save_point = True
|
near_save_point = True
|
||||||
break
|
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]
|
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]
|
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)
|
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 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:
|
if (not player.shatterstep_platform_spawned) and player.shatterstep_platform_cd <= 0.0:
|
||||||
jump_v0 = abs(JUMP_LIGHT)
|
jump_v0 = abs(JUMP_LIGHT)
|
||||||
@@ -7301,7 +7495,7 @@ while running:
|
|||||||
player.shatterstep_landing_pending = False
|
player.shatterstep_landing_pending = False
|
||||||
player.shatterstep_landing_damage = 0.0
|
player.shatterstep_landing_damage = 0.0
|
||||||
else:
|
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:
|
if LANTERN_RESPAWN_ROOM and LANTERN_RESPAWN_POS:
|
||||||
lx, ly = LANTERN_RESPAWN_POS
|
lx, ly = LANTERN_RESPAWN_POS
|
||||||
if LANTERN_RESPAWN_ROOM != ROOM.id:
|
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,
|
"version": 1,
|
||||||
"saved_at": "2026-09-29T12:12:10",
|
"saved_at": "2026-09-29T12:14:42",
|
||||||
"player": {
|
"player": {
|
||||||
"room_id": "room_town_gloomreach.json",
|
"room_id": "room_town_gloomreach.json",
|
||||||
"x": 1315,
|
"x": 1250,
|
||||||
"y": 360,
|
"y": 360,
|
||||||
"health": 10,
|
"health": 12,
|
||||||
"charge": 125.0,
|
"charge": 125.0,
|
||||||
"shade": false,
|
"shade": false,
|
||||||
"coins_light": 0,
|
"coins_light": 0,
|
||||||
|
|||||||
Reference in New Issue
Block a user