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_shade": -9.0,
|
||||
"gravity_light": 0.5,
|
||||
"gravity_shade": 0.4
|
||||
"gravity_shade": 0.4,
|
||||
"shade_door_grace": 0.6
|
||||
},
|
||||
|
||||
"resources": {
|
||||
|
||||
@@ -93,49 +93,56 @@
|
||||
"w": 20,
|
||||
"h": 160,
|
||||
"group": "trigger",
|
||||
"layer": 0
|
||||
"layer": 0,
|
||||
"quest": "quest_first_kills"
|
||||
},
|
||||
{
|
||||
"x": 1100,
|
||||
"y": 620,
|
||||
"w": 320,
|
||||
"h": 20,
|
||||
"group": "ground"
|
||||
"group": "ground",
|
||||
"layer": 0
|
||||
},
|
||||
{
|
||||
"x": 800,
|
||||
"y": 320,
|
||||
"w": 160,
|
||||
"h": 20,
|
||||
"group": "ground"
|
||||
"group": "ground",
|
||||
"layer": 0
|
||||
},
|
||||
{
|
||||
"x": 1580,
|
||||
"y": 320,
|
||||
"w": 20,
|
||||
"h": 580,
|
||||
"group": "ground"
|
||||
"group": "ground",
|
||||
"layer": 0
|
||||
},
|
||||
{
|
||||
"x": 1220,
|
||||
"y": 760,
|
||||
"w": 360,
|
||||
"h": 20,
|
||||
"group": "ground"
|
||||
"group": "ground",
|
||||
"layer": 0
|
||||
},
|
||||
{
|
||||
"x": 20,
|
||||
"y": 880,
|
||||
"w": 1560,
|
||||
"h": 20,
|
||||
"group": "ground"
|
||||
"group": "ground",
|
||||
"layer": 0
|
||||
},
|
||||
{
|
||||
"x": 20,
|
||||
"y": 640,
|
||||
"w": 20,
|
||||
"h": 240,
|
||||
"group": "ground"
|
||||
"group": "ground",
|
||||
"layer": 0
|
||||
},
|
||||
{
|
||||
"x": 160,
|
||||
@@ -143,28 +150,33 @@
|
||||
"w": 20,
|
||||
"h": 240,
|
||||
"group": "trigger",
|
||||
"name": "first_quest_complete"
|
||||
"layer": 0,
|
||||
"name": "first_quest_complete",
|
||||
"quest_turned_in": "quest_first_kills"
|
||||
},
|
||||
{
|
||||
"x": 580,
|
||||
"y": 640,
|
||||
"w": 20,
|
||||
"h": 120,
|
||||
"group": "ground"
|
||||
"group": "ground",
|
||||
"layer": 0
|
||||
},
|
||||
{
|
||||
"x": 480,
|
||||
"y": 740,
|
||||
"w": 100,
|
||||
"h": 20,
|
||||
"group": "ground"
|
||||
"group": "ground",
|
||||
"layer": 0
|
||||
},
|
||||
{
|
||||
"x": 400,
|
||||
"y": 400,
|
||||
"w": 100,
|
||||
"h": 20,
|
||||
"group": "ground"
|
||||
"group": "ground",
|
||||
"layer": 0
|
||||
},
|
||||
{
|
||||
"x": 220,
|
||||
@@ -172,6 +184,7 @@
|
||||
"w": 80,
|
||||
"h": 20,
|
||||
"group": "moving",
|
||||
"layer": 0,
|
||||
"path": [
|
||||
[
|
||||
220,
|
||||
@@ -188,41 +201,48 @@
|
||||
"y": 220,
|
||||
"w": 140,
|
||||
"h": 20,
|
||||
"group": "ground"
|
||||
"group": "ground",
|
||||
"layer": 0
|
||||
},
|
||||
{
|
||||
"x": 1520,
|
||||
"y": 640,
|
||||
"w": 60,
|
||||
"h": 20,
|
||||
"group": "ground"
|
||||
"group": "ground",
|
||||
"layer": 0
|
||||
}
|
||||
],
|
||||
"enemies": [
|
||||
{
|
||||
"x": 600,
|
||||
"y": 840,
|
||||
"type": "enemy"
|
||||
"type": "enemy",
|
||||
"layer": 0
|
||||
},
|
||||
{
|
||||
"x": 960,
|
||||
"y": 840,
|
||||
"type": "enemy"
|
||||
"type": "enemy",
|
||||
"layer": 0
|
||||
},
|
||||
{
|
||||
"x": 200,
|
||||
"y": 580,
|
||||
"type": "enemy"
|
||||
"type": "enemy",
|
||||
"layer": 0
|
||||
},
|
||||
{
|
||||
"x": 520,
|
||||
"y": 700,
|
||||
"type": "enemy"
|
||||
"type": "enemy",
|
||||
"layer": 0
|
||||
},
|
||||
{
|
||||
"x": 1520,
|
||||
"y": 840,
|
||||
"type": "enemy"
|
||||
"type": "enemy",
|
||||
"layer": 0
|
||||
}
|
||||
],
|
||||
"npcs": [],
|
||||
@@ -235,6 +255,28 @@
|
||||
"object_id": "quest_point",
|
||||
"layer": 0,
|
||||
"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": [
|
||||
@@ -257,12 +299,14 @@
|
||||
"y": 780,
|
||||
"w": 80,
|
||||
"h": 100,
|
||||
"to": "",
|
||||
"spawn_id": "",
|
||||
"to": "room_04",
|
||||
"spawn_id": "room4_top_right",
|
||||
"spawn": [
|
||||
120,
|
||||
780
|
||||
]
|
||||
1480,
|
||||
100
|
||||
],
|
||||
"hint": "",
|
||||
"layer": 0
|
||||
}
|
||||
],
|
||||
"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.quest) out.quest = String(rect.quest);
|
||||
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);
|
||||
return out;
|
||||
}
|
||||
@@ -505,13 +508,14 @@
|
||||
if(Array.isArray(src.ability_doors)){
|
||||
for(const d of src.ability_doors){
|
||||
if(!d) continue;
|
||||
const kind = (typeof d === "object" && d.kind === "shade") ? "shade" : "ranged";
|
||||
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"){
|
||||
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 {
|
||||
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 [sx,sy] = worldToScreen(d.x, d.y, canvas);
|
||||
const [sx2,sy2] = worldToScreen(d.x+d.w, d.y+d.h, canvas);
|
||||
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);
|
||||
if(d.kind === "shade"){
|
||||
ctx.fillStyle = "rgba(129,110,190,0.20)";
|
||||
ctx.fillRect(sx,sy, sx2-sx, sy2-sy);
|
||||
ctx.strokeStyle = "rgba(129,110,190,0.80)";
|
||||
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"){
|
||||
const ex = it.ex;
|
||||
const [sx,sy] = worldToScreen(ex.x, ex.y, canvas);
|
||||
@@ -1948,7 +1960,10 @@ Cancel = Choose whether to remove without saving`);
|
||||
} else if(sid){
|
||||
pos = (dstWrap.data.entry_spawns && dstWrap.data.entry_spawns[sid]) ? dstWrap.data.entry_spawns[sid] : null;
|
||||
}
|
||||
if(!pos && Array.isArray(ex.spawn) && ex.spawn.length >= 2){
|
||||
// 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;
|
||||
}
|
||||
if(!pos) continue;
|
||||
@@ -2143,6 +2158,16 @@ Cancel = Choose whether to remove without saving`);
|
||||
<div style="margin-top:8px;">
|
||||
<div class="mini">unlock when quest complete (id)</div>
|
||||
<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>` : ""}
|
||||
${(p.group||"ground") === "hazard" ? `
|
||||
<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">layer (lower = back)</div><input id="adlayer" type="number" value="${d.layer||0}"/></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;">
|
||||
<button id="delSel" class="danger">Delete</button>
|
||||
</div>
|
||||
@@ -2376,6 +2408,16 @@ Cancel = Choose whether to remove without saving`);
|
||||
const v = ptqcomplete.value.trim();
|
||||
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 hzrespawn=q("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 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(()=>{
|
||||
room.ability_doors.splice(sel.index,1);
|
||||
state.selection=null;
|
||||
@@ -3279,6 +3322,9 @@ Cancel = Choose whether to remove without saving`);
|
||||
if(p.name) obj.name = p.name;
|
||||
if(p.quest) obj.quest = p.quest;
|
||||
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;
|
||||
platforms[group].push(obj);
|
||||
} else {
|
||||
@@ -3315,10 +3361,11 @@ Cancel = Choose whether to remove without saving`);
|
||||
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 out = { rect: [ad.x, ad.y, ad.w, ad.h] };
|
||||
if(ad.layer) out.layer = ad.layer;
|
||||
if(ad.kind === "shade") out.kind = "shade";
|
||||
return out;
|
||||
});
|
||||
|
||||
@@ -3850,6 +3897,14 @@ els.canvasA.addEventListener("pointerdown", onPointerDown);
|
||||
e.preventDefault();
|
||||
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'){
|
||||
e.preventDefault();
|
||||
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))
|
||||
GRAVITY_LIGHT = float(_phys(["player", "gravity_light"], 0.5))
|
||||
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)
|
||||
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"]
|
||||
if "quest_complete" in p:
|
||||
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:
|
||||
obj["respawn"] = p["respawn"]
|
||||
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]]})
|
||||
elif isinstance(d, dict):
|
||||
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")):
|
||||
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
|
||||
|
||||
return out
|
||||
@@ -1077,6 +1089,12 @@ class Room:
|
||||
meta["quest"] = str(tp.get("quest"))
|
||||
if 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.platforms_trigger.append(rect)
|
||||
elif isinstance(tp, (list, tuple)) and len(tp) >= 4:
|
||||
@@ -1155,7 +1173,7 @@ class Room:
|
||||
rect = list(d)
|
||||
if rect is None:
|
||||
continue
|
||||
nd = {"rect": rect}
|
||||
nd = {"rect": rect, "kind": d.get("kind", "ranged") if isinstance(d, dict) else "ranged"}
|
||||
if isinstance(d, dict):
|
||||
try:
|
||||
nd["layer"] = int(d.get("layer", 0))
|
||||
@@ -1421,6 +1439,8 @@ class Projectile:
|
||||
for d in doors:
|
||||
if getattr(d, "open", False):
|
||||
continue
|
||||
if getattr(d, "kind", "ranged") == "shade":
|
||||
continue # shade doors cannot be shot open
|
||||
if self.rect.colliderect(d.rect):
|
||||
d.open = True
|
||||
mark_door_broken(getattr(d, "room_id", None), d.rect)
|
||||
@@ -1906,17 +1926,22 @@ class CoinPickup:
|
||||
|
||||
|
||||
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.open = False
|
||||
self.room_id = room_id
|
||||
self.kind = "shade" if str(kind) == "shade" else "ranged"
|
||||
|
||||
def draw(self, surface, cx, cy):
|
||||
if self.open:
|
||||
return
|
||||
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)
|
||||
pygame.draw.rect(surface, (40, 80, 40), r, 2)
|
||||
if self.kind == "shade":
|
||||
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]:
|
||||
@@ -1925,7 +1950,7 @@ def spawn_ability_doors(room) -> list[AbilityDoor]:
|
||||
if not isinstance(d, dict):
|
||||
continue
|
||||
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.layer = int(d.get("layer", 0))
|
||||
out.append(door)
|
||||
@@ -1942,8 +1967,12 @@ class RewardChest:
|
||||
if self.opened:
|
||||
return False
|
||||
light_amt, dark_amt = self.coins
|
||||
player.coins_light += int(light_amt)
|
||||
player.coins_dark += int(dark_amt)
|
||||
cx = self.rect.centerx
|
||||
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
|
||||
return True
|
||||
|
||||
@@ -3260,6 +3289,7 @@ class Player:
|
||||
self.on_ground = False
|
||||
self.ground_platform = None
|
||||
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.health = int(BASE_MAX_HEALTH)
|
||||
@@ -3601,6 +3631,7 @@ class Player:
|
||||
if not self.shade and self.charge <= 0:
|
||||
return
|
||||
self.shade = not self.shade
|
||||
self.shade_door_grace = SHADE_DOOR_GRACE
|
||||
|
||||
def attack(self):
|
||||
if self.healing:
|
||||
@@ -3813,6 +3844,9 @@ class Player:
|
||||
self.just_air_jumped = False
|
||||
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:
|
||||
self.invincibility_timer -= dt
|
||||
|
||||
@@ -3967,6 +4001,7 @@ class Player:
|
||||
if self.charge <= 0:
|
||||
self.charge = 0
|
||||
self.shade = False
|
||||
self.shade_door_grace = SHADE_DOOR_GRACE
|
||||
else:
|
||||
# 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):
|
||||
@@ -4164,6 +4199,9 @@ class Enemy:
|
||||
self.drop_ignore_timer = 0.0
|
||||
self.drop_ignore_platform = None
|
||||
|
||||
# True when killed by touching a hazard zone (drops nothing).
|
||||
self.hazard_kill = False
|
||||
|
||||
|
||||
def take_damage(self, 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.vel_x = 0.0
|
||||
self.vel_y = 0.0
|
||||
self.hazard_kill = False
|
||||
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.health = float(self.max_health)
|
||||
@@ -4719,6 +4758,8 @@ def spawn_enemies_from_room(room: Room):
|
||||
t = str(ent.get("type", "enemy"))
|
||||
if t == "boss" and is_boss_defeated(room.id):
|
||||
continue
|
||||
if t == "miniboss" and room.id in DEFEATED_MINIBOSSES:
|
||||
continue
|
||||
pos = ent.get("pos", None)
|
||||
if not (isinstance(pos, (list, tuple)) and len(pos) == 2):
|
||||
if "x" in ent and "y" in ent:
|
||||
@@ -4939,18 +4980,79 @@ class HouseObject:
|
||||
|
||||
def trigger_wall_blocking(rect) -> bool:
|
||||
"""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
|
||||
(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)."""
|
||||
meta = platform_meta.get(id(rect), {})
|
||||
qid = meta.get("quest")
|
||||
if qid:
|
||||
return qid not in quest_manager.active
|
||||
qc = meta.get("quest_complete")
|
||||
if qc:
|
||||
return not (quest_manager.is_complete(qc) or qc in quest_manager.ever_completed)
|
||||
return False
|
||||
if meta.get("lock_when_pass"):
|
||||
present = id(rect) in LOCKED_TRIGGER_WALLS
|
||||
elif qid:
|
||||
present = qid not in quest_manager.active and qid not in quest_manager.ever_completed
|
||||
elif meta.get("quest_complete"):
|
||||
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]] = {}
|
||||
@@ -5228,7 +5330,7 @@ def _draw_tether_chain_loop(surface, rect: pygame.Rect, light: bool, cx: float,
|
||||
|
||||
|
||||
def get_pause_menu_rects():
|
||||
items = ["Resume", "Save Game", "Settings", "Main Menu", "Close Game"]
|
||||
items = ["Resume", "Settings", "Main Menu", "Close Game"]
|
||||
rects = []
|
||||
start_y = HEIGHT // 2 + 20
|
||||
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):
|
||||
items = ["Resume", "Save Game", "Settings", "Main Menu", "Close Game"]
|
||||
items = ["Resume", "Settings", "Main Menu", "Close Game"]
|
||||
rects = get_pause_menu_rects()
|
||||
mouse_pos = pygame.mouse.get_pos()
|
||||
hover_idx = None
|
||||
@@ -5557,6 +5659,7 @@ def apply_room(room: Room, set_player_spawn=True):
|
||||
global token_drops
|
||||
global ACTIVE_REFIGHT
|
||||
global ROOM_DROP_MULT, ROOM_COIN_MULT
|
||||
reset_trigger_wall_locks()
|
||||
|
||||
|
||||
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.vel_y = 0.0
|
||||
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:
|
||||
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."""
|
||||
global enemies, npcs, loot_drops, shockwaves, coin_drops, token_drops, projectiles
|
||||
global shatter_platforms, shatter_aoes, dust_puffs, tether_shots, ability_doors
|
||||
reset_trigger_wall_locks()
|
||||
enemies = spawn_enemies_from_room(ROOM)
|
||||
npcs = spawn_npcs_from_room(ROOM)
|
||||
loot_drops = []
|
||||
@@ -5722,6 +5828,10 @@ camera_x = camera_y = 0.0
|
||||
# Track bosses defeated per room id (runtime only)
|
||||
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)
|
||||
ROOM_DROP_MULT = 1.0
|
||||
ROOM_COIN_MULT = 1.0
|
||||
@@ -5734,6 +5844,17 @@ def mark_boss_defeated(room_id: str):
|
||||
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
|
||||
# ----------------------------
|
||||
@@ -5856,6 +5977,11 @@ class QuestManager:
|
||||
def get(self, quest_id: str) -> Quest | None:
|
||||
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]:
|
||||
return [q for q in self.quests.values() if q.giver == giver_id]
|
||||
|
||||
@@ -5899,8 +6025,10 @@ class QuestManager:
|
||||
else:
|
||||
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.
|
||||
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."""
|
||||
completed = []
|
||||
for qid in list(self.active.keys()):
|
||||
@@ -5910,6 +6038,8 @@ class QuestManager:
|
||||
obj = q.objective
|
||||
if obj.get("kind") != "kill" or str(obj.get("target", "")) != enemy_type:
|
||||
continue
|
||||
if obj.get("shade") and not shade:
|
||||
continue
|
||||
was = self.is_complete(qid)
|
||||
self.progress(qid, 1)
|
||||
if not was and self.is_complete(qid):
|
||||
@@ -5994,6 +6124,17 @@ QUEST_DEFS_DEFAULT = [
|
||||
"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()
|
||||
@@ -6203,6 +6344,8 @@ def save_at_lantern(sp: SavePoint):
|
||||
pickup_feed.push_center("Lantern - game saved, fully restored", duration=2.0)
|
||||
if respawn_jars_if_elsewhere():
|
||||
pickup_feed.push_center("Butterfly jars have respawned!", duration=2.5)
|
||||
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:
|
||||
@@ -6645,8 +6788,9 @@ def apply_save_data(data: dict):
|
||||
DEFEATED_BOSSES.update(data.get("defeated_bosses") or [])
|
||||
BROKEN_DOORS.clear()
|
||||
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()
|
||||
DEFEATED_MINIBOSSES.clear()
|
||||
JAR_DEATH_ROOM = None
|
||||
corpse = None
|
||||
|
||||
@@ -6671,6 +6815,8 @@ def apply_save_data(data: dict):
|
||||
apply_room(room, set_player_spawn=True)
|
||||
|
||||
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:
|
||||
player.health = int(pdata["health"])
|
||||
if "charge" in pdata:
|
||||
@@ -6759,8 +6905,6 @@ def _do_save_game():
|
||||
def _return_to_main_menu():
|
||||
global GAME_MODE, paused, pause_menu, pause_menu_index, pause_use_mouse
|
||||
global settings_wait_action
|
||||
if CURRENT_SAVE_NAME:
|
||||
_do_save_game()
|
||||
inv_ui.visible = False
|
||||
trade_ui.visible = False
|
||||
trade_ui.active_npc = None
|
||||
@@ -7190,12 +7334,10 @@ while running:
|
||||
if idx == 0:
|
||||
paused = False
|
||||
elif idx == 1:
|
||||
_do_save_game()
|
||||
elif idx == 2:
|
||||
pause_menu = "settings"
|
||||
elif idx == 3:
|
||||
elif idx == 2:
|
||||
_return_to_main_menu()
|
||||
elif idx == 4:
|
||||
elif idx == 3:
|
||||
running = False
|
||||
break
|
||||
|
||||
@@ -7210,21 +7352,19 @@ while running:
|
||||
|
||||
if paused and pause_menu == "main":
|
||||
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
|
||||
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
|
||||
elif e.key in (key_for("ui_confirm"), key_for("ui_confirm_alt")):
|
||||
if pause_menu_index == 0:
|
||||
paused = False
|
||||
elif pause_menu_index == 1:
|
||||
_do_save_game()
|
||||
elif pause_menu_index == 2:
|
||||
pause_menu = "settings"
|
||||
elif pause_menu_index == 3:
|
||||
elif pause_menu_index == 2:
|
||||
_return_to_main_menu()
|
||||
elif pause_menu_index == 4:
|
||||
elif pause_menu_index == 3:
|
||||
running = False
|
||||
elif key_is("pause", e.key):
|
||||
paused = False
|
||||
@@ -7271,7 +7411,11 @@ while running:
|
||||
else:
|
||||
near_qg = get_near_quest_giver(player, quest_givers)
|
||||
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 paused:
|
||||
@@ -7479,7 +7623,22 @@ while running:
|
||||
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
|
||||
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).
|
||||
if player.shade:
|
||||
@@ -7500,6 +7659,24 @@ while running:
|
||||
player.ground_platform = rect
|
||||
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
|
||||
# (defaults to the room's player spawn). A lethal hit falls through to the normal death respawn.
|
||||
for hz in platforms_hazard:
|
||||
@@ -7592,10 +7769,21 @@ while running:
|
||||
player.shatterstep_landing_pending = False
|
||||
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
|
||||
for en in enemies:
|
||||
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.
|
||||
# (Prevents enemies from shoving each other into platforms / off ledges.)
|
||||
# resolve_enemy_collisions(enemies)
|
||||
@@ -7702,6 +7890,8 @@ while running:
|
||||
if en.health > 0:
|
||||
alive.append(en)
|
||||
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))
|
||||
etype = getattr(en, "type_id", "")
|
||||
if etype == "boss":
|
||||
@@ -7709,9 +7899,11 @@ while running:
|
||||
bid = boss_id_for_room(ROOM.id)
|
||||
if bid and ACTIVE_REFIGHT is None:
|
||||
token_drops.append(TokenPickup(en.rect.centerx - 8, en.rect.top - 20, bid))
|
||||
elif etype == "miniboss":
|
||||
DEFEATED_MINIBOSSES.add(ROOM.id)
|
||||
|
||||
# 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)
|
||||
if q:
|
||||
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)
|
||||
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:
|
||||
q = quest_manager.get(near_qg.quest_id)
|
||||
label = "Press F"
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
{
|
||||
"version": 1,
|
||||
"saved_at": "2026-09-29T18:54:05",
|
||||
"saved_at": "2026-09-30T11:51:08",
|
||||
"player": {
|
||||
"room_id": "room_02.json",
|
||||
"x": 774,
|
||||
"y": 320,
|
||||
"room_id": "room_04.json",
|
||||
"x": 590,
|
||||
"y": 240,
|
||||
"health": 10,
|
||||
"charge": 100.0,
|
||||
"shade": false,
|
||||
"coins_light": 4,
|
||||
"coins_dark": 0,
|
||||
"butterfly_jars": 1,
|
||||
"coins_light": 39,
|
||||
"coins_dark": 4,
|
||||
"butterfly_jars": 2,
|
||||
"has_ranged": false,
|
||||
"reward_chest_opened": false,
|
||||
"opened_chests": [
|
||||
"room_03.json@90,194"
|
||||
],
|
||||
"equipment": {
|
||||
"helmet": null,
|
||||
"chest": null,
|
||||
@@ -24,6 +26,9 @@
|
||||
"quests": {
|
||||
"active": {},
|
||||
"completed_one_time": [],
|
||||
"ever_completed": [
|
||||
"quest_first_kills"
|
||||
],
|
||||
"tokens": 0
|
||||
},
|
||||
"boss_diary": {
|
||||
@@ -32,8 +37,8 @@
|
||||
"defeated_bosses": [],
|
||||
"broken_doors": [],
|
||||
"lantern_respawn": {
|
||||
"room_id": "room_02.json",
|
||||
"x": 720,
|
||||
"y": 320
|
||||
"room_id": "room_04.json",
|
||||
"x": 560,
|
||||
"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