more rooms
more rooms, level editor changes, more logic for ability checks and trigger doors, platform body collision fixes
This commit is contained in:
@@ -5,7 +5,8 @@
|
|||||||
"jump_light": -12.0,
|
"jump_light": -12.0,
|
||||||
"jump_shade": -9.0,
|
"jump_shade": -9.0,
|
||||||
"gravity_light": 0.5,
|
"gravity_light": 0.5,
|
||||||
"gravity_shade": 0.4
|
"gravity_shade": 0.4,
|
||||||
|
"shade_door_grace": 0.6
|
||||||
},
|
},
|
||||||
|
|
||||||
"resources": {
|
"resources": {
|
||||||
|
|||||||
@@ -93,49 +93,56 @@
|
|||||||
"w": 20,
|
"w": 20,
|
||||||
"h": 160,
|
"h": 160,
|
||||||
"group": "trigger",
|
"group": "trigger",
|
||||||
"layer": 0
|
"layer": 0,
|
||||||
|
"quest": "quest_first_kills"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 1100,
|
"x": 1100,
|
||||||
"y": 620,
|
"y": 620,
|
||||||
"w": 320,
|
"w": 320,
|
||||||
"h": 20,
|
"h": 20,
|
||||||
"group": "ground"
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 800,
|
"x": 800,
|
||||||
"y": 320,
|
"y": 320,
|
||||||
"w": 160,
|
"w": 160,
|
||||||
"h": 20,
|
"h": 20,
|
||||||
"group": "ground"
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 1580,
|
"x": 1580,
|
||||||
"y": 320,
|
"y": 320,
|
||||||
"w": 20,
|
"w": 20,
|
||||||
"h": 580,
|
"h": 580,
|
||||||
"group": "ground"
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 1220,
|
"x": 1220,
|
||||||
"y": 760,
|
"y": 760,
|
||||||
"w": 360,
|
"w": 360,
|
||||||
"h": 20,
|
"h": 20,
|
||||||
"group": "ground"
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 880,
|
"y": 880,
|
||||||
"w": 1560,
|
"w": 1560,
|
||||||
"h": 20,
|
"h": 20,
|
||||||
"group": "ground"
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 640,
|
"y": 640,
|
||||||
"w": 20,
|
"w": 20,
|
||||||
"h": 240,
|
"h": 240,
|
||||||
"group": "ground"
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 160,
|
"x": 160,
|
||||||
@@ -143,28 +150,33 @@
|
|||||||
"w": 20,
|
"w": 20,
|
||||||
"h": 240,
|
"h": 240,
|
||||||
"group": "trigger",
|
"group": "trigger",
|
||||||
"name": "first_quest_complete"
|
"layer": 0,
|
||||||
|
"name": "first_quest_complete",
|
||||||
|
"quest_turned_in": "quest_first_kills"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 580,
|
"x": 580,
|
||||||
"y": 640,
|
"y": 640,
|
||||||
"w": 20,
|
"w": 20,
|
||||||
"h": 120,
|
"h": 120,
|
||||||
"group": "ground"
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 480,
|
"x": 480,
|
||||||
"y": 740,
|
"y": 740,
|
||||||
"w": 100,
|
"w": 100,
|
||||||
"h": 20,
|
"h": 20,
|
||||||
"group": "ground"
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 400,
|
"x": 400,
|
||||||
"y": 400,
|
"y": 400,
|
||||||
"w": 100,
|
"w": 100,
|
||||||
"h": 20,
|
"h": 20,
|
||||||
"group": "ground"
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 220,
|
"x": 220,
|
||||||
@@ -172,6 +184,7 @@
|
|||||||
"w": 80,
|
"w": 80,
|
||||||
"h": 20,
|
"h": 20,
|
||||||
"group": "moving",
|
"group": "moving",
|
||||||
|
"layer": 0,
|
||||||
"path": [
|
"path": [
|
||||||
[
|
[
|
||||||
220,
|
220,
|
||||||
@@ -188,41 +201,48 @@
|
|||||||
"y": 220,
|
"y": 220,
|
||||||
"w": 140,
|
"w": 140,
|
||||||
"h": 20,
|
"h": 20,
|
||||||
"group": "ground"
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 1520,
|
"x": 1520,
|
||||||
"y": 640,
|
"y": 640,
|
||||||
"w": 60,
|
"w": 60,
|
||||||
"h": 20,
|
"h": 20,
|
||||||
"group": "ground"
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"enemies": [
|
"enemies": [
|
||||||
{
|
{
|
||||||
"x": 600,
|
"x": 600,
|
||||||
"y": 840,
|
"y": 840,
|
||||||
"type": "enemy"
|
"type": "enemy",
|
||||||
|
"layer": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 960,
|
"x": 960,
|
||||||
"y": 840,
|
"y": 840,
|
||||||
"type": "enemy"
|
"type": "enemy",
|
||||||
|
"layer": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 200,
|
"x": 200,
|
||||||
"y": 580,
|
"y": 580,
|
||||||
"type": "enemy"
|
"type": "enemy",
|
||||||
|
"layer": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 520,
|
"x": 520,
|
||||||
"y": 700,
|
"y": 700,
|
||||||
"type": "enemy"
|
"type": "enemy",
|
||||||
|
"layer": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 1520,
|
"x": 1520,
|
||||||
"y": 840,
|
"y": 840,
|
||||||
"type": "enemy"
|
"type": "enemy",
|
||||||
|
"layer": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"npcs": [],
|
"npcs": [],
|
||||||
@@ -235,6 +255,28 @@
|
|||||||
"object_id": "quest_point",
|
"object_id": "quest_point",
|
||||||
"layer": 0,
|
"layer": 0,
|
||||||
"quest_id": "quest_first_kills"
|
"quest_id": "quest_first_kills"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 90,
|
||||||
|
"y": 194,
|
||||||
|
"type": "reward_chest",
|
||||||
|
"radius": 180,
|
||||||
|
"object_id": null,
|
||||||
|
"layer": 0,
|
||||||
|
"quest_id": "",
|
||||||
|
"coins": [
|
||||||
|
10,
|
||||||
|
2
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 880,
|
||||||
|
"y": 320,
|
||||||
|
"type": "light_source",
|
||||||
|
"radius": 180,
|
||||||
|
"object_id": "flow_butterfly",
|
||||||
|
"layer": 0,
|
||||||
|
"quest_id": ""
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"exits": [
|
"exits": [
|
||||||
@@ -257,12 +299,14 @@
|
|||||||
"y": 780,
|
"y": 780,
|
||||||
"w": 80,
|
"w": 80,
|
||||||
"h": 100,
|
"h": 100,
|
||||||
"to": "",
|
"to": "room_04",
|
||||||
"spawn_id": "",
|
"spawn_id": "room4_top_right",
|
||||||
"spawn": [
|
"spawn": [
|
||||||
120,
|
1480,
|
||||||
780
|
100
|
||||||
]
|
],
|
||||||
|
"hint": "",
|
||||||
|
"layer": 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"ability_doors": [],
|
"ability_doors": [],
|
||||||
|
|||||||
@@ -0,0 +1,308 @@
|
|||||||
|
{
|
||||||
|
"name": "room_04",
|
||||||
|
"display_name": "room_04",
|
||||||
|
"meta": {
|
||||||
|
"id": "room_04",
|
||||||
|
"name": "room_04"
|
||||||
|
},
|
||||||
|
"bounds": {},
|
||||||
|
"player_spawn": [
|
||||||
|
1440,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"entry_spawns": {
|
||||||
|
"room4_top_right": [
|
||||||
|
1480,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"hazard_respawn": [
|
||||||
|
1080,
|
||||||
|
180
|
||||||
|
],
|
||||||
|
"room4_bottom_left": [
|
||||||
|
120,
|
||||||
|
620
|
||||||
|
],
|
||||||
|
"room4_bottom_right": [
|
||||||
|
1480,
|
||||||
|
640
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"platforms": [
|
||||||
|
{
|
||||||
|
"x": 1340,
|
||||||
|
"y": 160,
|
||||||
|
"w": 260,
|
||||||
|
"h": 20,
|
||||||
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 160,
|
||||||
|
"y": 0,
|
||||||
|
"w": 1440,
|
||||||
|
"h": 20,
|
||||||
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1580,
|
||||||
|
"y": 20,
|
||||||
|
"w": 20,
|
||||||
|
"h": 140,
|
||||||
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1340,
|
||||||
|
"y": 180,
|
||||||
|
"w": 20,
|
||||||
|
"h": 60,
|
||||||
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1020,
|
||||||
|
"y": 240,
|
||||||
|
"w": 340,
|
||||||
|
"h": 20,
|
||||||
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 920,
|
||||||
|
"y": 240,
|
||||||
|
"w": 100,
|
||||||
|
"h": 20,
|
||||||
|
"group": "hazard",
|
||||||
|
"layer": 0,
|
||||||
|
"respawn": [
|
||||||
|
1080,
|
||||||
|
180
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 460,
|
||||||
|
"y": 240,
|
||||||
|
"w": 460,
|
||||||
|
"h": 20,
|
||||||
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 900,
|
||||||
|
"y": 260,
|
||||||
|
"w": 140,
|
||||||
|
"h": 20,
|
||||||
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 160,
|
||||||
|
"y": 20,
|
||||||
|
"w": 20,
|
||||||
|
"h": 520,
|
||||||
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 20,
|
||||||
|
"y": 700,
|
||||||
|
"w": 1580,
|
||||||
|
"h": 20,
|
||||||
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 0,
|
||||||
|
"y": 520,
|
||||||
|
"w": 160,
|
||||||
|
"h": 20,
|
||||||
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 0,
|
||||||
|
"y": 540,
|
||||||
|
"w": 20,
|
||||||
|
"h": 180,
|
||||||
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 240,
|
||||||
|
"y": 280,
|
||||||
|
"w": 180,
|
||||||
|
"h": 20,
|
||||||
|
"group": "moving",
|
||||||
|
"layer": 0,
|
||||||
|
"path": [
|
||||||
|
[
|
||||||
|
240,
|
||||||
|
280
|
||||||
|
],
|
||||||
|
[
|
||||||
|
240,
|
||||||
|
640
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 460,
|
||||||
|
"y": 260,
|
||||||
|
"w": 20,
|
||||||
|
"h": 280,
|
||||||
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 480,
|
||||||
|
"y": 300,
|
||||||
|
"w": 880,
|
||||||
|
"h": 20,
|
||||||
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1340,
|
||||||
|
"y": 320,
|
||||||
|
"w": 20,
|
||||||
|
"h": 220,
|
||||||
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 460,
|
||||||
|
"y": 540,
|
||||||
|
"w": 20,
|
||||||
|
"h": 160,
|
||||||
|
"group": "trigger",
|
||||||
|
"layer": 0,
|
||||||
|
"name": "close_when_pass",
|
||||||
|
"lock_when_pass": true,
|
||||||
|
"open_on_miniboss_dead": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1340,
|
||||||
|
"y": 540,
|
||||||
|
"w": 20,
|
||||||
|
"h": 160,
|
||||||
|
"group": "trigger",
|
||||||
|
"layer": 0,
|
||||||
|
"name": "open_miniboss_dead",
|
||||||
|
"open_on_miniboss_dead": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1360,
|
||||||
|
"y": 520,
|
||||||
|
"w": 240,
|
||||||
|
"h": 20,
|
||||||
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1580,
|
||||||
|
"y": 540,
|
||||||
|
"w": 20,
|
||||||
|
"h": 160,
|
||||||
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 720,
|
||||||
|
"y": 620,
|
||||||
|
"w": 60,
|
||||||
|
"h": 20,
|
||||||
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 820,
|
||||||
|
"y": 540,
|
||||||
|
"w": 60,
|
||||||
|
"h": 20,
|
||||||
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"enemies": [
|
||||||
|
{
|
||||||
|
"x": 760,
|
||||||
|
"y": 200,
|
||||||
|
"type": "enemy",
|
||||||
|
"layer": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 880,
|
||||||
|
"y": 640,
|
||||||
|
"type": "miniboss",
|
||||||
|
"layer": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"npcs": [],
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"x": 560,
|
||||||
|
"y": 240,
|
||||||
|
"type": "save_point",
|
||||||
|
"radius": 90,
|
||||||
|
"object_id": "lamp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"exits": [
|
||||||
|
{
|
||||||
|
"x": 1500,
|
||||||
|
"y": 60,
|
||||||
|
"w": 80,
|
||||||
|
"h": 100,
|
||||||
|
"to": "room_03",
|
||||||
|
"spawn_id": "room3_bottom_left",
|
||||||
|
"spawn": [
|
||||||
|
140,
|
||||||
|
800
|
||||||
|
],
|
||||||
|
"hint": "",
|
||||||
|
"layer": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 20,
|
||||||
|
"y": 600,
|
||||||
|
"w": 80,
|
||||||
|
"h": 100,
|
||||||
|
"to": "room_06",
|
||||||
|
"spawn_id": "room6_top_right",
|
||||||
|
"spawn": [
|
||||||
|
520,
|
||||||
|
240
|
||||||
|
],
|
||||||
|
"hint": "",
|
||||||
|
"layer": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1500,
|
||||||
|
"y": 600,
|
||||||
|
"w": 80,
|
||||||
|
"h": 100,
|
||||||
|
"to": "room_05",
|
||||||
|
"spawn_id": "room5_top_left",
|
||||||
|
"spawn": [
|
||||||
|
120,
|
||||||
|
120
|
||||||
|
],
|
||||||
|
"hint": "",
|
||||||
|
"layer": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ability_doors": [
|
||||||
|
{
|
||||||
|
"x": 160,
|
||||||
|
"y": 540,
|
||||||
|
"w": 20,
|
||||||
|
"h": 160,
|
||||||
|
"layer": 0,
|
||||||
|
"kind": "shade"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"room_w": 1600,
|
||||||
|
"room_h": 900
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
{
|
||||||
|
"name": "room_05",
|
||||||
|
"display_name": "room_05",
|
||||||
|
"meta": {
|
||||||
|
"id": "room_05",
|
||||||
|
"name": "room_05"
|
||||||
|
},
|
||||||
|
"bounds": {},
|
||||||
|
"player_spawn": [
|
||||||
|
140,
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"entry_spawns": {
|
||||||
|
"room5_top_left": [
|
||||||
|
120,
|
||||||
|
120
|
||||||
|
],
|
||||||
|
"room5_bottom_right": [
|
||||||
|
1400,
|
||||||
|
440
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"platforms": [
|
||||||
|
{
|
||||||
|
"x": 0,
|
||||||
|
"y": 180,
|
||||||
|
"w": 240,
|
||||||
|
"h": 20,
|
||||||
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 20,
|
||||||
|
"h": 180,
|
||||||
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 20,
|
||||||
|
"y": 0,
|
||||||
|
"w": 1520,
|
||||||
|
"h": 20,
|
||||||
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 340,
|
||||||
|
"y": 180,
|
||||||
|
"w": 240,
|
||||||
|
"h": 20,
|
||||||
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 700,
|
||||||
|
"y": 180,
|
||||||
|
"w": 200,
|
||||||
|
"h": 20,
|
||||||
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 860,
|
||||||
|
"y": 500,
|
||||||
|
"w": 680,
|
||||||
|
"h": 20,
|
||||||
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1520,
|
||||||
|
"y": 380,
|
||||||
|
"w": 20,
|
||||||
|
"h": 120,
|
||||||
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1320,
|
||||||
|
"y": 360,
|
||||||
|
"w": 220,
|
||||||
|
"h": 20,
|
||||||
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1520,
|
||||||
|
"y": 20,
|
||||||
|
"w": 20,
|
||||||
|
"h": 340,
|
||||||
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"enemies": [
|
||||||
|
{
|
||||||
|
"x": 520,
|
||||||
|
"y": 140,
|
||||||
|
"type": "enemy",
|
||||||
|
"layer": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1000,
|
||||||
|
"y": 460,
|
||||||
|
"type": "enemy",
|
||||||
|
"layer": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"npcs": [],
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"x": 1400,
|
||||||
|
"y": 360,
|
||||||
|
"type": "save_point",
|
||||||
|
"radius": 90,
|
||||||
|
"object_id": "lamp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"exits": [
|
||||||
|
{
|
||||||
|
"x": 20,
|
||||||
|
"y": 80,
|
||||||
|
"w": 80,
|
||||||
|
"h": 100,
|
||||||
|
"to": "room_06",
|
||||||
|
"spawn_id": "room4_bottom_right",
|
||||||
|
"spawn": [
|
||||||
|
1480,
|
||||||
|
640
|
||||||
|
],
|
||||||
|
"hint": "",
|
||||||
|
"layer": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1440,
|
||||||
|
"y": 400,
|
||||||
|
"w": 80,
|
||||||
|
"h": 100,
|
||||||
|
"to": "",
|
||||||
|
"spawn_id": "",
|
||||||
|
"spawn": [
|
||||||
|
1440,
|
||||||
|
400
|
||||||
|
],
|
||||||
|
"hint": "",
|
||||||
|
"layer": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ability_doors": [],
|
||||||
|
"room_w": 1600,
|
||||||
|
"room_h": 900
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
{
|
||||||
|
"name": "room_06",
|
||||||
|
"display_name": "room_06",
|
||||||
|
"meta": {
|
||||||
|
"id": "room_06",
|
||||||
|
"name": "room_06"
|
||||||
|
},
|
||||||
|
"bounds": {},
|
||||||
|
"player_spawn": [
|
||||||
|
480,
|
||||||
|
240
|
||||||
|
],
|
||||||
|
"entry_spawns": {
|
||||||
|
"room6_top_right": [
|
||||||
|
520,
|
||||||
|
240
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"platforms": [
|
||||||
|
{
|
||||||
|
"x": 0,
|
||||||
|
"y": 300,
|
||||||
|
"w": 660,
|
||||||
|
"h": 20,
|
||||||
|
"group": "ground"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 420,
|
||||||
|
"y": 180,
|
||||||
|
"w": 240,
|
||||||
|
"h": 20,
|
||||||
|
"group": "ground"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 640,
|
||||||
|
"y": 200,
|
||||||
|
"w": 20,
|
||||||
|
"h": 100,
|
||||||
|
"group": "ground"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 420,
|
||||||
|
"y": 0,
|
||||||
|
"w": 20,
|
||||||
|
"h": 180,
|
||||||
|
"group": "ground"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 420,
|
||||||
|
"h": 20,
|
||||||
|
"group": "ground"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 0,
|
||||||
|
"y": 20,
|
||||||
|
"w": 20,
|
||||||
|
"h": 280,
|
||||||
|
"group": "ground"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"enemies": [],
|
||||||
|
"npcs": [],
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"x": 140,
|
||||||
|
"y": 260,
|
||||||
|
"type": "quest_giver",
|
||||||
|
"radius": 180,
|
||||||
|
"object_id": "quest_point",
|
||||||
|
"quest_id": "first_kills_shade"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"exits": [
|
||||||
|
{
|
||||||
|
"x": 560,
|
||||||
|
"y": 200,
|
||||||
|
"w": 80,
|
||||||
|
"h": 100,
|
||||||
|
"to": "room_04",
|
||||||
|
"spawn_id": "room4_bottom_left",
|
||||||
|
"spawn": [
|
||||||
|
120,
|
||||||
|
620
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ability_doors": [],
|
||||||
|
"room_w": 1600,
|
||||||
|
"room_h": 900
|
||||||
|
}
|
||||||
@@ -350,6 +350,9 @@
|
|||||||
if(rect.name) out.name = String(rect.name);
|
if(rect.name) out.name = String(rect.name);
|
||||||
if(rect.quest) out.quest = String(rect.quest);
|
if(rect.quest) out.quest = String(rect.quest);
|
||||||
if(rect.quest_complete) out.quest_complete = String(rect.quest_complete);
|
if(rect.quest_complete) out.quest_complete = String(rect.quest_complete);
|
||||||
|
if(rect.quest_turned_in) out.quest_turned_in = String(rect.quest_turned_in);
|
||||||
|
if(rect.lock_when_pass) out.lock_when_pass = true;
|
||||||
|
if(rect.open_on_miniboss_dead) out.open_on_miniboss_dead = true;
|
||||||
if(Array.isArray(rect.respawn)) out.respawn = deepClone(rect.respawn);
|
if(Array.isArray(rect.respawn)) out.respawn = deepClone(rect.respawn);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@@ -505,13 +508,14 @@
|
|||||||
if(Array.isArray(src.ability_doors)){
|
if(Array.isArray(src.ability_doors)){
|
||||||
for(const d of src.ability_doors){
|
for(const d of src.ability_doors){
|
||||||
if(!d) continue;
|
if(!d) continue;
|
||||||
|
const kind = (typeof d === "object" && d.kind === "shade") ? "shade" : "ranged";
|
||||||
if(Array.isArray(d) && d.length >= 4){
|
if(Array.isArray(d) && d.length >= 4){
|
||||||
r.ability_doors.push({ x:+d[0]||0, y:+d[1]||0, w:+d[2]||0, h:+d[3]||0, layer: d.length>=5 ? (+d[4]||0) : 0 });
|
r.ability_doors.push({ x:+d[0]||0, y:+d[1]||0, w:+d[2]||0, h:+d[3]||0, layer: d.length>=5 ? (+d[4]||0) : 0, kind });
|
||||||
} else if(typeof d === "object"){
|
} else if(typeof d === "object"){
|
||||||
if(Array.isArray(d.rect)){
|
if(Array.isArray(d.rect)){
|
||||||
r.ability_doors.push({ x:+d.rect[0]||0, y:+d.rect[1]||0, w:+d.rect[2]||0, h:+d.rect[3]||0, layer: +(d.layer||0) });
|
r.ability_doors.push({ x:+d.rect[0]||0, y:+d.rect[1]||0, w:+d.rect[2]||0, h:+d.rect[3]||0, layer: +(d.layer||0), kind });
|
||||||
} else {
|
} else {
|
||||||
r.ability_doors.push({ x:+d.x||0, y:+d.y||0, w:+d.w||0, h:+d.h||0, layer: +(d.layer||0) });
|
r.ability_doors.push({ x:+d.x||0, y:+d.y||0, w:+d.w||0, h:+d.h||0, layer: +(d.layer||0), kind });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1645,11 +1649,19 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
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);
|
||||||
const [sx2,sy2] = worldToScreen(d.x+d.w, d.y+d.h, canvas);
|
const [sx2,sy2] = worldToScreen(d.x+d.w, d.y+d.h, canvas);
|
||||||
ctx.fillStyle = "rgba(16,185,129,0.18)";
|
if(d.kind === "shade"){
|
||||||
ctx.fillRect(sx,sy, sx2-sx, sy2-sy);
|
ctx.fillStyle = "rgba(129,110,190,0.20)";
|
||||||
ctx.strokeStyle = "rgba(16,185,129,0.75)";
|
ctx.fillRect(sx,sy, sx2-sx, sy2-sy);
|
||||||
ctx.lineWidth = 2*dpr;
|
ctx.strokeStyle = "rgba(129,110,190,0.80)";
|
||||||
ctx.strokeRect(sx,sy, sx2-sx, sy2-sy);
|
ctx.lineWidth = 2*dpr;
|
||||||
|
ctx.strokeRect(sx,sy, sx2-sx, sy2-sy);
|
||||||
|
} else {
|
||||||
|
ctx.fillStyle = "rgba(16,185,129,0.18)";
|
||||||
|
ctx.fillRect(sx,sy, sx2-sx, sy2-sy);
|
||||||
|
ctx.strokeStyle = "rgba(16,185,129,0.75)";
|
||||||
|
ctx.lineWidth = 2*dpr;
|
||||||
|
ctx.strokeRect(sx,sy, sx2-sx, sy2-sy);
|
||||||
|
}
|
||||||
} else if(it.kind === "exit"){
|
} else if(it.kind === "exit"){
|
||||||
const ex = it.ex;
|
const ex = it.ex;
|
||||||
const [sx,sy] = worldToScreen(ex.x, ex.y, canvas);
|
const [sx,sy] = worldToScreen(ex.x, ex.y, canvas);
|
||||||
@@ -1948,7 +1960,10 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
} else if(sid){
|
} else if(sid){
|
||||||
pos = (dstWrap.data.entry_spawns && dstWrap.data.entry_spawns[sid]) ? dstWrap.data.entry_spawns[sid] : null;
|
pos = (dstWrap.data.entry_spawns && dstWrap.data.entry_spawns[sid]) ? dstWrap.data.entry_spawns[sid] : null;
|
||||||
}
|
}
|
||||||
if(!pos && Array.isArray(ex.spawn) && ex.spawn.length >= 2){
|
// Only fall back to the raw spawn coordinate when the exit has NO spawn_id.
|
||||||
|
// A stale spawn_id (e.g. from a target room that doesn't define it) would
|
||||||
|
// point the line at an arbitrary spot, so skip those broken links instead.
|
||||||
|
if(!pos && !sid && Array.isArray(ex.spawn) && ex.spawn.length >= 2){
|
||||||
pos = ex.spawn;
|
pos = ex.spawn;
|
||||||
}
|
}
|
||||||
if(!pos) continue;
|
if(!pos) continue;
|
||||||
@@ -2143,6 +2158,16 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
<div style="margin-top:8px;">
|
<div style="margin-top:8px;">
|
||||||
<div class="mini">unlock when quest complete (id)</div>
|
<div class="mini">unlock when quest complete (id)</div>
|
||||||
<input id="ptqcomplete" type="text" value="${p.quest_complete||""}" placeholder="e.g. quest_first_kills"/>
|
<input id="ptqcomplete" type="text" value="${p.quest_complete||""}" placeholder="e.g. quest_first_kills"/>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:8px;">
|
||||||
|
<div class="mini">unlock when quest turned in (id)</div>
|
||||||
|
<input id="ptqturnedin" type="text" value="${p.quest_turned_in||""}" placeholder="e.g. quest_first_kills"/>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:8px;">
|
||||||
|
<label><input id="ptlockpass" type="checkbox" ${p.lock_when_pass?"checked":""}/> Appear once the player walks through (locks them in)</label>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:8px;">
|
||||||
|
<label><input id="ptminibossdead" type="checkbox" ${p.open_on_miniboss_dead?"checked":""}/> Present until the room's miniboss is slain</label>
|
||||||
</div>` : ""}
|
</div>` : ""}
|
||||||
${(p.group||"ground") === "hazard" ? `
|
${(p.group||"ground") === "hazard" ? `
|
||||||
<div style="margin-top:8px;">
|
<div style="margin-top:8px;">
|
||||||
@@ -2204,6 +2229,13 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
<div><div class="mini">h</div><input id="adh" type="number" value="${d.h}"/></div>
|
<div><div class="mini">h</div><input id="adh" type="number" value="${d.h}"/></div>
|
||||||
<div><div class="mini">layer (lower = back)</div><input id="adlayer" type="number" value="${d.layer||0}"/></div>
|
<div><div class="mini">layer (lower = back)</div><input id="adlayer" type="number" value="${d.layer||0}"/></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="margin-top:8px;">
|
||||||
|
<div class="mini">type</div>
|
||||||
|
<select id="adkind">
|
||||||
|
<option value="ranged" ${(d.kind||"ranged")==="ranged"?"selected":""}>Ranged door (broken by the Ranged bolt)</option>
|
||||||
|
<option value="shade" ${d.kind==="shade"?"selected":""}>Shade door (passable in shade form, solid in light)</option>
|
||||||
|
</select>
|
||||||
|
</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>
|
||||||
@@ -2376,6 +2408,16 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
const v = ptqcomplete.value.trim();
|
const v = ptqcomplete.value.trim();
|
||||||
if(v){ p.quest_complete = v; } else { delete p.quest_complete; }
|
if(v){ p.quest_complete = v; } else { delete p.quest_complete; }
|
||||||
}));
|
}));
|
||||||
|
const ptqturnedin=q("ptqturnedin"); if(ptqturnedin) ptqturnedin.addEventListener("change", ()=>pushAnd(()=>{
|
||||||
|
const v = ptqturnedin.value.trim();
|
||||||
|
if(v){ p.quest_turned_in = v; } else { delete p.quest_turned_in; }
|
||||||
|
}));
|
||||||
|
const ptlockpass=q("ptlockpass"); if(ptlockpass) ptlockpass.addEventListener("change", ()=>pushAnd(()=>{
|
||||||
|
if(ptlockpass.checked){ p.lock_when_pass = true; } else { delete p.lock_when_pass; }
|
||||||
|
}));
|
||||||
|
const ptminibossdead=q("ptminibossdead"); if(ptminibossdead) ptminibossdead.addEventListener("change", ()=>pushAnd(()=>{
|
||||||
|
if(ptminibossdead.checked){ p.open_on_miniboss_dead = true; } else { delete p.open_on_miniboss_dead; }
|
||||||
|
}));
|
||||||
const ptspeed=q("ptspeed"); if(ptspeed) ptspeed.addEventListener("change", ()=>pushAnd(()=>{ p.speed = Math.max(1, num(ptspeed.value)); }));
|
const ptspeed=q("ptspeed"); if(ptspeed) ptspeed.addEventListener("change", ()=>pushAnd(()=>{ p.speed = Math.max(1, num(ptspeed.value)); }));
|
||||||
const hzrespawn=q("hzrespawn");
|
const hzrespawn=q("hzrespawn");
|
||||||
if(hzrespawn){
|
if(hzrespawn){
|
||||||
@@ -2462,6 +2504,7 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
const adlayer=q("adlayer"); if(adlayer) adlayer.addEventListener("change", ()=>pushAnd(()=>{ d.layer = Math.round(num(adlayer.value)); }));
|
const adlayer=q("adlayer"); if(adlayer) adlayer.addEventListener("change", ()=>pushAnd(()=>{ d.layer = Math.round(num(adlayer.value)); }));
|
||||||
|
const adkind=q("adkind"); if(adkind) adkind.addEventListener("change", ()=>pushAnd(()=>{ d.kind = adkind.value === "shade" ? "shade" : "ranged"; }));
|
||||||
const del=q("delSel"); if(del) del.addEventListener("click", ()=>pushAnd(()=>{
|
const del=q("delSel"); if(del) del.addEventListener("click", ()=>pushAnd(()=>{
|
||||||
room.ability_doors.splice(sel.index,1);
|
room.ability_doors.splice(sel.index,1);
|
||||||
state.selection=null;
|
state.selection=null;
|
||||||
@@ -3279,6 +3322,9 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
if(p.name) obj.name = p.name;
|
if(p.name) obj.name = p.name;
|
||||||
if(p.quest) obj.quest = p.quest;
|
if(p.quest) obj.quest = p.quest;
|
||||||
if(p.quest_complete) obj.quest_complete = p.quest_complete;
|
if(p.quest_complete) obj.quest_complete = p.quest_complete;
|
||||||
|
if(p.quest_turned_in) obj.quest_turned_in = p.quest_turned_in;
|
||||||
|
if(p.lock_when_pass) obj.lock_when_pass = true;
|
||||||
|
if(p.open_on_miniboss_dead) obj.open_on_miniboss_dead = true;
|
||||||
if(Array.isArray(p.respawn)) obj.respawn = p.respawn;
|
if(Array.isArray(p.respawn)) obj.respawn = p.respawn;
|
||||||
platforms[group].push(obj);
|
platforms[group].push(obj);
|
||||||
} else {
|
} else {
|
||||||
@@ -3315,10 +3361,11 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
|
|
||||||
// ability doors back to {rect:[x,y,w,h]} (+ layer when set)
|
// ability doors back to {rect:[x,y,w,h]} (+ layer/kind when set)
|
||||||
const ability_doors = (d.ability_doors || []).map(ad => {
|
const ability_doors = (d.ability_doors || []).map(ad => {
|
||||||
const out = { rect: [ad.x, ad.y, ad.w, ad.h] };
|
const out = { rect: [ad.x, ad.y, ad.w, ad.h] };
|
||||||
if(ad.layer) out.layer = ad.layer;
|
if(ad.layer) out.layer = ad.layer;
|
||||||
|
if(ad.kind === "shade") out.kind = "shade";
|
||||||
return out;
|
return out;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -3850,6 +3897,14 @@ els.canvasA.addEventListener("pointerdown", onPointerDown);
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
deleteSelection(); return;
|
deleteSelection(); return;
|
||||||
}
|
}
|
||||||
|
// Tool switching shortcuts (match the toolbar button titles).
|
||||||
|
const toolByKey = { v:"select", p:"platform", e:"enemy_spawn", n:"npc_spawn", o:"object_spawn" };
|
||||||
|
const toolKey = toolByKey[e.key.toLowerCase()];
|
||||||
|
if(toolKey){
|
||||||
|
e.preventDefault();
|
||||||
|
setTool(toolKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if(e.key.toLowerCase()==='f'){
|
if(e.key.toLowerCase()==='f'){
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
fitViewToRoom(getActiveRoom(),'A');
|
fitViewToRoom(getActiveRoom(),'A');
|
||||||
|
|||||||
+231
-35
@@ -438,6 +438,7 @@ JUMP_LIGHT = float(_phys(["player", "jump_light"], -12.0))
|
|||||||
JUMP_SHADE = float(_phys(["player", "jump_shade"], -9.0))
|
JUMP_SHADE = float(_phys(["player", "jump_shade"], -9.0))
|
||||||
GRAVITY_LIGHT = float(_phys(["player", "gravity_light"], 0.5))
|
GRAVITY_LIGHT = float(_phys(["player", "gravity_light"], 0.5))
|
||||||
GRAVITY_SHADE = float(_phys(["player", "gravity_shade"], 0.4))
|
GRAVITY_SHADE = float(_phys(["player", "gravity_shade"], 0.4))
|
||||||
|
SHADE_DOOR_GRACE = float(_phys(["player", "shade_door_grace"], 0.6))
|
||||||
|
|
||||||
# Sprite offsets (tune to align art with hitbox)
|
# Sprite offsets (tune to align art with hitbox)
|
||||||
PLAYER_SPRITE_OFFSET_X = int(_cfg(["player", "sprite_offset_x"], 0))
|
PLAYER_SPRITE_OFFSET_X = int(_cfg(["player", "sprite_offset_x"], 0))
|
||||||
@@ -941,6 +942,12 @@ def normalize_room_json(room_json: dict) -> dict:
|
|||||||
obj["quest"] = p["quest"]
|
obj["quest"] = p["quest"]
|
||||||
if "quest_complete" in p:
|
if "quest_complete" in p:
|
||||||
obj["quest_complete"] = p["quest_complete"]
|
obj["quest_complete"] = p["quest_complete"]
|
||||||
|
if "quest_turned_in" in p:
|
||||||
|
obj["quest_turned_in"] = p["quest_turned_in"]
|
||||||
|
if "lock_when_pass" in p:
|
||||||
|
obj["lock_when_pass"] = p["lock_when_pass"]
|
||||||
|
if "open_on_miniboss_dead" in p:
|
||||||
|
obj["open_on_miniboss_dead"] = p["open_on_miniboss_dead"]
|
||||||
if isinstance(p.get("respawn"), (list, tuple)) and len(p["respawn"]) >= 2:
|
if isinstance(p.get("respawn"), (list, tuple)) and len(p["respawn"]) >= 2:
|
||||||
obj["respawn"] = p["respawn"]
|
obj["respawn"] = p["respawn"]
|
||||||
grouped.setdefault(group, []).append(obj)
|
grouped.setdefault(group, []).append(obj)
|
||||||
@@ -1018,9 +1025,14 @@ def normalize_room_json(room_json: dict) -> dict:
|
|||||||
norm_doors.append({"rect": [d[0], d[1], d[2], d[3]]})
|
norm_doors.append({"rect": [d[0], d[1], d[2], d[3]]})
|
||||||
elif isinstance(d, dict):
|
elif isinstance(d, dict):
|
||||||
if isinstance(d.get("rect"), (list, tuple)) and len(d["rect"]) == 4:
|
if isinstance(d.get("rect"), (list, tuple)) and len(d["rect"]) == 4:
|
||||||
norm_doors.append(d)
|
nd = dict(d)
|
||||||
|
nd["kind"] = d.get("kind", "ranged")
|
||||||
|
norm_doors.append(nd)
|
||||||
elif all(k in d for k in ("x", "y", "w", "h")):
|
elif all(k in d for k in ("x", "y", "w", "h")):
|
||||||
norm_doors.append({"rect": [d.get("x", 0), d.get("y", 0), d.get("w", 0), d.get("h", 0)]})
|
norm_doors.append({
|
||||||
|
"rect": [d.get("x", 0), d.get("y", 0), d.get("w", 0), d.get("h", 0)],
|
||||||
|
"kind": d.get("kind", "ranged"),
|
||||||
|
})
|
||||||
out["ability_doors"] = norm_doors
|
out["ability_doors"] = norm_doors
|
||||||
|
|
||||||
return out
|
return out
|
||||||
@@ -1077,6 +1089,12 @@ class Room:
|
|||||||
meta["quest"] = str(tp.get("quest"))
|
meta["quest"] = str(tp.get("quest"))
|
||||||
if tp.get("quest_complete"):
|
if tp.get("quest_complete"):
|
||||||
meta["quest_complete"] = str(tp.get("quest_complete"))
|
meta["quest_complete"] = str(tp.get("quest_complete"))
|
||||||
|
if tp.get("quest_turned_in"):
|
||||||
|
meta["quest_turned_in"] = str(tp.get("quest_turned_in"))
|
||||||
|
if tp.get("lock_when_pass"):
|
||||||
|
meta["lock_when_pass"] = True
|
||||||
|
if tp.get("open_on_miniboss_dead"):
|
||||||
|
meta["open_on_miniboss_dead"] = True
|
||||||
self.platform_meta[id(rect)] = meta
|
self.platform_meta[id(rect)] = meta
|
||||||
self.platforms_trigger.append(rect)
|
self.platforms_trigger.append(rect)
|
||||||
elif isinstance(tp, (list, tuple)) and len(tp) >= 4:
|
elif isinstance(tp, (list, tuple)) and len(tp) >= 4:
|
||||||
@@ -1155,7 +1173,7 @@ class Room:
|
|||||||
rect = list(d)
|
rect = list(d)
|
||||||
if rect is None:
|
if rect is None:
|
||||||
continue
|
continue
|
||||||
nd = {"rect": rect}
|
nd = {"rect": rect, "kind": d.get("kind", "ranged") if isinstance(d, dict) else "ranged"}
|
||||||
if isinstance(d, dict):
|
if isinstance(d, dict):
|
||||||
try:
|
try:
|
||||||
nd["layer"] = int(d.get("layer", 0))
|
nd["layer"] = int(d.get("layer", 0))
|
||||||
@@ -1421,6 +1439,8 @@ class Projectile:
|
|||||||
for d in doors:
|
for d in doors:
|
||||||
if getattr(d, "open", False):
|
if getattr(d, "open", False):
|
||||||
continue
|
continue
|
||||||
|
if getattr(d, "kind", "ranged") == "shade":
|
||||||
|
continue # shade doors cannot be shot open
|
||||||
if self.rect.colliderect(d.rect):
|
if self.rect.colliderect(d.rect):
|
||||||
d.open = True
|
d.open = True
|
||||||
mark_door_broken(getattr(d, "room_id", None), d.rect)
|
mark_door_broken(getattr(d, "room_id", None), d.rect)
|
||||||
@@ -1906,17 +1926,22 @@ class CoinPickup:
|
|||||||
|
|
||||||
|
|
||||||
class AbilityDoor:
|
class AbilityDoor:
|
||||||
def __init__(self, rect: pygame.Rect, room_id: str | None = None):
|
def __init__(self, rect: pygame.Rect, room_id: str | None = None, kind: str = "ranged"):
|
||||||
self.rect = rect
|
self.rect = rect
|
||||||
self.open = False
|
self.open = False
|
||||||
self.room_id = room_id
|
self.room_id = room_id
|
||||||
|
self.kind = "shade" if str(kind) == "shade" else "ranged"
|
||||||
|
|
||||||
def draw(self, surface, cx, cy):
|
def draw(self, surface, cx, cy):
|
||||||
if self.open:
|
if self.open:
|
||||||
return
|
return
|
||||||
r = pygame.Rect(self.rect.x - cx, self.rect.y - cy, self.rect.width, self.rect.height)
|
r = pygame.Rect(self.rect.x - cx, self.rect.y - cy, self.rect.width, self.rect.height)
|
||||||
pygame.draw.rect(surface, (120, 180, 120), r)
|
if self.kind == "shade":
|
||||||
pygame.draw.rect(surface, (40, 80, 40), r, 2)
|
pygame.draw.rect(surface, (110, 100, 170), r)
|
||||||
|
pygame.draw.rect(surface, (60, 50, 110), r, 2)
|
||||||
|
else:
|
||||||
|
pygame.draw.rect(surface, (120, 180, 120), r)
|
||||||
|
pygame.draw.rect(surface, (40, 80, 40), r, 2)
|
||||||
|
|
||||||
|
|
||||||
def spawn_ability_doors(room) -> list[AbilityDoor]:
|
def spawn_ability_doors(room) -> list[AbilityDoor]:
|
||||||
@@ -1925,7 +1950,7 @@ def spawn_ability_doors(room) -> list[AbilityDoor]:
|
|||||||
if not isinstance(d, dict):
|
if not isinstance(d, dict):
|
||||||
continue
|
continue
|
||||||
rect = pygame.Rect(*map(int, d.get("rect", [0, 0, 0, 0])))
|
rect = pygame.Rect(*map(int, d.get("rect", [0, 0, 0, 0])))
|
||||||
door = AbilityDoor(rect, room_id=room.id)
|
door = AbilityDoor(rect, room_id=room.id, kind=d.get("kind", "ranged"))
|
||||||
door.open = is_door_broken(room.id, rect)
|
door.open = is_door_broken(room.id, rect)
|
||||||
door.layer = int(d.get("layer", 0))
|
door.layer = int(d.get("layer", 0))
|
||||||
out.append(door)
|
out.append(door)
|
||||||
@@ -1942,8 +1967,12 @@ class RewardChest:
|
|||||||
if self.opened:
|
if self.opened:
|
||||||
return False
|
return False
|
||||||
light_amt, dark_amt = self.coins
|
light_amt, dark_amt = self.coins
|
||||||
player.coins_light += int(light_amt)
|
cx = self.rect.centerx
|
||||||
player.coins_dark += int(dark_amt)
|
cy = self.rect.top - 18 # pop out of the chest top, clear of its platform
|
||||||
|
for _ in range(max(0, int(light_amt))):
|
||||||
|
coin_drops.append(CoinPickup(cx, cy, "light"))
|
||||||
|
for _ in range(max(0, int(dark_amt))):
|
||||||
|
coin_drops.append(CoinPickup(cx, cy, "dark"))
|
||||||
self.opened = True
|
self.opened = True
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -3260,6 +3289,7 @@ class Player:
|
|||||||
self.on_ground = False
|
self.on_ground = False
|
||||||
self.ground_platform = None
|
self.ground_platform = None
|
||||||
self.shade = False
|
self.shade = False
|
||||||
|
self.shade_door_grace = 0.0 # grace period to pass shade doors after a form switch
|
||||||
|
|
||||||
self.charge = float(BASE_MAX_CHARGE)
|
self.charge = float(BASE_MAX_CHARGE)
|
||||||
self.health = int(BASE_MAX_HEALTH)
|
self.health = int(BASE_MAX_HEALTH)
|
||||||
@@ -3601,6 +3631,7 @@ class Player:
|
|||||||
if not self.shade and self.charge <= 0:
|
if not self.shade and self.charge <= 0:
|
||||||
return
|
return
|
||||||
self.shade = not self.shade
|
self.shade = not self.shade
|
||||||
|
self.shade_door_grace = SHADE_DOOR_GRACE
|
||||||
|
|
||||||
def attack(self):
|
def attack(self):
|
||||||
if self.healing:
|
if self.healing:
|
||||||
@@ -3813,6 +3844,9 @@ class Player:
|
|||||||
self.just_air_jumped = False
|
self.just_air_jumped = False
|
||||||
self.fairy_bob_t += dt
|
self.fairy_bob_t += dt
|
||||||
|
|
||||||
|
if self.shade_door_grace > 0.0:
|
||||||
|
self.shade_door_grace = max(0.0, self.shade_door_grace - dt)
|
||||||
|
|
||||||
if self.invincibility_timer > 0:
|
if self.invincibility_timer > 0:
|
||||||
self.invincibility_timer -= dt
|
self.invincibility_timer -= dt
|
||||||
|
|
||||||
@@ -3967,6 +4001,7 @@ class Player:
|
|||||||
if self.charge <= 0:
|
if self.charge <= 0:
|
||||||
self.charge = 0
|
self.charge = 0
|
||||||
self.shade = False
|
self.shade = False
|
||||||
|
self.shade_door_grace = SHADE_DOOR_GRACE
|
||||||
else:
|
else:
|
||||||
# no regen while healing, attacking, or during post-attack lockout
|
# no regen while healing, attacking, or during post-attack lockout
|
||||||
if (not self.healing) and (not self.attacking) and (self.charge_regen_lock <= 0.0):
|
if (not self.healing) and (not self.attacking) and (self.charge_regen_lock <= 0.0):
|
||||||
@@ -4164,6 +4199,9 @@ class Enemy:
|
|||||||
self.drop_ignore_timer = 0.0
|
self.drop_ignore_timer = 0.0
|
||||||
self.drop_ignore_platform = None
|
self.drop_ignore_platform = None
|
||||||
|
|
||||||
|
# True when killed by touching a hazard zone (drops nothing).
|
||||||
|
self.hazard_kill = False
|
||||||
|
|
||||||
|
|
||||||
def take_damage(self, dmg):
|
def take_damage(self, dmg):
|
||||||
self.health = max(0.0, float(self.health) - float(dmg))
|
self.health = max(0.0, float(self.health) - float(dmg))
|
||||||
@@ -4176,6 +4214,7 @@ class Enemy:
|
|||||||
self.rect.topleft = (self.start_x, self.start_y)
|
self.rect.topleft = (self.start_x, self.start_y)
|
||||||
self.vel_x = 0.0
|
self.vel_x = 0.0
|
||||||
self.vel_y = 0.0
|
self.vel_y = 0.0
|
||||||
|
self.hazard_kill = False
|
||||||
cfg = ENEMY_TYPES.get(self.type_id, ENEMY_TYPES.get("enemy", {}))
|
cfg = ENEMY_TYPES.get(self.type_id, ENEMY_TYPES.get("enemy", {}))
|
||||||
self.max_health = float(cfg.get("health", 3)) if isinstance(cfg, dict) else 3.0
|
self.max_health = float(cfg.get("health", 3)) if isinstance(cfg, dict) else 3.0
|
||||||
self.health = float(self.max_health)
|
self.health = float(self.max_health)
|
||||||
@@ -4719,6 +4758,8 @@ def spawn_enemies_from_room(room: Room):
|
|||||||
t = str(ent.get("type", "enemy"))
|
t = str(ent.get("type", "enemy"))
|
||||||
if t == "boss" and is_boss_defeated(room.id):
|
if t == "boss" and is_boss_defeated(room.id):
|
||||||
continue
|
continue
|
||||||
|
if t == "miniboss" and room.id in DEFEATED_MINIBOSSES:
|
||||||
|
continue
|
||||||
pos = ent.get("pos", None)
|
pos = ent.get("pos", None)
|
||||||
if not (isinstance(pos, (list, tuple)) and len(pos) == 2):
|
if not (isinstance(pos, (list, tuple)) and len(pos) == 2):
|
||||||
if "x" in ent and "y" in ent:
|
if "x" in ent and "y" in ent:
|
||||||
@@ -4939,18 +4980,79 @@ class HouseObject:
|
|||||||
|
|
||||||
def trigger_wall_blocking(rect) -> bool:
|
def trigger_wall_blocking(rect) -> bool:
|
||||||
"""A trigger wall blocks while its unlock condition is NOT met.
|
"""A trigger wall blocks while its unlock condition is NOT met.
|
||||||
- ``quest``: wall stays until the quest is accepted (becomes active).
|
- ``quest``: wall stays until the quest is accepted (becomes active),
|
||||||
|
and stays open afterwards (once ever completed).
|
||||||
- ``quest_complete``: wall stays until the quest objective is done
|
- ``quest_complete``: wall stays until the quest objective is done
|
||||||
(is_complete, or the quest has been completed at least once).
|
(is_complete, or the quest has been completed at least once).
|
||||||
|
- ``quest_turned_in``: wall stays until the quest has been completed
|
||||||
|
and turned in (only ever_completed counts).
|
||||||
|
- ``lock_when_pass``: wall is gone initially and only appears once the
|
||||||
|
player has walked through it (arena lock).
|
||||||
|
- ``open_on_miniboss_dead``: releases the wall once the room's miniboss
|
||||||
|
is slain (combines with the conditions above; with no other condition
|
||||||
|
the wall is simply present until then).
|
||||||
Walls with no condition are inert placeholders (never block)."""
|
Walls with no condition are inert placeholders (never block)."""
|
||||||
meta = platform_meta.get(id(rect), {})
|
meta = platform_meta.get(id(rect), {})
|
||||||
qid = meta.get("quest")
|
qid = meta.get("quest")
|
||||||
if qid:
|
if meta.get("lock_when_pass"):
|
||||||
return qid not in quest_manager.active
|
present = id(rect) in LOCKED_TRIGGER_WALLS
|
||||||
qc = meta.get("quest_complete")
|
elif qid:
|
||||||
if qc:
|
present = qid not in quest_manager.active and qid not in quest_manager.ever_completed
|
||||||
return not (quest_manager.is_complete(qc) or qc in quest_manager.ever_completed)
|
elif meta.get("quest_complete"):
|
||||||
return False
|
qc = meta["quest_complete"]
|
||||||
|
present = not (quest_manager.is_complete(qc) or qc in quest_manager.ever_completed)
|
||||||
|
elif meta.get("quest_turned_in"):
|
||||||
|
present = meta["quest_turned_in"] not in quest_manager.ever_completed
|
||||||
|
elif meta.get("open_on_miniboss_dead"):
|
||||||
|
present = True # always present until released
|
||||||
|
else:
|
||||||
|
return False # inert placeholder
|
||||||
|
|
||||||
|
if meta.get("open_on_miniboss_dead"):
|
||||||
|
present = present and miniboss_alive_in_room()
|
||||||
|
return present
|
||||||
|
|
||||||
|
|
||||||
|
# Lock-on-pass trigger walls (per-session, keyed by rect id).
|
||||||
|
LOCKED_TRIGGER_WALLS: set[int] = set()
|
||||||
|
TRIGGER_WALL_TOUCH: dict[int, bool] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def miniboss_alive_in_room() -> bool:
|
||||||
|
return any(getattr(en, "type_id", "") == "miniboss" and en.health > 0 for en in enemies)
|
||||||
|
|
||||||
|
|
||||||
|
def push_rect_out_of_rects(rect, rects):
|
||||||
|
"""Push a pygame.Rect out of any overlapping rect, moving to the nearest edge.
|
||||||
|
|
||||||
|
Used to safely eject the player from a platform they've ended up inside
|
||||||
|
(e.g. a shade door that became solid again, or an awkward spawn point)
|
||||||
|
instead of letting the physics resolve it with a jarring teleport."""
|
||||||
|
for obs in rects:
|
||||||
|
if not rect.colliderect(obs):
|
||||||
|
continue
|
||||||
|
inter = rect.clip(obs)
|
||||||
|
if inter.width <= 0 or inter.height <= 0:
|
||||||
|
continue
|
||||||
|
if inter.width < inter.height:
|
||||||
|
# narrower overlap on X: push to the nearest horizontal side
|
||||||
|
if rect.right - obs.left <= obs.right - rect.left:
|
||||||
|
rect.right = obs.left
|
||||||
|
else:
|
||||||
|
rect.left = obs.right
|
||||||
|
else:
|
||||||
|
# narrower overlap on Y: push to the nearest vertical side
|
||||||
|
if rect.bottom - obs.top <= obs.bottom - rect.top:
|
||||||
|
rect.bottom = obs.top
|
||||||
|
else:
|
||||||
|
rect.top = obs.bottom
|
||||||
|
return rect
|
||||||
|
|
||||||
|
|
||||||
|
def reset_trigger_wall_locks():
|
||||||
|
"""Clear lock-on-pass trigger state (on room change)."""
|
||||||
|
LOCKED_TRIGGER_WALLS.clear()
|
||||||
|
TRIGGER_WALL_TOUCH.clear()
|
||||||
|
|
||||||
|
|
||||||
prev_moving_pos: dict[int, tuple[int, int]] = {}
|
prev_moving_pos: dict[int, tuple[int, int]] = {}
|
||||||
@@ -5228,7 +5330,7 @@ def _draw_tether_chain_loop(surface, rect: pygame.Rect, light: bool, cx: float,
|
|||||||
|
|
||||||
|
|
||||||
def get_pause_menu_rects():
|
def get_pause_menu_rects():
|
||||||
items = ["Resume", "Save Game", "Settings", "Main Menu", "Close Game"]
|
items = ["Resume", "Settings", "Main Menu", "Close Game"]
|
||||||
rects = []
|
rects = []
|
||||||
start_y = HEIGHT // 2 + 20
|
start_y = HEIGHT // 2 + 20
|
||||||
for i, label in enumerate(items):
|
for i, label in enumerate(items):
|
||||||
@@ -5240,7 +5342,7 @@ def get_pause_menu_rects():
|
|||||||
|
|
||||||
|
|
||||||
def draw_pause_menu(surface, selected_idx: int, use_mouse: bool):
|
def draw_pause_menu(surface, selected_idx: int, use_mouse: bool):
|
||||||
items = ["Resume", "Save Game", "Settings", "Main Menu", "Close Game"]
|
items = ["Resume", "Settings", "Main Menu", "Close Game"]
|
||||||
rects = get_pause_menu_rects()
|
rects = get_pause_menu_rects()
|
||||||
mouse_pos = pygame.mouse.get_pos()
|
mouse_pos = pygame.mouse.get_pos()
|
||||||
hover_idx = None
|
hover_idx = None
|
||||||
@@ -5557,6 +5659,7 @@ def apply_room(room: Room, set_player_spawn=True):
|
|||||||
global token_drops
|
global token_drops
|
||||||
global ACTIVE_REFIGHT
|
global ACTIVE_REFIGHT
|
||||||
global ROOM_DROP_MULT, ROOM_COIN_MULT
|
global ROOM_DROP_MULT, ROOM_COIN_MULT
|
||||||
|
reset_trigger_wall_locks()
|
||||||
|
|
||||||
|
|
||||||
ROOM = room
|
ROOM = room
|
||||||
@@ -5609,6 +5712,8 @@ def apply_room(room: Room, set_player_spawn=True):
|
|||||||
player.rect.midbottom = (player.start_x, player.start_y)
|
player.rect.midbottom = (player.start_x, player.start_y)
|
||||||
player.vel_y = 0.0
|
player.vel_y = 0.0
|
||||||
player.on_ground = False
|
player.on_ground = False
|
||||||
|
# Don't spawn embedded in a platform: push out to the nearest edge.
|
||||||
|
push_rect_out_of_rects(player.rect, ROOM.platforms)
|
||||||
|
|
||||||
def boss_alive_in_room() -> bool:
|
def boss_alive_in_room() -> bool:
|
||||||
return any(getattr(en, "type_id", "") == "boss" and en.health > 0 for en in enemies)
|
return any(getattr(en, "type_id", "") == "boss" and en.health > 0 for en in enemies)
|
||||||
@@ -5618,6 +5723,7 @@ def reset_room_combat_state():
|
|||||||
"""Rebuild room enemies/NPCs/doors and clear transient combat objects."""
|
"""Rebuild room enemies/NPCs/doors and clear transient combat objects."""
|
||||||
global enemies, npcs, loot_drops, shockwaves, coin_drops, token_drops, projectiles
|
global enemies, npcs, loot_drops, shockwaves, coin_drops, token_drops, projectiles
|
||||||
global shatter_platforms, shatter_aoes, dust_puffs, tether_shots, ability_doors
|
global shatter_platforms, shatter_aoes, dust_puffs, tether_shots, ability_doors
|
||||||
|
reset_trigger_wall_locks()
|
||||||
enemies = spawn_enemies_from_room(ROOM)
|
enemies = spawn_enemies_from_room(ROOM)
|
||||||
npcs = spawn_npcs_from_room(ROOM)
|
npcs = spawn_npcs_from_room(ROOM)
|
||||||
loot_drops = []
|
loot_drops = []
|
||||||
@@ -5722,6 +5828,10 @@ camera_x = camera_y = 0.0
|
|||||||
# Track bosses defeated per room id (runtime only)
|
# Track bosses defeated per room id (runtime only)
|
||||||
DEFEATED_BOSSES: set[str] = set()
|
DEFEATED_BOSSES: set[str] = set()
|
||||||
|
|
||||||
|
# Minibosses stay defeated (per room) until the player saves at a lantern
|
||||||
|
# in a different room — then they respawn. Runtime only, like butterfly jars.
|
||||||
|
DEFEATED_MINIBOSSES: set[str] = set()
|
||||||
|
|
||||||
# Reality Break: per-room drop multipliers (reset on room change)
|
# Reality Break: per-room drop multipliers (reset on room change)
|
||||||
ROOM_DROP_MULT = 1.0
|
ROOM_DROP_MULT = 1.0
|
||||||
ROOM_COIN_MULT = 1.0
|
ROOM_COIN_MULT = 1.0
|
||||||
@@ -5734,6 +5844,17 @@ def mark_boss_defeated(room_id: str):
|
|||||||
DEFEATED_BOSSES.add(room_id)
|
DEFEATED_BOSSES.add(room_id)
|
||||||
|
|
||||||
|
|
||||||
|
def respawn_defeated_minibosses_if_elsewhere() -> bool:
|
||||||
|
"""Minibosses stay defeated until the player saves at a lantern in a
|
||||||
|
different room than the one where the miniboss was slain. Returns True
|
||||||
|
if any defeated miniboss was respawned (i.e. removed from the set)."""
|
||||||
|
if not DEFEATED_MINIBOSSES:
|
||||||
|
return False
|
||||||
|
before = len(DEFEATED_MINIBOSSES)
|
||||||
|
DEFEATED_MINIBOSSES.intersection_update({ROOM.id})
|
||||||
|
return len(DEFEATED_MINIBOSSES) != before
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------
|
# ----------------------------
|
||||||
# Quest system
|
# Quest system
|
||||||
# ----------------------------
|
# ----------------------------
|
||||||
@@ -5856,6 +5977,11 @@ class QuestManager:
|
|||||||
def get(self, quest_id: str) -> Quest | None:
|
def get(self, quest_id: str) -> Quest | None:
|
||||||
return self.quests.get(quest_id)
|
return self.quests.get(quest_id)
|
||||||
|
|
||||||
|
def requires_shade(self, quest_id: str) -> bool:
|
||||||
|
"""Whether a quest can only be taken/collected in shade form."""
|
||||||
|
q = self.quests.get(quest_id)
|
||||||
|
return bool(q is not None and q.objective.get("shade"))
|
||||||
|
|
||||||
def quests_from(self, giver_id: str) -> list[Quest]:
|
def quests_from(self, giver_id: str) -> list[Quest]:
|
||||||
return [q for q in self.quests.values() if q.giver == giver_id]
|
return [q for q in self.quests.values() if q.giver == giver_id]
|
||||||
|
|
||||||
@@ -5899,8 +6025,10 @@ class QuestManager:
|
|||||||
else:
|
else:
|
||||||
self.completed_one_time.add(quest_id)
|
self.completed_one_time.add(quest_id)
|
||||||
|
|
||||||
def progress_kill(self, enemy_type: str) -> list[str]:
|
def progress_kill(self, enemy_type: str, shade: bool = False) -> list[str]:
|
||||||
"""Progress every active kill-quest matching this enemy type.
|
"""Progress every active kill-quest matching this enemy type.
|
||||||
|
A quest whose objective has ``shade: true`` only counts kills made
|
||||||
|
while the player is in shade form.
|
||||||
Returns quest ids that just became complete."""
|
Returns quest ids that just became complete."""
|
||||||
completed = []
|
completed = []
|
||||||
for qid in list(self.active.keys()):
|
for qid in list(self.active.keys()):
|
||||||
@@ -5910,6 +6038,8 @@ class QuestManager:
|
|||||||
obj = q.objective
|
obj = q.objective
|
||||||
if obj.get("kind") != "kill" or str(obj.get("target", "")) != enemy_type:
|
if obj.get("kind") != "kill" or str(obj.get("target", "")) != enemy_type:
|
||||||
continue
|
continue
|
||||||
|
if obj.get("shade") and not shade:
|
||||||
|
continue
|
||||||
was = self.is_complete(qid)
|
was = self.is_complete(qid)
|
||||||
self.progress(qid, 1)
|
self.progress(qid, 1)
|
||||||
if not was and self.is_complete(qid):
|
if not was and self.is_complete(qid):
|
||||||
@@ -5994,6 +6124,17 @@ QUEST_DEFS_DEFAULT = [
|
|||||||
"coin_range": [5, 15],
|
"coin_range": [5, 15],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "first_kills_shade",
|
||||||
|
"name": "First Kills (Shade)",
|
||||||
|
"description": "Kill 4 enemies while in shade form.",
|
||||||
|
"type": "one_time",
|
||||||
|
"giver": "object:quest_point",
|
||||||
|
"objective": {"kind": "kill", "target": "enemy", "count": 4, "shade": True},
|
||||||
|
"reward": {
|
||||||
|
"guaranteed_item": {"item_id": "necklace_placeholder"},
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
quest_manager = QuestManager()
|
quest_manager = QuestManager()
|
||||||
@@ -6203,6 +6344,8 @@ def save_at_lantern(sp: SavePoint):
|
|||||||
pickup_feed.push_center("Lantern - game saved, fully restored", duration=2.0)
|
pickup_feed.push_center("Lantern - game saved, fully restored", duration=2.0)
|
||||||
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)
|
||||||
|
if respawn_defeated_minibosses_if_elsewhere():
|
||||||
|
pickup_feed.push_center("Defeated minibosses have respawned!", duration=2.5)
|
||||||
|
|
||||||
|
|
||||||
def _ellipsize_text(text: str, fnt, max_w: int) -> str:
|
def _ellipsize_text(text: str, fnt, max_w: int) -> str:
|
||||||
@@ -6645,8 +6788,9 @@ def apply_save_data(data: dict):
|
|||||||
DEFEATED_BOSSES.update(data.get("defeated_bosses") or [])
|
DEFEATED_BOSSES.update(data.get("defeated_bosses") or [])
|
||||||
BROKEN_DOORS.clear()
|
BROKEN_DOORS.clear()
|
||||||
BROKEN_DOORS.update(data.get("broken_doors") or [])
|
BROKEN_DOORS.update(data.get("broken_doors") or [])
|
||||||
# Butterfly jars respawn at their points on reload; the corpse is session-only.
|
# Butterfly jars and minibosses respawn on reload (session-only); corpse too.
|
||||||
COLLECTED_JARS.clear()
|
COLLECTED_JARS.clear()
|
||||||
|
DEFEATED_MINIBOSSES.clear()
|
||||||
JAR_DEATH_ROOM = None
|
JAR_DEATH_ROOM = None
|
||||||
corpse = None
|
corpse = None
|
||||||
|
|
||||||
@@ -6671,6 +6815,8 @@ def apply_save_data(data: dict):
|
|||||||
apply_room(room, set_player_spawn=True)
|
apply_room(room, set_player_spawn=True)
|
||||||
|
|
||||||
player.respawn()
|
player.respawn()
|
||||||
|
# respawn() re-places at the raw spawn; make sure we're not embedded.
|
||||||
|
push_rect_out_of_rects(player.rect, room.platforms)
|
||||||
if "health" in pdata:
|
if "health" in pdata:
|
||||||
player.health = int(pdata["health"])
|
player.health = int(pdata["health"])
|
||||||
if "charge" in pdata:
|
if "charge" in pdata:
|
||||||
@@ -6759,8 +6905,6 @@ def _do_save_game():
|
|||||||
def _return_to_main_menu():
|
def _return_to_main_menu():
|
||||||
global GAME_MODE, paused, pause_menu, pause_menu_index, pause_use_mouse
|
global GAME_MODE, paused, pause_menu, pause_menu_index, pause_use_mouse
|
||||||
global settings_wait_action
|
global settings_wait_action
|
||||||
if CURRENT_SAVE_NAME:
|
|
||||||
_do_save_game()
|
|
||||||
inv_ui.visible = False
|
inv_ui.visible = False
|
||||||
trade_ui.visible = False
|
trade_ui.visible = False
|
||||||
trade_ui.active_npc = None
|
trade_ui.active_npc = None
|
||||||
@@ -7190,12 +7334,10 @@ while running:
|
|||||||
if idx == 0:
|
if idx == 0:
|
||||||
paused = False
|
paused = False
|
||||||
elif idx == 1:
|
elif idx == 1:
|
||||||
_do_save_game()
|
|
||||||
elif idx == 2:
|
|
||||||
pause_menu = "settings"
|
pause_menu = "settings"
|
||||||
elif idx == 3:
|
elif idx == 2:
|
||||||
_return_to_main_menu()
|
_return_to_main_menu()
|
||||||
elif idx == 4:
|
elif idx == 3:
|
||||||
running = False
|
running = False
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -7210,21 +7352,19 @@ while running:
|
|||||||
|
|
||||||
if paused and pause_menu == "main":
|
if paused and pause_menu == "main":
|
||||||
if key_is("ui_up", e.key):
|
if key_is("ui_up", e.key):
|
||||||
pause_menu_index = (pause_menu_index - 1) % 5
|
pause_menu_index = (pause_menu_index - 1) % 4
|
||||||
pause_use_mouse = False
|
pause_use_mouse = False
|
||||||
elif key_is("ui_down", e.key):
|
elif key_is("ui_down", e.key):
|
||||||
pause_menu_index = (pause_menu_index + 1) % 5
|
pause_menu_index = (pause_menu_index + 1) % 4
|
||||||
pause_use_mouse = False
|
pause_use_mouse = False
|
||||||
elif e.key in (key_for("ui_confirm"), key_for("ui_confirm_alt")):
|
elif e.key in (key_for("ui_confirm"), key_for("ui_confirm_alt")):
|
||||||
if pause_menu_index == 0:
|
if pause_menu_index == 0:
|
||||||
paused = False
|
paused = False
|
||||||
elif pause_menu_index == 1:
|
elif pause_menu_index == 1:
|
||||||
_do_save_game()
|
|
||||||
elif pause_menu_index == 2:
|
|
||||||
pause_menu = "settings"
|
pause_menu = "settings"
|
||||||
elif pause_menu_index == 3:
|
elif pause_menu_index == 2:
|
||||||
_return_to_main_menu()
|
_return_to_main_menu()
|
||||||
elif pause_menu_index == 4:
|
elif pause_menu_index == 3:
|
||||||
running = False
|
running = False
|
||||||
elif key_is("pause", e.key):
|
elif key_is("pause", e.key):
|
||||||
paused = False
|
paused = False
|
||||||
@@ -7271,7 +7411,11 @@ while running:
|
|||||||
else:
|
else:
|
||||||
near_qg = get_near_quest_giver(player, quest_givers)
|
near_qg = get_near_quest_giver(player, quest_givers)
|
||||||
if near_qg:
|
if near_qg:
|
||||||
interact_quest_giver(near_qg)
|
if quest_manager.requires_shade(near_qg.quest_id) and not player.shade:
|
||||||
|
# Shade-gated quest: no interaction while in light form.
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
interact_quest_giver(near_qg)
|
||||||
|
|
||||||
if key_is("pause", e.key):
|
if key_is("pause", e.key):
|
||||||
if paused:
|
if paused:
|
||||||
@@ -7479,7 +7623,22 @@ while running:
|
|||||||
blocking_triggers = [t for t in platforms_trigger if trigger_wall_blocking(t)]
|
blocking_triggers = [t for t in platforms_trigger if trigger_wall_blocking(t)]
|
||||||
|
|
||||||
collision_platforms = platforms + [d.rect for d in ability_doors if not d.open] + blocking_triggers
|
collision_platforms = platforms + [d.rect for d in ability_doors if not d.open] + blocking_triggers
|
||||||
player_collision_platforms = collision_platforms + [p.rect for p in shatter_platforms if p.alive]
|
|
||||||
|
# Shade doors are solid for the world, but the player can pass through them
|
||||||
|
# freely while in shade form — and for a short grace period after switching
|
||||||
|
# forms, so they aren't instantly ejected from a door they're standing in.
|
||||||
|
player_ignores_shade_doors = player.shade or player.shade_door_grace > 0.0
|
||||||
|
if player_ignores_shade_doors:
|
||||||
|
shade_door_ids = {id(d.rect) for d in ability_doors if (not d.open) and d.kind == "shade"}
|
||||||
|
player_collision_platforms = [r for r in collision_platforms if id(r) not in shade_door_ids]
|
||||||
|
else:
|
||||||
|
player_collision_platforms = collision_platforms
|
||||||
|
# Grace expired: safely eject the player from any shade door they're
|
||||||
|
# still inside (nearest edge) before collision runs.
|
||||||
|
for d in ability_doors:
|
||||||
|
if (not d.open) and d.kind == "shade" and player.rect.colliderect(d.rect):
|
||||||
|
push_rect_out_of_rects(player.rect, [d.rect])
|
||||||
|
player_collision_platforms = player_collision_platforms + [p.rect for p in shatter_platforms if p.alive]
|
||||||
|
|
||||||
# Shade + clipping an enemy drains charge twice as fast (shade still avoids damage).
|
# Shade + clipping an enemy drains charge twice as fast (shade still avoids damage).
|
||||||
if player.shade:
|
if player.shade:
|
||||||
@@ -7500,6 +7659,24 @@ while running:
|
|||||||
player.ground_platform = rect
|
player.ground_platform = rect
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Lock-on-pass trigger walls: gone initially, they appear once the player has
|
||||||
|
# walked through them (crossing from one side to the other) — arena lock.
|
||||||
|
for rect in platforms_trigger:
|
||||||
|
meta = platform_meta.get(id(rect), {})
|
||||||
|
if not meta.get("lock_when_pass"):
|
||||||
|
continue
|
||||||
|
key = id(rect)
|
||||||
|
if key in LOCKED_TRIGGER_WALLS:
|
||||||
|
continue
|
||||||
|
entry = TRIGGER_WALL_TOUCH.get(key)
|
||||||
|
if entry is None:
|
||||||
|
if player.rect.colliderect(rect):
|
||||||
|
TRIGGER_WALL_TOUCH[key] = "L" if player.rect.centerx < rect.centerx else "R"
|
||||||
|
elif entry == "L" and player.rect.left >= rect.right:
|
||||||
|
LOCKED_TRIGGER_WALLS.add(key)
|
||||||
|
elif entry == "R" and player.rect.right <= rect.left:
|
||||||
|
LOCKED_TRIGGER_WALLS.add(key)
|
||||||
|
|
||||||
# Hazard zones: touching one deals 1 damage and teleports the player to its linked respawn
|
# 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.
|
# (defaults to the room's player spawn). A lethal hit falls through to the normal death respawn.
|
||||||
for hz in platforms_hazard:
|
for hz in platforms_hazard:
|
||||||
@@ -7592,10 +7769,21 @@ while running:
|
|||||||
player.shatterstep_landing_pending = False
|
player.shatterstep_landing_pending = False
|
||||||
player.shatterstep_landing_damage = 0.0
|
player.shatterstep_landing_damage = 0.0
|
||||||
|
|
||||||
|
# Never respawn embedded in a platform: push out to the nearest edge.
|
||||||
|
push_rect_out_of_rects(player.rect, ROOM.platforms)
|
||||||
|
|
||||||
# Update enemies
|
# Update enemies
|
||||||
for en in enemies:
|
for en in enemies:
|
||||||
en.update(dt, collision_platforms, ground_platforms, player, shockwaves)
|
en.update(dt, collision_platforms, ground_platforms, player, shockwaves)
|
||||||
|
|
||||||
|
# Enemies that touch a hazard zone die instantly, dropping nothing.
|
||||||
|
for en in enemies:
|
||||||
|
if en.health <= 0:
|
||||||
|
continue
|
||||||
|
if any(en.rect.colliderect(hz) for hz in platforms_hazard):
|
||||||
|
en.health = 0.0
|
||||||
|
en.hazard_kill = True
|
||||||
|
|
||||||
# Enemy enemies do NOT physically collide.
|
# Enemy enemies do NOT physically collide.
|
||||||
# (Prevents enemies from shoving each other into platforms / off ledges.)
|
# (Prevents enemies from shoving each other into platforms / off ledges.)
|
||||||
# resolve_enemy_collisions(enemies)
|
# resolve_enemy_collisions(enemies)
|
||||||
@@ -7702,6 +7890,8 @@ while running:
|
|||||||
if en.health > 0:
|
if en.health > 0:
|
||||||
alive.append(en)
|
alive.append(en)
|
||||||
else:
|
else:
|
||||||
|
if getattr(en, "hazard_kill", False):
|
||||||
|
continue # hazard deaths drop nothing and give no quest credit
|
||||||
loot_drops.extend(spawn_drops_from_enemy(en))
|
loot_drops.extend(spawn_drops_from_enemy(en))
|
||||||
etype = getattr(en, "type_id", "")
|
etype = getattr(en, "type_id", "")
|
||||||
if etype == "boss":
|
if etype == "boss":
|
||||||
@@ -7709,9 +7899,11 @@ while running:
|
|||||||
bid = boss_id_for_room(ROOM.id)
|
bid = boss_id_for_room(ROOM.id)
|
||||||
if bid and ACTIVE_REFIGHT is None:
|
if bid and ACTIVE_REFIGHT is None:
|
||||||
token_drops.append(TokenPickup(en.rect.centerx - 8, en.rect.top - 20, bid))
|
token_drops.append(TokenPickup(en.rect.centerx - 8, en.rect.top - 20, bid))
|
||||||
|
elif etype == "miniboss":
|
||||||
|
DEFEATED_MINIBOSSES.add(ROOM.id)
|
||||||
|
|
||||||
# Quest kill tracking
|
# Quest kill tracking
|
||||||
for qid in quest_manager.progress_kill(etype):
|
for qid in quest_manager.progress_kill(etype, player.shade):
|
||||||
q = quest_manager.get(qid)
|
q = quest_manager.get(qid)
|
||||||
if q:
|
if q:
|
||||||
pickup_feed.push_plain(f"Quest fulfilled: {q.name} - return to the quest object!")
|
pickup_feed.push_plain(f"Quest fulfilled: {q.name} - return to the quest object!")
|
||||||
@@ -7874,6 +8066,10 @@ while running:
|
|||||||
|
|
||||||
# Quest object hint bubble (shown near the player)
|
# Quest object hint bubble (shown near the player)
|
||||||
near_qg = get_near_quest_giver(player, quest_givers)
|
near_qg = get_near_quest_giver(player, quest_givers)
|
||||||
|
# Shade-gated quests are only interactable while in shade form; in light
|
||||||
|
# form the object is visible but shows no interaction prompt.
|
||||||
|
if near_qg and quest_manager.requires_shade(near_qg.quest_id) and not player.shade:
|
||||||
|
near_qg = None
|
||||||
if near_qg:
|
if near_qg:
|
||||||
q = quest_manager.get(near_qg.quest_id)
|
q = quest_manager.get(near_qg.quest_id)
|
||||||
label = "Press F"
|
label = "Press F"
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"saved_at": "2026-09-29T18:54:05",
|
"saved_at": "2026-09-30T11:51:08",
|
||||||
"player": {
|
"player": {
|
||||||
"room_id": "room_02.json",
|
"room_id": "room_04.json",
|
||||||
"x": 774,
|
"x": 590,
|
||||||
"y": 320,
|
"y": 240,
|
||||||
"health": 10,
|
"health": 10,
|
||||||
"charge": 100.0,
|
"charge": 100.0,
|
||||||
"shade": false,
|
"shade": false,
|
||||||
"coins_light": 4,
|
"coins_light": 39,
|
||||||
"coins_dark": 0,
|
"coins_dark": 4,
|
||||||
"butterfly_jars": 1,
|
"butterfly_jars": 2,
|
||||||
"has_ranged": false,
|
"has_ranged": false,
|
||||||
"reward_chest_opened": false,
|
"opened_chests": [
|
||||||
|
"room_03.json@90,194"
|
||||||
|
],
|
||||||
"equipment": {
|
"equipment": {
|
||||||
"helmet": null,
|
"helmet": null,
|
||||||
"chest": null,
|
"chest": null,
|
||||||
@@ -24,6 +26,9 @@
|
|||||||
"quests": {
|
"quests": {
|
||||||
"active": {},
|
"active": {},
|
||||||
"completed_one_time": [],
|
"completed_one_time": [],
|
||||||
|
"ever_completed": [
|
||||||
|
"quest_first_kills"
|
||||||
|
],
|
||||||
"tokens": 0
|
"tokens": 0
|
||||||
},
|
},
|
||||||
"boss_diary": {
|
"boss_diary": {
|
||||||
@@ -32,8 +37,8 @@
|
|||||||
"defeated_bosses": [],
|
"defeated_bosses": [],
|
||||||
"broken_doors": [],
|
"broken_doors": [],
|
||||||
"lantern_respawn": {
|
"lantern_respawn": {
|
||||||
"room_id": "room_02.json",
|
"room_id": "room_04.json",
|
||||||
"x": 720,
|
"x": 560,
|
||||||
"y": 320
|
"y": 240
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 1,
|
|
||||||
"saved_at": "2026-09-29T12:14:42",
|
|
||||||
"player": {
|
|
||||||
"room_id": "room_town_gloomreach.json",
|
|
||||||
"x": 1250,
|
|
||||||
"y": 360,
|
|
||||||
"health": 12,
|
|
||||||
"charge": 125.0,
|
|
||||||
"shade": false,
|
|
||||||
"coins_light": 0,
|
|
||||||
"coins_dark": 0,
|
|
||||||
"butterfly_jars": 3,
|
|
||||||
"has_ranged": true,
|
|
||||||
"reward_chest_opened": true,
|
|
||||||
"equipment": {
|
|
||||||
"helmet": {
|
|
||||||
"id": "adaptive_traveller_helm",
|
|
||||||
"name": "Adaptive Traveller's Helmet",
|
|
||||||
"set": "adaptive_traveller",
|
|
||||||
"slot": "helmet",
|
|
||||||
"base_stats": {},
|
|
||||||
"rarity_overrides": {
|
|
||||||
"tempered": {
|
|
||||||
"extra_stats": {
|
|
||||||
"charge": 10
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"attuned": {
|
|
||||||
"extra_stats": {
|
|
||||||
"charge": 15
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"awakened": {
|
|
||||||
"extra_stats": {
|
|
||||||
"charge": 15,
|
|
||||||
"charge_regen_pct": 0.15
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"rarity": "tempered",
|
|
||||||
"tier": 1,
|
|
||||||
"stats": {
|
|
||||||
"charge": 10.0
|
|
||||||
},
|
|
||||||
"abilities": []
|
|
||||||
},
|
|
||||||
"chest": {
|
|
||||||
"id": "dawnrunner_chest",
|
|
||||||
"name": "Dawnrunner Chestplate",
|
|
||||||
"set": "dawnrunner",
|
|
||||||
"slot": "chest",
|
|
||||||
"base_stats": {},
|
|
||||||
"rarity_overrides": {
|
|
||||||
"tempered": {
|
|
||||||
"extra_stats": {
|
|
||||||
"charge": 10
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"attuned": {
|
|
||||||
"extra_stats": {
|
|
||||||
"charge": 15,
|
|
||||||
"move_speed_light_pct": 0.05
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"awakened": {
|
|
||||||
"extra_stats": {
|
|
||||||
"charge": 15,
|
|
||||||
"move_speed_light_pct": 0.05,
|
|
||||||
"heal_plus": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"rarity": "attuned",
|
|
||||||
"tier": 2,
|
|
||||||
"stats": {
|
|
||||||
"charge": 15.0,
|
|
||||||
"move_speed_light_pct": 0.05
|
|
||||||
},
|
|
||||||
"abilities": []
|
|
||||||
},
|
|
||||||
"pants": {
|
|
||||||
"id": "dawnrunner_greaves",
|
|
||||||
"name": "Dawnrunner Greaves",
|
|
||||||
"set": "dawnrunner",
|
|
||||||
"slot": "pants",
|
|
||||||
"base_stats": {},
|
|
||||||
"rarity_overrides": {
|
|
||||||
"tempered": {
|
|
||||||
"extra_stats": {
|
|
||||||
"heal_speed_pct": 0.1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"attuned": {
|
|
||||||
"extra_stats": {
|
|
||||||
"heal_speed_pct": 0.1,
|
|
||||||
"move_speed_light_pct": 0.1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"awakened": {
|
|
||||||
"extra_stats": {
|
|
||||||
"heal_speed_pct": 0.15,
|
|
||||||
"move_speed_light_pct": 0.15
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"rarity": "tempered",
|
|
||||||
"tier": 1,
|
|
||||||
"stats": {
|
|
||||||
"heal_speed_pct": 0.1
|
|
||||||
},
|
|
||||||
"abilities": []
|
|
||||||
},
|
|
||||||
"necklace": {
|
|
||||||
"id": "necklace_placeholder",
|
|
||||||
"name": "Plain Necklace",
|
|
||||||
"slot": "necklace",
|
|
||||||
"item_type": "necklace",
|
|
||||||
"base_stats": {
|
|
||||||
"health": 2
|
|
||||||
},
|
|
||||||
"rarity": "",
|
|
||||||
"tier": 0,
|
|
||||||
"abilities": [],
|
|
||||||
"stats": {
|
|
||||||
"health": 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"inventory": [
|
|
||||||
{
|
|
||||||
"id": "twister_helm",
|
|
||||||
"name": "Twister Helmet",
|
|
||||||
"set": "twister",
|
|
||||||
"slot": "helmet",
|
|
||||||
"base_stats": {},
|
|
||||||
"rarity_overrides": {
|
|
||||||
"tempered": {
|
|
||||||
"extra_stats": {
|
|
||||||
"attack_speed_pct": 0.1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"attuned": {
|
|
||||||
"extra_stats": {
|
|
||||||
"attack_speed_pct": 0.15
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"awakened": {
|
|
||||||
"extra_stats": {
|
|
||||||
"attack_speed_pct": 0.15,
|
|
||||||
"charge_regen_pct": 0.15
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"rarity": "tempered",
|
|
||||||
"tier": 1,
|
|
||||||
"stats": {
|
|
||||||
"attack_speed_pct": 0.1
|
|
||||||
},
|
|
||||||
"abilities": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "dawnrunner_helm",
|
|
||||||
"name": "Dawnrunner Helmet",
|
|
||||||
"set": "dawnrunner",
|
|
||||||
"slot": "helmet",
|
|
||||||
"base_stats": {},
|
|
||||||
"rarity_overrides": {
|
|
||||||
"tempered": {
|
|
||||||
"extra_stats": {
|
|
||||||
"move_speed_light_pct": 0.1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"attuned": {
|
|
||||||
"extra_stats": {
|
|
||||||
"move_speed_light_pct": 0.1,
|
|
||||||
"heal_speed_pct": 0.1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"awakened": {
|
|
||||||
"extra_stats": {
|
|
||||||
"move_speed_light_pct": 0.15,
|
|
||||||
"heal_speed_pct": 0.2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"rarity": "tempered",
|
|
||||||
"tier": 1,
|
|
||||||
"stats": {
|
|
||||||
"move_speed_light_pct": 0.1
|
|
||||||
},
|
|
||||||
"abilities": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"quests": {
|
|
||||||
"active": {
|
|
||||||
"quest_first_kills": {
|
|
||||||
"progress": 21
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"completed_one_time": [],
|
|
||||||
"tokens": 0
|
|
||||||
},
|
|
||||||
"boss_diary": {
|
|
||||||
"slotted": {
|
|
||||||
"boss_01": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"defeated_bosses": [
|
|
||||||
"room_boss_01.json"
|
|
||||||
],
|
|
||||||
"broken_doors": [
|
|
||||||
"room_07.json:-980,2180,80,600",
|
|
||||||
"room_07.json:1940,2720,360,20",
|
|
||||||
"room_07.json:4100,2180,40,600",
|
|
||||||
"room_reward_01.json:1580,60,40,300",
|
|
||||||
"room_reward_01.json:1700,60,60,300"
|
|
||||||
],
|
|
||||||
"lantern_respawn": {
|
|
||||||
"room_id": "room_town_gloomreach.json",
|
|
||||||
"x": 1300,
|
|
||||||
"y": 360
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user