attack changes
dissolve platforms, mouse for attack, directional attack (i think), hold for attack
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"charge_cost": 5.0,
|
"charge_cost": 5.0,
|
||||||
"damage_mult": 0.5,
|
"damage_mult": 0.5,
|
||||||
"cooldown": 0.45,
|
"cooldown": 0.63,
|
||||||
"projectile": {
|
"projectile": {
|
||||||
"speed": 650.0,
|
"speed": 650.0,
|
||||||
"lifetime": 1.2,
|
"lifetime": 1.2,
|
||||||
|
|||||||
@@ -231,7 +231,7 @@
|
|||||||
"y": 540,
|
"y": 540,
|
||||||
"w": 60,
|
"w": 60,
|
||||||
"h": 20,
|
"h": 20,
|
||||||
"group": "moving",
|
"group": "dissolve",
|
||||||
"layer": 0,
|
"layer": 0,
|
||||||
"path": [
|
"path": [
|
||||||
[
|
[
|
||||||
@@ -242,7 +242,9 @@
|
|||||||
820,
|
820,
|
||||||
600
|
600
|
||||||
]
|
]
|
||||||
]
|
],
|
||||||
|
"stand_time": 2,
|
||||||
|
"dissolve_time": 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 1040,
|
"x": 1040,
|
||||||
|
|||||||
@@ -16,15 +16,15 @@
|
|||||||
120
|
120
|
||||||
],
|
],
|
||||||
"room5_bottom_right": [
|
"room5_bottom_right": [
|
||||||
1400,
|
1420,
|
||||||
440
|
780
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"platforms": [
|
"platforms": [
|
||||||
{
|
{
|
||||||
"x": 0,
|
"x": 20,
|
||||||
"y": 180,
|
"y": 180,
|
||||||
"w": 240,
|
"w": 220,
|
||||||
"h": 20,
|
"h": 20,
|
||||||
"group": "ground",
|
"group": "ground",
|
||||||
"layer": 0
|
"layer": 0
|
||||||
@@ -33,15 +33,7 @@
|
|||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
"w": 20,
|
"w": 20,
|
||||||
"h": 180,
|
"h": 840,
|
||||||
"group": "ground",
|
|
||||||
"layer": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x": 20,
|
|
||||||
"y": 0,
|
|
||||||
"w": 1520,
|
|
||||||
"h": 20,
|
|
||||||
"group": "ground",
|
"group": "ground",
|
||||||
"layer": 0
|
"layer": 0
|
||||||
},
|
},
|
||||||
@@ -62,83 +54,83 @@
|
|||||||
"layer": 0
|
"layer": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 860,
|
"x": 0,
|
||||||
"y": 500,
|
"y": 840,
|
||||||
"w": 680,
|
"w": 1520,
|
||||||
"h": 20,
|
"h": 20,
|
||||||
"group": "ground",
|
"group": "ground",
|
||||||
"layer": 0
|
"layer": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 1520,
|
"x": 1520,
|
||||||
"y": 380,
|
"y": 680,
|
||||||
"w": 20,
|
"w": 20,
|
||||||
"h": 120,
|
"h": 180,
|
||||||
"group": "ground",
|
"group": "ground",
|
||||||
"layer": 0
|
"layer": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 1320,
|
"x": 1380,
|
||||||
"y": 360,
|
"y": 700,
|
||||||
"w": 220,
|
"w": 20,
|
||||||
|
"h": 140,
|
||||||
|
"group": "trigger",
|
||||||
|
"layer": 0,
|
||||||
|
"name": "open_when_boss_slain",
|
||||||
|
"open_on_boss_dead": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 1380,
|
||||||
|
"y": 680,
|
||||||
|
"w": 140,
|
||||||
"h": 20,
|
"h": 20,
|
||||||
"group": "ground",
|
"group": "trigger",
|
||||||
"layer": 0
|
"layer": 0,
|
||||||
|
"name": "open_when_boss_slain",
|
||||||
|
"open_on_boss_dead": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 1520,
|
"x": 1380,
|
||||||
"y": 20,
|
"y": 0,
|
||||||
"w": 20,
|
"w": 20,
|
||||||
"h": 340,
|
"h": 680,
|
||||||
"group": "ground",
|
"group": "ground",
|
||||||
"layer": 0
|
"layer": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 1180,
|
"x": 1040,
|
||||||
"y": 440,
|
"y": 740,
|
||||||
"w": 1,
|
"w": 160,
|
||||||
"h": 1,
|
|
||||||
"group": "ground"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x": 1180,
|
|
||||||
"y": 420,
|
|
||||||
"w": 60,
|
|
||||||
"h": 20,
|
"h": 20,
|
||||||
"group": "moving",
|
"group": "moving",
|
||||||
|
"layer": 0,
|
||||||
"path": [
|
"path": [
|
||||||
[
|
[
|
||||||
1180,
|
1040,
|
||||||
420
|
740
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
1180,
|
1040,
|
||||||
360
|
200
|
||||||
]
|
|
||||||
]
|
]
|
||||||
|
],
|
||||||
|
"open_on_boss_dead": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"enemies": [
|
"enemies": [
|
||||||
{
|
{
|
||||||
"x": 520,
|
"x": 620,
|
||||||
"y": 140,
|
"y": 720,
|
||||||
"type": "enemy",
|
"type": "boss",
|
||||||
"layer": 0,
|
"layer": 0,
|
||||||
"behavior": ""
|
"behavior": "charger"
|
||||||
},
|
|
||||||
{
|
|
||||||
"x": 1000,
|
|
||||||
"y": 460,
|
|
||||||
"type": "enemy",
|
|
||||||
"layer": 0,
|
|
||||||
"behavior": ""
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"npcs": [],
|
"npcs": [],
|
||||||
"props": [
|
"props": [
|
||||||
{
|
{
|
||||||
"x": 1400,
|
"x": 860,
|
||||||
"y": 360,
|
"y": 180,
|
||||||
"type": "save_point",
|
"type": "save_point",
|
||||||
"radius": 90,
|
"radius": 90,
|
||||||
"object_id": "lamp",
|
"object_id": "lamp",
|
||||||
@@ -163,14 +155,14 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"x": 1440,
|
"x": 1440,
|
||||||
"y": 400,
|
"y": 740,
|
||||||
"w": 80,
|
"w": 80,
|
||||||
"h": 100,
|
"h": 100,
|
||||||
"to": "room_06",
|
"to": "room_07",
|
||||||
"spawn_id": "room6_top_right",
|
"spawn_id": "room7_top_left",
|
||||||
"spawn": [
|
"spawn": [
|
||||||
520,
|
180,
|
||||||
240
|
460
|
||||||
],
|
],
|
||||||
"hint": "",
|
"hint": "",
|
||||||
"layer": 0
|
"layer": 0
|
||||||
@@ -178,5 +170,6 @@
|
|||||||
],
|
],
|
||||||
"ability_doors": [],
|
"ability_doors": [],
|
||||||
"room_w": 1600,
|
"room_w": 1600,
|
||||||
"room_h": 900
|
"room_h": 900,
|
||||||
|
"mist": false
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
"name": "room_07",
|
||||||
|
"display_name": "room_07",
|
||||||
|
"meta": {
|
||||||
|
"id": "room_07",
|
||||||
|
"name": "room_07"
|
||||||
|
},
|
||||||
|
"bounds": {},
|
||||||
|
"player_spawn": [
|
||||||
|
220,
|
||||||
|
440
|
||||||
|
],
|
||||||
|
"entry_spawns": {
|
||||||
|
"room7_top_left": [
|
||||||
|
180,
|
||||||
|
460
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"platforms": [
|
||||||
|
{
|
||||||
|
"x": 60,
|
||||||
|
"y": 200,
|
||||||
|
"w": 20,
|
||||||
|
"h": 340,
|
||||||
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 80,
|
||||||
|
"y": 520,
|
||||||
|
"w": 920,
|
||||||
|
"h": 20,
|
||||||
|
"group": "ground",
|
||||||
|
"layer": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"enemies": [],
|
||||||
|
"npcs": [],
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"x": 440,
|
||||||
|
"y": 494,
|
||||||
|
"type": "reward_chest",
|
||||||
|
"radius": 180,
|
||||||
|
"object_id": null,
|
||||||
|
"layer": 0,
|
||||||
|
"quest_id": "",
|
||||||
|
"coins": [
|
||||||
|
30,
|
||||||
|
10
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 820,
|
||||||
|
"y": 500,
|
||||||
|
"type": "ability_pickup",
|
||||||
|
"radius": 180,
|
||||||
|
"object_id": null,
|
||||||
|
"layer": 0,
|
||||||
|
"quest_id": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"x": 640,
|
||||||
|
"y": 520,
|
||||||
|
"type": "save_point",
|
||||||
|
"radius": 90,
|
||||||
|
"object_id": "lamp"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"exits": [
|
||||||
|
{
|
||||||
|
"x": 80,
|
||||||
|
"y": 420,
|
||||||
|
"w": 80,
|
||||||
|
"h": 100,
|
||||||
|
"to": "room_05",
|
||||||
|
"spawn_id": "room5_bottom_right",
|
||||||
|
"spawn": [
|
||||||
|
1420,
|
||||||
|
780
|
||||||
|
],
|
||||||
|
"hint": "",
|
||||||
|
"layer": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ability_doors": [],
|
||||||
|
"room_w": 1600,
|
||||||
|
"room_h": 900,
|
||||||
|
"mist": false
|
||||||
|
}
|
||||||
@@ -114,6 +114,7 @@
|
|||||||
<option value="hazard">Hazard (red - damage)</option>
|
<option value="hazard">Hazard (red - damage)</option>
|
||||||
<option value="moving">Moving (path)</option>
|
<option value="moving">Moving (path)</option>
|
||||||
<option value="trigger">Trigger (name)</option>
|
<option value="trigger">Trigger (name)</option>
|
||||||
|
<option value="dissolve">Dissolve (timed)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="muted" style="margin-top:8px;">
|
<div class="muted" style="margin-top:8px;">
|
||||||
@@ -361,7 +362,10 @@
|
|||||||
if(rect.quest_turned_in) out.quest_turned_in = String(rect.quest_turned_in);
|
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.lock_when_pass) out.lock_when_pass = true;
|
||||||
if(rect.open_on_miniboss_dead) out.open_on_miniboss_dead = true;
|
if(rect.open_on_miniboss_dead) out.open_on_miniboss_dead = true;
|
||||||
|
if(rect.open_on_boss_dead) out.open_on_boss_dead = true;
|
||||||
if(Array.isArray(rect.respawn)) out.respawn = deepClone(rect.respawn);
|
if(Array.isArray(rect.respawn)) out.respawn = deepClone(rect.respawn);
|
||||||
|
if(rect.stand_time !== undefined) out.stand_time = +rect.stand_time;
|
||||||
|
if(rect.dissolve_time !== undefined) out.dissolve_time = +rect.dissolve_time;
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -1448,6 +1452,8 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
save_point: { ring: "rgba(56,189,248,0.30)", fill: "rgba(56,189,248,0.9)", label: "Lantern" },
|
save_point: { ring: "rgba(56,189,248,0.30)", fill: "rgba(56,189,248,0.9)", label: "Lantern" },
|
||||||
butterfly_jar: { ring: "rgba(168,85,247,0.22)", fill: "rgba(168,85,247,0.9)", label: "Jar" },
|
butterfly_jar: { ring: "rgba(168,85,247,0.22)", fill: "rgba(168,85,247,0.9)", label: "Jar" },
|
||||||
diary: { ring: "rgba(150,110,70,0.25)", fill: "rgba(150,110,70,0.95)", label: "Diary" },
|
diary: { ring: "rgba(150,110,70,0.25)", fill: "rgba(150,110,70,0.95)", label: "Diary" },
|
||||||
|
reward_chest: { ring: "rgba(180,120,60,0.30)", fill: "rgba(190,125,70,0.95)", label: "Chest" },
|
||||||
|
ability_pickup:{ ring: "rgba(200,230,255,0.30)", fill: "rgba(200,230,255,0.95)", label: "Ability" },
|
||||||
};
|
};
|
||||||
const ptype = p.type || "light_source";
|
const ptype = p.type || "light_source";
|
||||||
const st = PROP_STYLES[ptype] || { ring: "rgba(148,163,184,0.25)", fill: "rgba(148,163,184,0.9)", label: ptype };
|
const st = PROP_STYLES[ptype] || { ring: "rgba(148,163,184,0.25)", fill: "rgba(148,163,184,0.9)", label: ptype };
|
||||||
@@ -1553,6 +1559,7 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
if(p && (p.group === "moving")) layerable.push({ z: +(p.layer||0), cat: 1, kind: "moving", p });
|
if(p && (p.group === "moving")) layerable.push({ z: +(p.layer||0), cat: 1, kind: "moving", p });
|
||||||
else if(p && (p.group === "trigger")) layerable.push({ z: +(p.layer||0), cat: 1, kind: "trigger", p });
|
else if(p && (p.group === "trigger")) layerable.push({ z: +(p.layer||0), cat: 1, kind: "trigger", p });
|
||||||
else if(p && (p.group === "hazard")) layerable.push({ z: +(p.layer||0), cat: 1, kind: "hazard", p });
|
else if(p && (p.group === "hazard")) layerable.push({ z: +(p.layer||0), cat: 1, kind: "hazard", p });
|
||||||
|
else if(p && (p.group === "dissolve")) layerable.push({ z: +(p.layer||0), cat: 1, kind: "dissolve", p });
|
||||||
else layerable.push({ z: +(p.layer||0), cat: 1, kind: "platform", p });
|
else layerable.push({ z: +(p.layer||0), cat: 1, kind: "platform", p });
|
||||||
}
|
}
|
||||||
for(const d of (room.ability_doors || [])) if(d) layerable.push({ z: +(d.layer||0), cat: 2, kind: "door", d });
|
for(const d of (room.ability_doors || [])) if(d) layerable.push({ z: +(d.layer||0), cat: 2, kind: "door", d });
|
||||||
@@ -1655,6 +1662,18 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.setLineDash([]);
|
ctx.setLineDash([]);
|
||||||
}
|
}
|
||||||
|
} else if(it.kind === "dissolve"){
|
||||||
|
const p = it.p;
|
||||||
|
const [sx,sy] = worldToScreen(p.x, p.y, canvas);
|
||||||
|
const [sx2,sy2] = worldToScreen(p.x+p.w, p.y+p.h, canvas);
|
||||||
|
ctx.fillStyle = "rgba(96,165,250,0.15)";
|
||||||
|
ctx.fillRect(sx,sy, sx2-sx, sy2-sy);
|
||||||
|
ctx.strokeStyle = "rgba(150,210,255,0.9)";
|
||||||
|
ctx.lineWidth = 2*dpr;
|
||||||
|
ctx.strokeRect(sx,sy, sx2-sx, sy2-sy);
|
||||||
|
ctx.fillStyle = "rgba(229,231,235,0.9)";
|
||||||
|
ctx.font = `${11*dpr}px system-ui`;
|
||||||
|
ctx.fillText("Dissolve", sx+4*dpr, sy-4*dpr);
|
||||||
} else if(it.kind === "door"){
|
} else if(it.kind === "door"){
|
||||||
const d = it.d;
|
const d = it.d;
|
||||||
const [sx,sy] = worldToScreen(d.x, d.y, canvas);
|
const [sx,sy] = worldToScreen(d.x, d.y, canvas);
|
||||||
@@ -2162,6 +2181,7 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
<option value="hazard" ${p.group==="hazard"?"selected":""}>Hazard (red - damage)</option>
|
<option value="hazard" ${p.group==="hazard"?"selected":""}>Hazard (red - damage)</option>
|
||||||
<option value="moving" ${p.group==="moving"?"selected":""}>Moving (path)</option>
|
<option value="moving" ${p.group==="moving"?"selected":""}>Moving (path)</option>
|
||||||
<option value="trigger" ${p.group==="trigger"?"selected":""}>Trigger (name)</option>
|
<option value="trigger" ${p.group==="trigger"?"selected":""}>Trigger (name)</option>
|
||||||
|
<option value="dissolve" ${p.group==="dissolve"?"selected":""}>Dissolve (timed)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
${(p.group||"ground") === "trigger" ? `
|
${(p.group||"ground") === "trigger" ? `
|
||||||
@@ -2186,6 +2206,9 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
</div>
|
</div>
|
||||||
<div style="margin-top:8px;">
|
<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>
|
<label><input id="ptminibossdead" type="checkbox" ${p.open_on_miniboss_dead?"checked":""}/> Present until the room's miniboss is slain</label>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:8px;">
|
||||||
|
<label><input id="ptbossdead" type="checkbox" ${p.open_on_boss_dead?"checked":""}/> Present until the room's boss is slain</label>
|
||||||
</div>` : ""}
|
</div>` : ""}
|
||||||
${(p.group||"ground") === "hazard" ? `
|
${(p.group||"ground") === "hazard" ? `
|
||||||
<div style="margin-top:8px;">
|
<div style="margin-top:8px;">
|
||||||
@@ -2204,7 +2227,16 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
<div style="margin-top:8px;">
|
<div style="margin-top:8px;">
|
||||||
<div class="mini">speed (px/s)</div>
|
<div class="mini">speed (px/s)</div>
|
||||||
<input id="ptspeed" type="number" value="${p.speed||120}"/>
|
<input id="ptspeed" type="number" value="${p.speed||120}"/>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:8px;">
|
||||||
|
<label><input id="pmbossdead" type="checkbox" ${p.open_on_boss_dead?"checked":""}/> Hidden until the room's boss is slain</label>
|
||||||
</div>` : ""}
|
</div>` : ""}
|
||||||
|
${(p.group||"ground") === "dissolve" ? `
|
||||||
|
<div class="grid2" style="margin-top:8px;">
|
||||||
|
<div><div class="mini">stand time (s)</div><input id="ptstand" type="number" step="0.1" min="0.1" value="${p.stand_time!==undefined?p.stand_time:1.0}"/></div>
|
||||||
|
<div><div class="mini">dissolve time (s)</div><input id="ptdissolve" type="number" step="0.1" min="0.1" value="${p.dissolve_time!==undefined?p.dissolve_time:2.0}"/></div>
|
||||||
|
</div>
|
||||||
|
<div class="muted" style="margin-top:6px;">While the player stands on it, after the stand time it becomes translucent (no hitbox) for the dissolve time, then reforms.</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>
|
||||||
@@ -2431,6 +2463,10 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
const pg=q("pg"); if(pg) pg.addEventListener("change", ()=>pushAnd(()=>{
|
const pg=q("pg"); if(pg) pg.addEventListener("change", ()=>pushAnd(()=>{
|
||||||
p.group = pg.value || "ground";
|
p.group = pg.value || "ground";
|
||||||
if(p.group === "moving" && !Array.isArray(p.path)) p.path = [[p.x, p.y]];
|
if(p.group === "moving" && !Array.isArray(p.path)) p.path = [[p.x, p.y]];
|
||||||
|
if(p.group === "dissolve"){
|
||||||
|
if(p.stand_time === undefined) p.stand_time = 1.0;
|
||||||
|
if(p.dissolve_time === undefined) p.dissolve_time = 2.0;
|
||||||
|
}
|
||||||
if(p.group !== "trigger" && p.name !== undefined && p.name !== "") delete p.name;
|
if(p.group !== "trigger" && p.name !== undefined && p.name !== "") delete p.name;
|
||||||
}));
|
}));
|
||||||
const ptname=q("ptname"); if(ptname) ptname.addEventListener("change", ()=>pushAnd(()=>{
|
const ptname=q("ptname"); if(ptname) ptname.addEventListener("change", ()=>pushAnd(()=>{
|
||||||
@@ -2455,7 +2491,15 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
const ptminibossdead=q("ptminibossdead"); if(ptminibossdead) ptminibossdead.addEventListener("change", ()=>pushAnd(()=>{
|
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; }
|
if(ptminibossdead.checked){ p.open_on_miniboss_dead = true; } else { delete p.open_on_miniboss_dead; }
|
||||||
}));
|
}));
|
||||||
|
const ptbossdead=q("ptbossdead"); if(ptbossdead) ptbossdead.addEventListener("change", ()=>pushAnd(()=>{
|
||||||
|
if(ptbossdead.checked){ p.open_on_boss_dead = true; } else { delete p.open_on_boss_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 pmbossdead=q("pmbossdead"); if(pmbossdead) pmbossdead.addEventListener("change", ()=>pushAnd(()=>{
|
||||||
|
if(pmbossdead.checked){ p.open_on_boss_dead = true; } else { delete p.open_on_boss_dead; }
|
||||||
|
}));
|
||||||
|
const ptstand=q("ptstand"); if(ptstand) ptstand.addEventListener("change", ()=>pushAnd(()=>{ p.stand_time = Math.max(0.1, num(ptstand.value)); }));
|
||||||
|
const ptdissolve=q("ptdissolve"); if(ptdissolve) ptdissolve.addEventListener("change", ()=>pushAnd(()=>{ p.dissolve_time = Math.max(0.1, num(ptdissolve.value)); }));
|
||||||
const hzrespawn=q("hzrespawn");
|
const hzrespawn=q("hzrespawn");
|
||||||
if(hzrespawn){
|
if(hzrespawn){
|
||||||
// preselect current respawn if it matches an entry spawn
|
// preselect current respawn if it matches an entry spawn
|
||||||
@@ -3353,19 +3397,24 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
for(const p of (d.platforms || [])){
|
for(const p of (d.platforms || [])){
|
||||||
const group = p.group || "ground";
|
const group = p.group || "ground";
|
||||||
if(!platforms[group]) platforms[group] = [];
|
if(!platforms[group]) platforms[group] = [];
|
||||||
if(group === "moving" || group === "trigger" || group === "hazard"){
|
if(group === "moving" || group === "trigger" || group === "hazard" || group === "dissolve"){
|
||||||
const obj = { x: p.x, y: p.y, w: p.w, h: p.h };
|
const obj = { x: p.x, y: p.y, w: p.w, h: p.h };
|
||||||
if(p.layer) obj.layer = p.layer;
|
if(p.layer) obj.layer = p.layer;
|
||||||
if(group === "moving"){
|
if(group === "moving"){
|
||||||
if(Array.isArray(p.path)) obj.path = p.path;
|
if(Array.isArray(p.path)) obj.path = p.path;
|
||||||
if(p.speed !== undefined) obj.speed = p.speed;
|
if(p.speed !== undefined) obj.speed = p.speed;
|
||||||
}
|
}
|
||||||
|
if(group === "dissolve"){
|
||||||
|
if(p.stand_time !== undefined) obj.stand_time = p.stand_time;
|
||||||
|
if(p.dissolve_time !== undefined) obj.dissolve_time = p.dissolve_time;
|
||||||
|
}
|
||||||
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.quest_turned_in) obj.quest_turned_in = p.quest_turned_in;
|
||||||
if(p.lock_when_pass) obj.lock_when_pass = true;
|
if(p.lock_when_pass) obj.lock_when_pass = true;
|
||||||
if(p.open_on_miniboss_dead) obj.open_on_miniboss_dead = true;
|
if(p.open_on_miniboss_dead) obj.open_on_miniboss_dead = true;
|
||||||
|
if(p.open_on_boss_dead) obj.open_on_boss_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 {
|
||||||
|
|||||||
+360
-52
@@ -19,16 +19,24 @@ SHOCKWAVE_LIFETIME = 2.0 # seconds
|
|||||||
SHOCKWAVE_HEIGHT = 20 # hitbox height
|
SHOCKWAVE_HEIGHT = 20 # hitbox height
|
||||||
SHOCKWAVE_DAMAGE = 2
|
SHOCKWAVE_DAMAGE = 2
|
||||||
BOSS_POISON_DURATION = 4.0 # boss attacks poison the player for this many seconds
|
BOSS_POISON_DURATION = 4.0 # boss attacks poison the player for this many seconds
|
||||||
ATTACK_COOLDOWN_BASE = 0.35 # recovery between melee swings; attack speed reduces it
|
ATTACK_COOLDOWN_BASE = 0.49 # recovery between melee swings; attack speed reduces it
|
||||||
|
|
||||||
# Horizontal deadzone: enemies only turn toward the player when the player is this far
|
# Horizontal deadzone: enemies only turn toward the player when the player is this far
|
||||||
# to one side, so a player standing directly above/below no longer makes them wiggle.
|
# to one side, so a player standing directly above/below no longer makes them wiggle.
|
||||||
CHASE_DEADZONE = 6
|
CHASE_DEADZONE = 6
|
||||||
|
|
||||||
|
# --- Miniboss "Discharge" (anti-camp AOE) ---
|
||||||
|
DISCHARGE_DAMAGE = 2 # AOE damage to the player
|
||||||
|
DISCHARGE_CHARGE_TIME = 1.5 # seconds standing still before the blast (telegraph)
|
||||||
|
DISCHARGE_RADIUS = 280 # blast radius in px (ignores platforms)
|
||||||
|
DISCHARGE_COOLDOWN = 4.0 # seconds before the miniboss can discharge again
|
||||||
|
DISCHARGE_SUSTAIN = 0.6 # how long the player must be unreachable before it commits
|
||||||
|
DISCHARGE_ABOVE_GAP = 30 # player feet must be this far above the miniboss's head to count as unreachable
|
||||||
|
|
||||||
# --- Ranged ability / projectile settings ---
|
# --- Ranged ability / projectile settings ---
|
||||||
RANGED_CHARGE_COST = 5.0 # costs more than melee (melee costs 1)
|
RANGED_CHARGE_COST = 5.0 # costs more than melee (melee costs 1)
|
||||||
RANGED_DAMAGE_MULT = 0.5 # 50% of melee damage
|
RANGED_DAMAGE_MULT = 0.5 # 50% of melee damage
|
||||||
RANGED_COOLDOWN = 0.45 # seconds between shots (feel-tweak)
|
RANGED_COOLDOWN = 0.63 # seconds between shots (feel-tweak)
|
||||||
PROJECTILE_SPEED = 650.0 # px/sec
|
PROJECTILE_SPEED = 650.0 # px/sec
|
||||||
PROJECTILE_LIFETIME = 1.2 # seconds
|
PROJECTILE_LIFETIME = 1.2 # seconds
|
||||||
PROJECTILE_SIZE = (14, 6) # width, height
|
PROJECTILE_SIZE = (14, 6) # width, height
|
||||||
@@ -79,6 +87,10 @@ SHATTER_AOE_LIFETIME = 0.25
|
|||||||
SHATTER_ICON_SIZE = 32
|
SHATTER_ICON_SIZE = 32
|
||||||
SHATTER_ICON_POS = (20, 20)
|
SHATTER_ICON_POS = (20, 20)
|
||||||
|
|
||||||
|
# --- Dissolve platforms (per-room "dissolve" group) ---
|
||||||
|
DISSOLVE_STAND_TIME = 1.0 # seconds the player must stand before it dissolves
|
||||||
|
DISSOLVE_TIME = 2.0 # seconds it stays translucent (no hitbox) once dissolved
|
||||||
|
|
||||||
# --- Tether ability ---
|
# --- Tether ability ---
|
||||||
TETHER_BIND_DURATION = 3.0
|
TETHER_BIND_DURATION = 3.0
|
||||||
TETHER_COOLDOWN = 6.0
|
TETHER_COOLDOWN = 6.0
|
||||||
@@ -578,18 +590,45 @@ def _build_mist_tile(size: int = MIST_TILE_SIZE) -> pygame.Surface:
|
|||||||
pygame.draw.circle(surf, color, (cx + ox, cy + oy), r)
|
pygame.draw.circle(surf, color, (cx + ox, cy + oy), r)
|
||||||
return surf
|
return surf
|
||||||
|
|
||||||
|
def _build_mist_tile_slow(size: int = MIST_TILE_SIZE) -> pygame.Surface:
|
||||||
|
"""Second, fainter mist layer with a different pattern, used to make the
|
||||||
|
haze visibly drift (two layers scrolling at different speeds)."""
|
||||||
|
surf = pygame.Surface((size, size), pygame.SRCALPHA)
|
||||||
|
rng = random.Random(90210)
|
||||||
|
surf.fill((70, 20, 115, 4))
|
||||||
|
for _ in range(9):
|
||||||
|
cx = rng.randint(0, size)
|
||||||
|
cy = rng.randint(0, size)
|
||||||
|
r = rng.randint(70, 160)
|
||||||
|
alpha = rng.randint(5, 12)
|
||||||
|
color = (110, 40, 165, alpha)
|
||||||
|
for ox in (-size, 0, size):
|
||||||
|
for oy in (-size, 0, size):
|
||||||
|
pygame.draw.circle(surf, color, (cx + ox, cy + oy), r)
|
||||||
|
return surf
|
||||||
|
|
||||||
MIST_TILE = _build_mist_tile()
|
MIST_TILE = _build_mist_tile()
|
||||||
|
MIST_TILE_SLOW = _build_mist_tile_slow()
|
||||||
|
|
||||||
def draw_mist_overlay(target: pygame.Surface):
|
def draw_mist_overlay(target: pygame.Surface):
|
||||||
if MIST_TILE is None or not getattr(ROOM, "mist", False):
|
if MIST_TILE is None or not getattr(ROOM, "mist", False):
|
||||||
return
|
return
|
||||||
size = MIST_TILE_SIZE
|
size = MIST_TILE_SIZE
|
||||||
t = pygame.time.get_ticks() / 1000.0
|
t = pygame.time.get_ticks() / 1000.0
|
||||||
dx = int((t * 10.0) % size)
|
# Base layer drifts one way...
|
||||||
dy = int((t * 6.0) % size)
|
dx = int((t * 30.0) % size)
|
||||||
|
dy = int((t * 22.0) % size)
|
||||||
for tx in range(-1, WIDTH // size + 2):
|
for tx in range(-1, WIDTH // size + 2):
|
||||||
for ty in range(-1, HEIGHT // size + 2):
|
for ty in range(-1, HEIGHT // size + 2):
|
||||||
target.blit(MIST_TILE, (tx * size - dx, ty * size - dy))
|
target.blit(MIST_TILE, (tx * size - dx, ty * size - dy))
|
||||||
|
# ...while the fainter layer drifts slower and in a different direction,
|
||||||
|
# so the mist visibly curls instead of looking like a static texture.
|
||||||
|
if MIST_TILE_SLOW is not None:
|
||||||
|
dx2 = int((t * 9.0) % size)
|
||||||
|
dy2 = int((size - int((t * 5.0) % size)) % size)
|
||||||
|
for tx in range(-1, WIDTH // size + 2):
|
||||||
|
for ty in range(-1, HEIGHT // size + 2):
|
||||||
|
target.blit(MIST_TILE_SLOW, (tx * size - dx2, ty * size - dy2))
|
||||||
|
|
||||||
VIGNETTE_CLEAR_RADIUS = 150
|
VIGNETTE_CLEAR_RADIUS = 150
|
||||||
_VIGNETTE_HOLE_CACHE: dict[tuple[int, int], pygame.Surface] = {}
|
_VIGNETTE_HOLE_CACHE: dict[tuple[int, int], pygame.Surface] = {}
|
||||||
@@ -974,7 +1013,7 @@ def normalize_room_json(room_json: dict) -> dict:
|
|||||||
grouped["ground"].append(arr)
|
grouped["ground"].append(arr)
|
||||||
elif isinstance(p, dict):
|
elif isinstance(p, dict):
|
||||||
group = p.get("group", "ground")
|
group = p.get("group", "ground")
|
||||||
if group in ("moving", "trigger", "hazard"):
|
if group in ("moving", "trigger", "hazard", "dissolve"):
|
||||||
obj = {"x": p.get("x", 0), "y": p.get("y", 0), "w": p.get("w", 0), "h": p.get("h", 0)}
|
obj = {"x": p.get("x", 0), "y": p.get("y", 0), "w": p.get("w", 0), "h": p.get("h", 0)}
|
||||||
if "layer" in p and p.get("layer"):
|
if "layer" in p and p.get("layer"):
|
||||||
obj["layer"] = p["layer"]
|
obj["layer"] = p["layer"]
|
||||||
@@ -983,6 +1022,11 @@ def normalize_room_json(room_json: dict) -> dict:
|
|||||||
obj["path"] = p["path"]
|
obj["path"] = p["path"]
|
||||||
if "speed" in p:
|
if "speed" in p:
|
||||||
obj["speed"] = p["speed"]
|
obj["speed"] = p["speed"]
|
||||||
|
if group == "dissolve":
|
||||||
|
if "stand_time" in p:
|
||||||
|
obj["stand_time"] = p["stand_time"]
|
||||||
|
if "dissolve_time" in p:
|
||||||
|
obj["dissolve_time"] = p["dissolve_time"]
|
||||||
if "name" in p:
|
if "name" in p:
|
||||||
obj["name"] = p["name"]
|
obj["name"] = p["name"]
|
||||||
if "quest" in p:
|
if "quest" in p:
|
||||||
@@ -995,6 +1039,8 @@ def normalize_room_json(room_json: dict) -> dict:
|
|||||||
obj["lock_when_pass"] = p["lock_when_pass"]
|
obj["lock_when_pass"] = p["lock_when_pass"]
|
||||||
if "open_on_miniboss_dead" in p:
|
if "open_on_miniboss_dead" in p:
|
||||||
obj["open_on_miniboss_dead"] = p["open_on_miniboss_dead"]
|
obj["open_on_miniboss_dead"] = p["open_on_miniboss_dead"]
|
||||||
|
if "open_on_boss_dead" in p:
|
||||||
|
obj["open_on_boss_dead"] = p["open_on_boss_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)
|
||||||
@@ -1099,6 +1145,7 @@ class Room:
|
|||||||
|
|
||||||
self.platform_layer: dict[int, int] = {}
|
self.platform_layer: dict[int, int] = {}
|
||||||
self.platform_meta: dict[int, dict] = {} # id(rect) -> {"name"|"path"|"speed", ...}
|
self.platform_meta: dict[int, dict] = {} # id(rect) -> {"name"|"path"|"speed", ...}
|
||||||
|
self.platforms_dissolve: list[pygame.Rect] = []
|
||||||
platforms = room_json.get("platforms", {}) if isinstance(room_json, dict) else {}
|
platforms = room_json.get("platforms", {}) if isinstance(room_json, dict) else {}
|
||||||
if isinstance(platforms, list):
|
if isinstance(platforms, list):
|
||||||
self.platforms_ground = rects_from_list(platforms, self.platform_layer)
|
self.platforms_ground = rects_from_list(platforms, self.platform_layer)
|
||||||
@@ -1142,6 +1189,8 @@ class Room:
|
|||||||
meta["lock_when_pass"] = True
|
meta["lock_when_pass"] = True
|
||||||
if tp.get("open_on_miniboss_dead"):
|
if tp.get("open_on_miniboss_dead"):
|
||||||
meta["open_on_miniboss_dead"] = True
|
meta["open_on_miniboss_dead"] = True
|
||||||
|
if tp.get("open_on_boss_dead"):
|
||||||
|
meta["open_on_boss_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:
|
||||||
@@ -1163,7 +1212,10 @@ class Room:
|
|||||||
else:
|
else:
|
||||||
path = [[rect.x, rect.y]]
|
path = [[rect.x, rect.y]]
|
||||||
speed = float(mp.get("speed", 120))
|
speed = float(mp.get("speed", 120))
|
||||||
self.platform_meta[id(rect)] = {"path": path, "speed": speed, "i": 0, "t": 0.0}
|
m_meta = {"path": path, "speed": speed, "i": 0, "t": 0.0}
|
||||||
|
if mp.get("open_on_boss_dead"):
|
||||||
|
m_meta["open_on_boss_dead"] = True
|
||||||
|
self.platform_meta[id(rect)] = m_meta
|
||||||
self.platforms_moving.append(rect)
|
self.platforms_moving.append(rect)
|
||||||
elif isinstance(mp, (list, tuple)) and len(mp) >= 4:
|
elif isinstance(mp, (list, tuple)) and len(mp) >= 4:
|
||||||
rect = pygame.Rect(int(mp[0]), int(mp[1]), int(mp[2]), int(mp[3]))
|
rect = pygame.Rect(int(mp[0]), int(mp[1]), int(mp[2]), int(mp[3]))
|
||||||
@@ -1173,16 +1225,37 @@ class Room:
|
|||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
pass
|
pass
|
||||||
self.platforms_moving.append(rect)
|
self.platforms_moving.append(rect)
|
||||||
|
self.platforms_dissolve = []
|
||||||
|
for dp in platforms.get("dissolve", []):
|
||||||
|
if isinstance(dp, dict):
|
||||||
|
rect = pygame.Rect(int(dp.get("x", 0)), int(dp.get("y", 0)), int(dp.get("w", 0)), int(dp.get("h", 0)))
|
||||||
|
self.platform_layer[id(rect)] = int(dp.get("layer", 0))
|
||||||
|
self.platform_meta[id(rect)] = {
|
||||||
|
"stand_time": float(dp.get("stand_time", DISSOLVE_STAND_TIME)),
|
||||||
|
"dissolve_time": float(dp.get("dissolve_time", DISSOLVE_TIME)),
|
||||||
|
"timer": 0.0,
|
||||||
|
"dissolving": False,
|
||||||
|
}
|
||||||
|
self.platforms_dissolve.append(rect)
|
||||||
|
elif isinstance(dp, (list, tuple)) and len(dp) >= 4:
|
||||||
|
rect = pygame.Rect(int(dp[0]), int(dp[1]), int(dp[2]), int(dp[3]))
|
||||||
|
if len(dp) >= 5:
|
||||||
|
try:
|
||||||
|
self.platform_layer[id(rect)] = int(dp[4])
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
self.platforms_dissolve.append(rect)
|
||||||
else:
|
else:
|
||||||
self.platforms_ground = []
|
self.platforms_ground = []
|
||||||
self.platforms_floating = []
|
self.platforms_floating = []
|
||||||
self.platforms_hazard = []
|
self.platforms_hazard = []
|
||||||
self.platforms_trigger = []
|
self.platforms_trigger = []
|
||||||
self.platforms_moving = []
|
self.platforms_moving = []
|
||||||
|
self.platforms_dissolve = []
|
||||||
self.ground_platforms = self.platforms_ground[:]
|
self.ground_platforms = self.platforms_ground[:]
|
||||||
self.platforms = self.platforms_ground + self.platforms_floating + self.platforms_moving
|
self.platforms = self.platforms_ground + self.platforms_floating + self.platforms_moving
|
||||||
|
|
||||||
all_platform_rects = self.platforms_ground + self.platforms_floating + self.platforms_moving + self.platforms_hazard + self.platforms_trigger
|
all_platform_rects = self.platforms_ground + self.platforms_floating + self.platforms_moving + self.platforms_hazard + self.platforms_trigger + self.platforms_dissolve
|
||||||
if all_platform_rects:
|
if all_platform_rects:
|
||||||
self.min_x = min(r.left for r in all_platform_rects)
|
self.min_x = min(r.left for r in all_platform_rects)
|
||||||
self.max_x = max(r.right for r in all_platform_rects)
|
self.max_x = max(r.right for r in all_platform_rects)
|
||||||
@@ -1534,9 +1607,12 @@ class Projectile:
|
|||||||
w = max(1, int(img.get_width() * scale))
|
w = max(1, int(img.get_width() * scale))
|
||||||
h = max(1, int(img.get_height() * scale))
|
h = max(1, int(img.get_height() * scale))
|
||||||
img = pygame.transform.scale(img, (w, h))
|
img = pygame.transform.scale(img, (w, h))
|
||||||
if self.dir > 0:
|
# Raw sprite points LEFT; flip it so it points right, then rotate to
|
||||||
|
# match the actual flight direction so up/down shots aren't horizontal.
|
||||||
img = pygame.transform.flip(img, True, False)
|
img = pygame.transform.flip(img, True, False)
|
||||||
surface.blit(img, (self.rect.centerx - w // 2 - cx, self.rect.centery - h // 2 - cy))
|
angle = -math.degrees(math.atan2(self.dir_y, self.dir_x))
|
||||||
|
img = pygame.transform.rotate(img, angle)
|
||||||
|
surface.blit(img, (self.rect.centerx - img.get_width() // 2 - cx, self.rect.centery - img.get_height() // 2 - cy))
|
||||||
return
|
return
|
||||||
# simple readable bolt
|
# simple readable bolt
|
||||||
pygame.draw.rect(
|
pygame.draw.rect(
|
||||||
@@ -1761,11 +1837,15 @@ class LightSource:
|
|||||||
|
|
||||||
|
|
||||||
class SavePoint:
|
class SavePoint:
|
||||||
"""Lantern save point: pressing interact nearby saves the game and sets the respawn point."""
|
"""Lantern save point: pressing interact nearby saves the game and sets the respawn point.
|
||||||
|
|
||||||
|
One-time use per room visit: once used, the lantern stays spent until the
|
||||||
|
player leaves the room and comes back (the flag resets on room load)."""
|
||||||
def __init__(self, x: int, y: int, radius: int = 90):
|
def __init__(self, x: int, y: int, radius: int = 90):
|
||||||
self.x = int(x)
|
self.x = int(x)
|
||||||
self.y = int(y)
|
self.y = int(y)
|
||||||
self.radius = int(radius)
|
self.radius = int(radius)
|
||||||
|
self.used = False
|
||||||
self.t = random.random() * 10.0
|
self.t = random.random() * 10.0
|
||||||
if LAMP_RAW is not None:
|
if LAMP_RAW is not None:
|
||||||
self.img = LAMP_RAW
|
self.img = LAMP_RAW
|
||||||
@@ -1780,6 +1860,10 @@ class SavePoint:
|
|||||||
surface.blit(self.img, (self.rect.x - cx, self.rect.y - cy))
|
surface.blit(self.img, (self.rect.x - cx, self.rect.y - cy))
|
||||||
else:
|
else:
|
||||||
pygame.draw.rect(surface, (150, 200, 230), pygame.Rect(self.rect.x - cx, self.rect.y - cy, self.rect.width, self.rect.height))
|
pygame.draw.rect(surface, (150, 200, 230), pygame.Rect(self.rect.x - cx, self.rect.y - cy, self.rect.width, self.rect.height))
|
||||||
|
if self.used:
|
||||||
|
dim = pygame.Surface((self.rect.width, self.rect.height), pygame.SRCALPHA)
|
||||||
|
dim.fill((30, 30, 40, 150))
|
||||||
|
surface.blit(dim, (self.rect.x - cx, self.rect.y - cy))
|
||||||
|
|
||||||
|
|
||||||
class ButterflyJar:
|
class ButterflyJar:
|
||||||
@@ -1933,14 +2017,14 @@ class LootPickup:
|
|||||||
|
|
||||||
|
|
||||||
class CoinPickup:
|
class CoinPickup:
|
||||||
def __init__(self, x, y, kind: str):
|
def __init__(self, x, y, kind: str, vx: float | None = None, vy: float | None = None):
|
||||||
# kind: "light" or "dark"
|
# kind: "light" or "dark"
|
||||||
self.kind = kind
|
self.kind = kind
|
||||||
self.rect = pygame.Rect(int(x), int(y), 16, 16)
|
self.rect = pygame.Rect(int(x), int(y), 16, 16)
|
||||||
|
|
||||||
# physics (same feel as loot)
|
# physics (same feel as loot)
|
||||||
self.vx = random.uniform(-80, 80)
|
self.vx = random.uniform(-80, 80) if vx is None else vx
|
||||||
self.vy = random.uniform(-140, -60)
|
self.vy = random.uniform(-140, -60) if vy is None else vy
|
||||||
self.on_ground = False
|
self.on_ground = False
|
||||||
|
|
||||||
self.bob_t = random.random() * 10.0
|
self.bob_t = random.random() * 10.0
|
||||||
@@ -2029,12 +2113,29 @@ class RewardChest:
|
|||||||
if self.opened:
|
if self.opened:
|
||||||
return False
|
return False
|
||||||
light_amt, dark_amt = self.coins
|
light_amt, dark_amt = self.coins
|
||||||
cx = self.rect.centerx
|
# spray coins across the chest width and above its top so they don't
|
||||||
cy = self.rect.top - 18 # pop out of the chest top, clear of its platform
|
# all stack on a player standing directly in front of the chest
|
||||||
|
half_span = self.rect.width // 2 + 24
|
||||||
for _ in range(max(0, int(light_amt))):
|
for _ in range(max(0, int(light_amt))):
|
||||||
coin_drops.append(CoinPickup(cx, cy, "light"))
|
ox = random.randint(-half_span, half_span)
|
||||||
|
oy = random.randint(0, 40)
|
||||||
|
coin_drops.append(CoinPickup(
|
||||||
|
self.rect.centerx + ox,
|
||||||
|
self.rect.top - 18 - oy,
|
||||||
|
"light",
|
||||||
|
vx=random.uniform(-170, 170),
|
||||||
|
vy=random.uniform(-230, -90),
|
||||||
|
))
|
||||||
for _ in range(max(0, int(dark_amt))):
|
for _ in range(max(0, int(dark_amt))):
|
||||||
coin_drops.append(CoinPickup(cx, cy, "dark"))
|
ox = random.randint(-half_span, half_span)
|
||||||
|
oy = random.randint(0, 40)
|
||||||
|
coin_drops.append(CoinPickup(
|
||||||
|
self.rect.centerx + ox,
|
||||||
|
self.rect.top - 18 - oy,
|
||||||
|
"dark",
|
||||||
|
vx=random.uniform(-170, 170),
|
||||||
|
vy=random.uniform(-230, -90),
|
||||||
|
))
|
||||||
self.opened = True
|
self.opened = True
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -3978,6 +4079,11 @@ class Player:
|
|||||||
elif key_pressed(keys, "move_right"):
|
elif key_pressed(keys, "move_right"):
|
||||||
self.facing_right = True
|
self.facing_right = True
|
||||||
|
|
||||||
|
# In mist rooms the shade form has no movement penalty — the haze is
|
||||||
|
# home to it, so it moves at full (light-form) speed.
|
||||||
|
if self.mist_active:
|
||||||
|
speed = PLAYER_SPEED
|
||||||
|
else:
|
||||||
speed = PLAYER_SHADE_SPEED if self.shade else PLAYER_SPEED
|
speed = PLAYER_SHADE_SPEED if self.shade else PLAYER_SPEED
|
||||||
form_bonus = self.bonus_move_speed_shade_pct if self.shade else self.bonus_move_speed_light_pct
|
form_bonus = self.bonus_move_speed_shade_pct if self.shade else self.bonus_move_speed_light_pct
|
||||||
speed *= (1.0 + self.bonus_move_speed_pct + form_bonus)
|
speed *= (1.0 + self.bonus_move_speed_pct + form_bonus)
|
||||||
@@ -4255,6 +4361,18 @@ class Enemy:
|
|||||||
self.max_step_up = 160
|
self.max_step_up = 160
|
||||||
self.max_step_forward = 120
|
self.max_step_forward = 120
|
||||||
|
|
||||||
|
# Miniboss "Discharge": stand-still telegraph then a platform-piercing AOE
|
||||||
|
# when the player camps somewhere the miniboss can't reach.
|
||||||
|
self.discharge_state = "idle" # "idle" | "charging"
|
||||||
|
self.discharge_timer = 0.0
|
||||||
|
self.discharge_cd = 0.0
|
||||||
|
self.discharge_buildup = 0.0
|
||||||
|
self.discharge_flash = 0.0
|
||||||
|
# Head height while grounded. The discharge checks the player against this
|
||||||
|
# (not the current airborne rect.top), so jumping up at a camping player
|
||||||
|
# no longer resets the discharge buildup.
|
||||||
|
self.discharge_ground_top = self.rect.top
|
||||||
|
|
||||||
self.contact_damage = int(cfg.get("contact_damage", 1))
|
self.contact_damage = int(cfg.get("contact_damage", 1))
|
||||||
self.drop_profile = str(cfg.get("drop_profile", "normal"))
|
self.drop_profile = str(cfg.get("drop_profile", "normal"))
|
||||||
|
|
||||||
@@ -4403,6 +4521,20 @@ class Enemy:
|
|||||||
best_top = p.top
|
best_top = p.top
|
||||||
return best
|
return best
|
||||||
|
|
||||||
|
def _player_unreachable_above(self, player) -> bool:
|
||||||
|
"""True if the player is camping above the miniboss, out of reach."""
|
||||||
|
ground_top = getattr(self, "discharge_ground_top", self.rect.top)
|
||||||
|
return player.rect.bottom < ground_top - DISCHARGE_ABOVE_GAP
|
||||||
|
|
||||||
|
def _discharge_release(self, player):
|
||||||
|
"""Resolve the Discharge blast: platform-piercing AOE damage."""
|
||||||
|
dx = player.rect.centerx - self.rect.centerx
|
||||||
|
dy = player.rect.centery - self.rect.centery
|
||||||
|
if math.hypot(dx, dy) <= DISCHARGE_RADIUS and (not player.shade):
|
||||||
|
player.take_damage(DISCHARGE_DAMAGE)
|
||||||
|
player.apply_knockback(self.rect, strength_px_per_frame=16.0, duration=0.25, pop_up_vel=-4.0)
|
||||||
|
self.discharge_flash = 0.35
|
||||||
|
|
||||||
|
|
||||||
def _probe_ground_now(self, platforms):
|
def _probe_ground_now(self, platforms):
|
||||||
"""Refresh on_ground + ground_platform every frame (stable)."""
|
"""Refresh on_ground + ground_platform every frame (stable)."""
|
||||||
@@ -4833,10 +4965,43 @@ class Enemy:
|
|||||||
self.vel_x = self.direction * desired_speed
|
self.vel_x = self.direction * desired_speed
|
||||||
|
|
||||||
|
|
||||||
|
# --- Miniboss Discharge: anti-camp AOE (stand still, then blast) ---
|
||||||
|
discharging = self.discharge_state == "charging"
|
||||||
|
if self.drop_profile == "miniboss":
|
||||||
|
# Track the resting head height so jumps at a camping player don't
|
||||||
|
# reset the discharge buildup (see _player_unreachable_above).
|
||||||
|
if self.on_ground:
|
||||||
|
self.discharge_ground_top = self.rect.top
|
||||||
|
if self.discharge_cd > 0.0:
|
||||||
|
self.discharge_cd = max(0.0, self.discharge_cd - dt)
|
||||||
|
if self.discharge_flash > 0.0:
|
||||||
|
self.discharge_flash = max(0.0, self.discharge_flash - dt)
|
||||||
|
if discharging:
|
||||||
|
self.discharge_timer -= dt
|
||||||
|
if self.discharge_timer <= 0.0:
|
||||||
|
self.discharge_state = "idle"
|
||||||
|
self.discharge_cd = DISCHARGE_COOLDOWN
|
||||||
|
self.discharge_buildup = 0.0
|
||||||
|
self._discharge_release(player)
|
||||||
|
discharging = False
|
||||||
|
elif chasing and (not stunned) and (not charg_acting):
|
||||||
|
if self._player_unreachable_above(player):
|
||||||
|
self.discharge_buildup += dt
|
||||||
|
if self.discharge_buildup >= DISCHARGE_SUSTAIN and self.discharge_cd <= 0.0:
|
||||||
|
self.discharge_state = "charging"
|
||||||
|
self.discharge_timer = DISCHARGE_CHARGE_TIME
|
||||||
|
self.discharge_buildup = 0.0
|
||||||
|
discharging = True
|
||||||
|
else:
|
||||||
|
self.discharge_buildup = 0.0
|
||||||
|
if discharging:
|
||||||
|
self.vel_x = 0.0
|
||||||
|
|
||||||
|
|
||||||
# --- CHASE LEDGE DROP ---
|
# --- CHASE LEDGE DROP ---
|
||||||
# If we're chasing and the player is below, step off the platform instead of edge-wiggling.
|
# If we're chasing and the player is below, step off the platform instead of edge-wiggling.
|
||||||
jump_target = None
|
jump_target = None
|
||||||
if chasing and self.on_ground and self.ground_platform is not None:
|
if chasing and (not discharging) and self.on_ground and self.ground_platform is not None:
|
||||||
gp = self.ground_platform
|
gp = self.ground_platform
|
||||||
|
|
||||||
# IMPORTANT: define this right here so it's ALWAYS available
|
# IMPORTANT: define this right here so it's ALWAYS available
|
||||||
@@ -4941,7 +5106,7 @@ class Enemy:
|
|||||||
|
|
||||||
|
|
||||||
# regular enemy jump helper (charger moves via charge states instead)
|
# regular enemy jump helper (charger moves via charge states instead)
|
||||||
if self.on_ground and chasing and self.jump_cd_timer <= 0 and (not charg_acting):
|
if self.on_ground and chasing and (not discharging) and self.jump_cd_timer <= 0 and (not charg_acting):
|
||||||
player_above = player.rect.centery < self.rect.centery - self.jump_when_player_above
|
player_above = player.rect.centery < self.rect.centery - self.jump_when_player_above
|
||||||
if player_above and (blocked_by_wall or self.reachable_platform_ahead(platforms)):
|
if player_above and (blocked_by_wall or self.reachable_platform_ahead(platforms)):
|
||||||
self.vel_y = self.jump_strength
|
self.vel_y = self.jump_strength
|
||||||
@@ -4985,6 +5150,28 @@ class Enemy:
|
|||||||
if self.bind_timer > 0.0:
|
if self.bind_timer > 0.0:
|
||||||
_draw_tether_chain_loop(surface, self.rect, light=self.bound_light, cx=cx, cy=cy)
|
_draw_tether_chain_loop(surface, self.rect, light=self.bound_light, cx=cx, cy=cy)
|
||||||
|
|
||||||
|
if self.discharge_state == "charging" or self.discharge_flash > 0.0:
|
||||||
|
if self.discharge_state == "charging":
|
||||||
|
progress = 1.0 - (self.discharge_timer / DISCHARGE_CHARGE_TIME)
|
||||||
|
radius = max(20, int(20 + progress * (DISCHARGE_RADIUS - 20)))
|
||||||
|
ring_color = (190, 80, 220, 40 + int(progress * 80))
|
||||||
|
body_color = (220, 120, 255)
|
||||||
|
else:
|
||||||
|
radius = int(DISCHARGE_RADIUS)
|
||||||
|
ring_color = (255, 180, 90, 130)
|
||||||
|
body_color = (255, 200, 120)
|
||||||
|
px = self.rect.centerx - cx
|
||||||
|
py = self.rect.centery - cy
|
||||||
|
ring = pygame.Surface((radius * 2, radius * 2), pygame.SRCALPHA)
|
||||||
|
pygame.draw.circle(ring, ring_color, (radius, radius), radius, 3)
|
||||||
|
surface.blit(ring, (px - radius, py - radius))
|
||||||
|
pygame.draw.rect(
|
||||||
|
surface,
|
||||||
|
body_color,
|
||||||
|
pygame.Rect(self.rect.x - cx, self.rect.y - cy, self.rect.width, self.rect.height),
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def resolve_enemy_collisions(enemies):
|
def resolve_enemy_collisions(enemies):
|
||||||
# for i in range(len(enemies)):
|
# for i in range(len(enemies)):
|
||||||
@@ -5146,7 +5333,7 @@ def draw_pause_controls(surface, x, y):
|
|||||||
f"{key_name(key_for('move_left'))}/{key_name(key_for('move_right'))} = move",
|
f"{key_name(key_for('move_left'))}/{key_name(key_for('move_right'))} = move",
|
||||||
f"{key_name(key_for('jump'))} = jump",
|
f"{key_name(key_for('jump'))} = jump",
|
||||||
f"{key_name(key_for('attack'))} = attack",
|
f"{key_name(key_for('attack'))} = attack",
|
||||||
f"{key_name(key_for('ranged'))} = ranged",
|
"Right Mouse Button = ranged",
|
||||||
f"{key_name(key_for('dash'))} = dash",
|
f"{key_name(key_for('dash'))} = dash",
|
||||||
f"{key_name(key_for('shatterstep'))} = shatterstep",
|
f"{key_name(key_for('shatterstep'))} = shatterstep",
|
||||||
f"{key_name(key_for('tether'))} = tether",
|
f"{key_name(key_for('tether'))} = tether",
|
||||||
@@ -5261,6 +5448,9 @@ def trigger_wall_blocking(rect) -> bool:
|
|||||||
- ``open_on_miniboss_dead``: releases the wall once the room's miniboss
|
- ``open_on_miniboss_dead``: releases the wall once the room's miniboss
|
||||||
is slain (combines with the conditions above; with no other condition
|
is slain (combines with the conditions above; with no other condition
|
||||||
the wall is simply present until then).
|
the wall is simply present until then).
|
||||||
|
- ``open_on_boss_dead``: releases the wall once the room's boss 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")
|
||||||
@@ -5275,11 +5465,15 @@ def trigger_wall_blocking(rect) -> bool:
|
|||||||
present = meta["quest_turned_in"] not in quest_manager.ever_completed
|
present = meta["quest_turned_in"] not in quest_manager.ever_completed
|
||||||
elif meta.get("open_on_miniboss_dead"):
|
elif meta.get("open_on_miniboss_dead"):
|
||||||
present = True # always present until released
|
present = True # always present until released
|
||||||
|
elif meta.get("open_on_boss_dead"):
|
||||||
|
present = True # always present until released
|
||||||
else:
|
else:
|
||||||
return False # inert placeholder
|
return False # inert placeholder
|
||||||
|
|
||||||
if meta.get("open_on_miniboss_dead"):
|
if meta.get("open_on_miniboss_dead"):
|
||||||
present = present and miniboss_alive_in_room()
|
present = present and miniboss_alive_in_room()
|
||||||
|
if meta.get("open_on_boss_dead"):
|
||||||
|
present = present and boss_alive_in_room()
|
||||||
return present
|
return present
|
||||||
|
|
||||||
|
|
||||||
@@ -5294,6 +5488,8 @@ def enemy_trigger_wall_blocking(rect) -> bool:
|
|||||||
meta = platform_meta.get(id(rect), {})
|
meta = platform_meta.get(id(rect), {})
|
||||||
if meta.get("open_on_miniboss_dead"):
|
if meta.get("open_on_miniboss_dead"):
|
||||||
return miniboss_alive_in_room()
|
return miniboss_alive_in_room()
|
||||||
|
if meta.get("open_on_boss_dead"):
|
||||||
|
return boss_alive_in_room()
|
||||||
return trigger_wall_blocking(rect)
|
return trigger_wall_blocking(rect)
|
||||||
|
|
||||||
|
|
||||||
@@ -5306,6 +5502,15 @@ def miniboss_alive_in_room() -> bool:
|
|||||||
return any(getattr(en, "drop_profile", "") == "miniboss" and en.health > 0 for en in enemies)
|
return any(getattr(en, "drop_profile", "") == "miniboss" and en.health > 0 for en in enemies)
|
||||||
|
|
||||||
|
|
||||||
|
def moving_platform_active(rect) -> bool:
|
||||||
|
"""A moving platform with ``open_on_boss_dead`` is hidden and inert while
|
||||||
|
the room's boss is alive; it appears and starts moving once the boss is slain."""
|
||||||
|
meta = platform_meta.get(id(rect), {})
|
||||||
|
if meta.get("open_on_boss_dead") and boss_alive_in_room():
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def push_rect_out_of_rects(rect, rects):
|
def push_rect_out_of_rects(rect, rects):
|
||||||
"""Push a pygame.Rect out of any overlapping rect, moving to the nearest edge.
|
"""Push a pygame.Rect out of any overlapping rect, moving to the nearest edge.
|
||||||
|
|
||||||
@@ -5370,8 +5575,11 @@ def update_moving_platforms(dt: float):
|
|||||||
"""Advance moving platforms along their waypoint paths (top-left follows the path).
|
"""Advance moving platforms along their waypoint paths (top-left follows the path).
|
||||||
|
|
||||||
A platform stops while the player or an enemy is in its way, and resumes
|
A platform stops while the player or an enemy is in its way, and resumes
|
||||||
from the same spot once the path is clear again."""
|
from the same spot once the path is clear again. Boss-gated platforms are
|
||||||
|
skipped entirely while their boss is alive."""
|
||||||
for rect in platforms_moving:
|
for rect in platforms_moving:
|
||||||
|
if not moving_platform_active(rect):
|
||||||
|
continue
|
||||||
mdata = platform_meta.get(id(rect))
|
mdata = platform_meta.get(id(rect))
|
||||||
if not mdata:
|
if not mdata:
|
||||||
continue
|
continue
|
||||||
@@ -5412,6 +5620,45 @@ def update_moving_platforms(dt: float):
|
|||||||
rect.y = new_y
|
rect.y = new_y
|
||||||
|
|
||||||
|
|
||||||
|
def update_dissolve_platforms(dt: float):
|
||||||
|
"""Advance dissolve-platform timers.
|
||||||
|
|
||||||
|
A dissolve platform behaves like a normal platform while solid. If the player
|
||||||
|
stands on it for its ``stand_time``, it turns translucent (no hitbox) for its
|
||||||
|
``dissolve_time``, then returns to solid again. Returns the rects that are
|
||||||
|
currently solid (callers add these to the collision lists).
|
||||||
|
"""
|
||||||
|
solid_rects = []
|
||||||
|
for rect in platforms_dissolve:
|
||||||
|
meta = platform_meta.get(id(rect))
|
||||||
|
if not meta:
|
||||||
|
solid_rects.append(rect)
|
||||||
|
continue
|
||||||
|
if meta.get("dissolving"):
|
||||||
|
meta["timer"] += dt
|
||||||
|
if meta["timer"] >= float(meta.get("dissolve_time", DISSOLVE_TIME)):
|
||||||
|
meta["dissolving"] = False
|
||||||
|
meta["timer"] = 0.0
|
||||||
|
continue
|
||||||
|
# Standing check uses geometry (like the moving-platform support check),
|
||||||
|
# not player.ground_platform, which flickers when resting exactly on top.
|
||||||
|
standing = (
|
||||||
|
player.rect.bottom >= rect.top - 2
|
||||||
|
and player.rect.bottom <= rect.top + 5
|
||||||
|
and player.rect.right > rect.left
|
||||||
|
and player.rect.left < rect.right
|
||||||
|
)
|
||||||
|
if standing:
|
||||||
|
meta["timer"] += dt
|
||||||
|
if meta["timer"] >= float(meta.get("stand_time", DISSOLVE_STAND_TIME)):
|
||||||
|
meta["dissolving"] = True
|
||||||
|
meta["timer"] = 0.0
|
||||||
|
else:
|
||||||
|
meta["timer"] = 0.0
|
||||||
|
solid_rects.append(rect)
|
||||||
|
return solid_rects
|
||||||
|
|
||||||
|
|
||||||
def draw_layerable(surface, cx, cy):
|
def draw_layerable(surface, cx, cy):
|
||||||
"""Draw all static room objects back-to-front by (layer, category).
|
"""Draw all static room objects back-to-front by (layer, category).
|
||||||
|
|
||||||
@@ -5440,9 +5687,18 @@ def draw_layerable(surface, cx, cy):
|
|||||||
items.append((getattr(reward_ability, "layer", 0), 0, "rewardability", reward_ability))
|
items.append((getattr(reward_ability, "layer", 0), 0, "rewardability", reward_ability))
|
||||||
# platforms
|
# platforms
|
||||||
for p in platforms:
|
for p in platforms:
|
||||||
|
# Boss-gated moving platforms are also in `platforms` (ground+floating+
|
||||||
|
# moving); skip them here so they're fully invisible while inactive.
|
||||||
|
if not moving_platform_active(p):
|
||||||
|
continue
|
||||||
items.append((platform_layer.get(id(p), 0), 1, "platform", p))
|
items.append((platform_layer.get(id(p), 0), 1, "platform", p))
|
||||||
|
# dissolve platforms (solid or translucent)
|
||||||
|
for p in platforms_dissolve:
|
||||||
|
items.append((platform_layer.get(id(p), 0), 1, "dissolve", p))
|
||||||
# moving platforms (solid, follow a path) layer alongside platforms
|
# moving platforms (solid, follow a path) layer alongside platforms
|
||||||
for p in platforms_moving:
|
for p in platforms_moving:
|
||||||
|
if not moving_platform_active(p):
|
||||||
|
continue
|
||||||
items.append((platform_layer.get(id(p), 0), 1, "moving", p))
|
items.append((platform_layer.get(id(p), 0), 1, "moving", p))
|
||||||
# hazard zones (red) layer alongside platforms
|
# hazard zones (red) layer alongside platforms
|
||||||
for p in platforms_hazard:
|
for p in platforms_hazard:
|
||||||
@@ -5478,6 +5734,16 @@ def draw_layerable(surface, cx, cy):
|
|||||||
PLATFORM_COLOR,
|
PLATFORM_COLOR,
|
||||||
pygame.Rect(obj.x - cx, obj.y - cy, obj.width, obj.height),
|
pygame.Rect(obj.x - cx, obj.y - cy, obj.width, obj.height),
|
||||||
)
|
)
|
||||||
|
elif kind == "dissolve":
|
||||||
|
mdata = platform_meta.get(id(obj), {})
|
||||||
|
drect = pygame.Rect(obj.x - cx, obj.y - cy, obj.width, obj.height)
|
||||||
|
if mdata.get("dissolving"):
|
||||||
|
fade = pygame.Surface((obj.width, obj.height), pygame.SRCALPHA)
|
||||||
|
fade.fill((96, 165, 250, 90))
|
||||||
|
surface.blit(fade, (drect.x, drect.y))
|
||||||
|
pygame.draw.rect(surface, (150, 210, 255), drect, 1)
|
||||||
|
else:
|
||||||
|
pygame.draw.rect(surface, PLATFORM_COLOR, drect)
|
||||||
elif kind == "moving":
|
elif kind == "moving":
|
||||||
mr = pygame.Rect(obj.x - cx, obj.y - cy, obj.width, obj.height)
|
mr = pygame.Rect(obj.x - cx, obj.y - cy, obj.width, obj.height)
|
||||||
pygame.draw.rect(surface, (80, 180, 220), mr)
|
pygame.draw.rect(surface, (80, 180, 220), mr)
|
||||||
@@ -5735,6 +6001,7 @@ platforms_floating = ROOM.platforms_floating
|
|||||||
platforms_hazard = ROOM.platforms_hazard
|
platforms_hazard = ROOM.platforms_hazard
|
||||||
platforms_trigger = ROOM.platforms_trigger
|
platforms_trigger = ROOM.platforms_trigger
|
||||||
platforms_moving = ROOM.platforms_moving
|
platforms_moving = ROOM.platforms_moving
|
||||||
|
platforms_dissolve = ROOM.platforms_dissolve
|
||||||
platform_meta = ROOM.platform_meta
|
platform_meta = ROOM.platform_meta
|
||||||
ground_platforms = ROOM.ground_platforms
|
ground_platforms = ROOM.ground_platforms
|
||||||
platforms = ROOM.platforms
|
platforms = ROOM.platforms
|
||||||
@@ -5966,6 +6233,7 @@ def apply_room(room: Room, set_player_spawn=True):
|
|||||||
"""Updates room-dependent lists + respawns enemies. Keeps inventory/equipment."""
|
"""Updates room-dependent lists + respawns enemies. Keeps inventory/equipment."""
|
||||||
global ROOM, KILL_Y, platforms_ground, platforms_floating, platforms_hazard, ground_platforms, platforms
|
global ROOM, KILL_Y, platforms_ground, platforms_floating, platforms_hazard, ground_platforms, platforms
|
||||||
global platforms_trigger, platforms_moving, platform_meta
|
global platforms_trigger, platforms_moving, platform_meta
|
||||||
|
global platforms_dissolve
|
||||||
global platform_layer
|
global platform_layer
|
||||||
global enemies, npcs, loot_drops, shockwaves, coin_drops, projectiles, ability_doors
|
global enemies, npcs, loot_drops, shockwaves, coin_drops, projectiles, ability_doors
|
||||||
global shatter_platforms, shatter_aoes, tether_shots
|
global shatter_platforms, shatter_aoes, tether_shots
|
||||||
@@ -5987,6 +6255,7 @@ def apply_room(room: Room, set_player_spawn=True):
|
|||||||
platforms_hazard = ROOM.platforms_hazard
|
platforms_hazard = ROOM.platforms_hazard
|
||||||
platforms_trigger = ROOM.platforms_trigger
|
platforms_trigger = ROOM.platforms_trigger
|
||||||
platforms_moving = ROOM.platforms_moving
|
platforms_moving = ROOM.platforms_moving
|
||||||
|
platforms_dissolve = ROOM.platforms_dissolve
|
||||||
platform_meta = ROOM.platform_meta
|
platform_meta = ROOM.platform_meta
|
||||||
ground_platforms = ROOM.ground_platforms
|
ground_platforms = ROOM.ground_platforms
|
||||||
platforms = ROOM.platforms
|
platforms = ROOM.platforms
|
||||||
@@ -6633,12 +6902,25 @@ def get_near_diary(player_obj, diary_obj) -> bool:
|
|||||||
return math.hypot(dx, dy) <= DiaryObject.INTERACT_RANGE
|
return math.hypot(dx, dy) <= DiaryObject.INTERACT_RANGE
|
||||||
|
|
||||||
|
|
||||||
|
CHEST_INTERACT_RANGE = 70
|
||||||
|
|
||||||
|
|
||||||
|
def get_near_chest(player_obj, chest_obj) -> bool:
|
||||||
|
if chest_obj is None or chest_obj.opened:
|
||||||
|
return False
|
||||||
|
dx = player_obj.rect.centerx - chest_obj.rect.centerx
|
||||||
|
dy = player_obj.rect.centery - chest_obj.rect.centery
|
||||||
|
return math.hypot(dx, dy) <= CHEST_INTERACT_RANGE
|
||||||
|
|
||||||
|
|
||||||
def get_near_lantern(player_obj, save_point_list, range_px: int | None = None) -> SavePoint | None:
|
def get_near_lantern(player_obj, save_point_list, range_px: int | None = None) -> SavePoint | None:
|
||||||
"""Return the nearest lantern the player can interact with, or None."""
|
"""Return the nearest un-used lantern the player can interact with, or None."""
|
||||||
best = None
|
best = None
|
||||||
best_d = None
|
best_d = None
|
||||||
px, py = player_obj.rect.midbottom
|
px, py = player_obj.rect.midbottom
|
||||||
for sp in save_point_list:
|
for sp in save_point_list:
|
||||||
|
if sp.used:
|
||||||
|
continue
|
||||||
r = range_px if range_px is not None else sp.radius
|
r = range_px if range_px is not None else sp.radius
|
||||||
if r <= 0:
|
if r <= 0:
|
||||||
continue
|
continue
|
||||||
@@ -6655,6 +6937,9 @@ def get_near_lantern(player_obj, save_point_list, range_px: int | None = None) -
|
|||||||
def save_at_lantern(sp: SavePoint):
|
def save_at_lantern(sp: SavePoint):
|
||||||
"""Set the lantern as the respawn point, restore HP/Charge, and save the game."""
|
"""Set the lantern as the respawn point, restore HP/Charge, and save the game."""
|
||||||
global LANTERN_RESPAWN_ROOM, LANTERN_RESPAWN_POS
|
global LANTERN_RESPAWN_ROOM, LANTERN_RESPAWN_POS
|
||||||
|
if sp.used:
|
||||||
|
return
|
||||||
|
sp.used = True
|
||||||
LANTERN_RESPAWN_ROOM = ROOM.id
|
LANTERN_RESPAWN_ROOM = ROOM.id
|
||||||
LANTERN_RESPAWN_POS = (sp.x, sp.y)
|
LANTERN_RESPAWN_POS = (sp.x, sp.y)
|
||||||
# Resting at a lantern fully restores the player.
|
# Resting at a lantern fully restores the player.
|
||||||
@@ -6949,6 +7234,9 @@ def draw_stats_overlay(surface):
|
|||||||
if not stats_overlay_visible:
|
if not stats_overlay_visible:
|
||||||
return
|
return
|
||||||
light_speed = PLAYER_SPEED * (1.0 + player.bonus_move_speed_pct + player.bonus_move_speed_light_pct)
|
light_speed = PLAYER_SPEED * (1.0 + player.bonus_move_speed_pct + player.bonus_move_speed_light_pct)
|
||||||
|
if player.mist_active:
|
||||||
|
shade_speed = light_speed
|
||||||
|
else:
|
||||||
shade_speed = PLAYER_SHADE_SPEED * (1.0 + player.bonus_move_speed_pct + player.bonus_move_speed_shade_pct)
|
shade_speed = PLAYER_SHADE_SPEED * (1.0 + player.bonus_move_speed_pct + player.bonus_move_speed_shade_pct)
|
||||||
heal_dur = player.effective_heal_duration()
|
heal_dur = player.effective_heal_duration()
|
||||||
cycle = player.effective_attack_duration() + player.effective_attack_cooldown()
|
cycle = player.effective_attack_duration() + player.effective_attack_cooldown()
|
||||||
@@ -7665,14 +7953,6 @@ while running:
|
|||||||
running = False
|
running = False
|
||||||
break
|
break
|
||||||
|
|
||||||
if (not paused) and (not inv_ui.visible) and (not trade_ui.visible):
|
|
||||||
if e.type == pygame.MOUSEBUTTONDOWN and e.button == 1:
|
|
||||||
player.set_attack_dir(camera_x, camera_y)
|
|
||||||
player.attack()
|
|
||||||
elif e.type == pygame.MOUSEBUTTONDOWN and e.button == 3:
|
|
||||||
player.set_attack_dir(camera_x, camera_y)
|
|
||||||
player.try_ranged_attack(projectiles, enemies, platforms)
|
|
||||||
|
|
||||||
if e.type == pygame.KEYDOWN:
|
if e.type == pygame.KEYDOWN:
|
||||||
was_trade_visible = trade_ui.visible
|
was_trade_visible = trade_ui.visible
|
||||||
was_inv_visible = inv_ui.visible
|
was_inv_visible = inv_ui.visible
|
||||||
@@ -7734,6 +8014,9 @@ while running:
|
|||||||
near_lantern = get_near_lantern(player, save_points)
|
near_lantern = get_near_lantern(player, save_points)
|
||||||
if near_lantern:
|
if near_lantern:
|
||||||
save_at_lantern(near_lantern)
|
save_at_lantern(near_lantern)
|
||||||
|
elif get_near_chest(player, reward_chest):
|
||||||
|
if reward_chest.open(player):
|
||||||
|
player.opened_chests.add(reward_chest.id)
|
||||||
elif get_near_diary(player, diary_obj):
|
elif get_near_diary(player, diary_obj):
|
||||||
diary_ui.toggle()
|
diary_ui.toggle()
|
||||||
else:
|
else:
|
||||||
@@ -7764,23 +8047,6 @@ while running:
|
|||||||
if key_is("toggle_shade", e.key):
|
if key_is("toggle_shade", e.key):
|
||||||
player.toggle_shade()
|
player.toggle_shade()
|
||||||
|
|
||||||
if key_is("attack", e.key):
|
|
||||||
player.set_attack_dir(camera_x, camera_y)
|
|
||||||
player.attack()
|
|
||||||
|
|
||||||
if key_is("ranged", e.key):
|
|
||||||
player.set_attack_dir(camera_x, camera_y)
|
|
||||||
player.try_ranged_attack(projectiles, enemies, platforms)
|
|
||||||
|
|
||||||
if key_is("dash", e.key):
|
|
||||||
player.try_dash()
|
|
||||||
|
|
||||||
if key_is("shatterstep", e.key):
|
|
||||||
player.try_shatterstep()
|
|
||||||
|
|
||||||
if key_is("tether", e.key):
|
|
||||||
player.try_tether(tether_shots)
|
|
||||||
|
|
||||||
if key_is("reality_break", e.key) and player.has_reality_break:
|
if key_is("reality_break", e.key) and player.has_reality_break:
|
||||||
if player.shade:
|
if player.shade:
|
||||||
ROOM_DROP_MULT = 0.5
|
ROOM_DROP_MULT = 0.5
|
||||||
@@ -7802,6 +8068,29 @@ while running:
|
|||||||
|
|
||||||
|
|
||||||
keys = pygame.key.get_pressed()
|
keys = pygame.key.get_pressed()
|
||||||
|
mouse_buttons = pygame.mouse.get_pressed()
|
||||||
|
|
||||||
|
# Hold any combat key (or mouse button) to keep re-casting the
|
||||||
|
# ability/attack as soon as its cooldown and resource checks allow
|
||||||
|
# (never faster than the cooldown).
|
||||||
|
if (not paused) and (not inv_ui.visible) and (not trade_ui.visible):
|
||||||
|
if key_pressed(keys, "attack") or mouse_buttons[0]:
|
||||||
|
player.set_attack_dir(camera_x, camera_y)
|
||||||
|
player.attack()
|
||||||
|
|
||||||
|
if key_pressed(keys, "ranged") or mouse_buttons[2]:
|
||||||
|
player.set_attack_dir(camera_x, camera_y)
|
||||||
|
player.try_ranged_attack(projectiles, enemies, platforms)
|
||||||
|
|
||||||
|
if key_pressed(keys, "dash"):
|
||||||
|
player.try_dash()
|
||||||
|
|
||||||
|
if key_pressed(keys, "shatterstep"):
|
||||||
|
player.try_shatterstep()
|
||||||
|
|
||||||
|
if key_pressed(keys, "tether"):
|
||||||
|
player.try_tether(tether_shots)
|
||||||
|
|
||||||
if paused:
|
if paused:
|
||||||
# draw last frame + pause overlay
|
# draw last frame + pause overlay
|
||||||
screen.fill(BG)
|
screen.fill(BG)
|
||||||
@@ -7934,6 +8223,8 @@ while running:
|
|||||||
support_moving = None
|
support_moving = None
|
||||||
if player.on_ground:
|
if player.on_ground:
|
||||||
for rect in platforms_moving:
|
for rect in platforms_moving:
|
||||||
|
if not moving_platform_active(rect):
|
||||||
|
continue
|
||||||
if (player.rect.bottom >= rect.top - 2 and player.rect.bottom <= rect.top + 5 and
|
if (player.rect.bottom >= rect.top - 2 and player.rect.bottom <= rect.top + 5 and
|
||||||
player.rect.right > rect.left and player.rect.left < rect.right):
|
player.rect.right > rect.left and player.rect.left < rect.right):
|
||||||
support_moving = rect
|
support_moving = rect
|
||||||
@@ -7958,7 +8249,13 @@ while running:
|
|||||||
# Trigger walls: solid only while their unlock quest is not yet active.
|
# Trigger walls: solid only while their unlock quest is not yet active.
|
||||||
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
|
# Dissolve platforms: solid while not dissolved; excluded from collision while dissolving.
|
||||||
|
solid_dissolve = update_dissolve_platforms(dt)
|
||||||
|
|
||||||
|
# Boss-gated moving platforms have no hitbox while their boss is alive.
|
||||||
|
inactive_moving_ids = {id(r) for r in platforms_moving if not moving_platform_active(r)}
|
||||||
|
player_solid = [r for r in platforms if id(r) not in inactive_moving_ids]
|
||||||
|
collision_platforms = player_solid + [d.rect for d in ability_doors if not d.open] + blocking_triggers + solid_dissolve
|
||||||
|
|
||||||
# Enemies collide with ground/floating platforms, doors and trigger walls —
|
# Enemies collide with ground/floating platforms, doors and trigger walls —
|
||||||
# but NOT moving platforms. Moving platforms are player traversal; a platform
|
# but NOT moving platforms. Moving platforms are player traversal; a platform
|
||||||
@@ -7969,6 +8266,7 @@ while running:
|
|||||||
enemy_collision_platforms = [r for r in platforms if id(r) not in moving_ids]
|
enemy_collision_platforms = [r for r in platforms if id(r) not in moving_ids]
|
||||||
enemy_collision_platforms += [d.rect for d in ability_doors if not d.open]
|
enemy_collision_platforms += [d.rect for d in ability_doors if not d.open]
|
||||||
enemy_collision_platforms += enemy_blocking_triggers
|
enemy_collision_platforms += enemy_blocking_triggers
|
||||||
|
enemy_collision_platforms += solid_dissolve
|
||||||
|
|
||||||
# Shade doors are solid for the world, but the player can pass through them
|
# 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
|
# freely while in shade form — and for a short grace period after switching
|
||||||
@@ -7997,6 +8295,8 @@ while running:
|
|||||||
# Glue the player to any moving platform they're standing on, so upward movement
|
# Glue the player to any moving platform they're standing on, so upward movement
|
||||||
# doesn't leave a gap that makes on_ground flicker (bobbing / eventual fall-off).
|
# doesn't leave a gap that makes on_ground flicker (bobbing / eventual fall-off).
|
||||||
for rect in platforms_moving:
|
for rect in platforms_moving:
|
||||||
|
if not moving_platform_active(rect):
|
||||||
|
continue
|
||||||
if (player.rect.bottom >= rect.top - 2 and player.rect.bottom <= rect.top + 5 and
|
if (player.rect.bottom >= rect.top - 2 and player.rect.bottom <= rect.top + 5 and
|
||||||
player.rect.right > rect.left and player.rect.left < rect.right):
|
player.rect.right > rect.left and player.rect.left < rect.right):
|
||||||
player.rect.bottom = rect.top
|
player.rect.bottom = rect.top
|
||||||
@@ -8287,14 +8587,12 @@ while running:
|
|||||||
|
|
||||||
# Pickup loot (only when inventory closed)
|
# Pickup loot (only when inventory closed)
|
||||||
if not inv_ui.visible and not trade_ui.visible:
|
if not inv_ui.visible and not trade_ui.visible:
|
||||||
if reward_chest and player.rect.colliderect(reward_chest.rect):
|
# Chests open on interact (F); the interact handler calls reward_chest.open().
|
||||||
if reward_chest.open(player):
|
|
||||||
player.opened_chests.add(reward_chest.id)
|
|
||||||
|
|
||||||
if reward_ability and player.rect.colliderect(reward_ability.rect):
|
if reward_ability and player.rect.colliderect(reward_ability.rect):
|
||||||
if reward_ability.ability_id == "ranged":
|
if reward_ability.ability_id == "ranged":
|
||||||
player.has_ranged = True
|
player.has_ranged = True
|
||||||
pickup_feed.push_center("Ranged Ability aquired (Press Q)", duration=5.0)
|
pickup_feed.push_center("Ranged Ability acquired (Right Mouse Button)", duration=5.0)
|
||||||
else:
|
else:
|
||||||
pickup_feed.push_center(f"Ability acquired: {reward_ability.ability_id}", duration=5.0)
|
pickup_feed.push_center(f"Ability acquired: {reward_ability.ability_id}", duration=5.0)
|
||||||
reward_ability = None
|
reward_ability = None
|
||||||
@@ -8460,6 +8758,16 @@ while running:
|
|||||||
int(near_lantern.rect.top - camera_y),
|
int(near_lantern.rect.top - camera_y),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Chest open hint bubble
|
||||||
|
if not inv_ui.visible and not trade_ui.visible and not diary_ui.visible:
|
||||||
|
if get_near_chest(player, reward_chest):
|
||||||
|
draw_speech_bubble(
|
||||||
|
screen,
|
||||||
|
"Press F - Open chest",
|
||||||
|
int(reward_chest.rect.centerx - camera_x),
|
||||||
|
int(reward_chest.rect.top - camera_y),
|
||||||
|
)
|
||||||
|
|
||||||
# Boss health bar (if boss exists)
|
# Boss health bar (if boss exists)
|
||||||
boss = next((en for en in enemies if getattr(en, "drop_profile", "") == "boss"), None)
|
boss = next((en for en in enemies if getattr(en, "drop_profile", "") == "boss"), None)
|
||||||
if boss is not None:
|
if boss is not None:
|
||||||
|
|||||||
@@ -1,22 +1,55 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"saved_at": "2026-10-01T10:39:22",
|
"saved_at": "2026-10-01T14:40:38",
|
||||||
"player": {
|
"player": {
|
||||||
"room_id": "room_04.json",
|
"room_id": "room_07.json",
|
||||||
"x": 509,
|
"x": 620,
|
||||||
"y": 240,
|
"y": 520,
|
||||||
"health": 10,
|
"health": 10,
|
||||||
"charge": 100.0,
|
"charge": 120.0,
|
||||||
"shade": false,
|
"shade": false,
|
||||||
"coins_light": 46,
|
"coins_light": 40,
|
||||||
"coins_dark": 6,
|
"coins_dark": 20,
|
||||||
"butterfly_jars": 2,
|
"butterfly_jars": 0,
|
||||||
"has_ranged": false,
|
"has_ranged": false,
|
||||||
"opened_chests": [
|
"opened_chests": [
|
||||||
"room_03.json@90,194"
|
"room_03.json@90,194",
|
||||||
|
"room_07.json@440,494"
|
||||||
],
|
],
|
||||||
"equipment": {
|
"equipment": {
|
||||||
"helmet": null,
|
"helmet": {
|
||||||
|
"id": "duskwarrior_helm",
|
||||||
|
"name": "Duskwarrior Helmet",
|
||||||
|
"set": "duskwarrior",
|
||||||
|
"slot": "helmet",
|
||||||
|
"base_stats": {},
|
||||||
|
"rarity_overrides": {
|
||||||
|
"tempered": {
|
||||||
|
"extra_stats": {
|
||||||
|
"charge": 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attuned": {
|
||||||
|
"extra_stats": {
|
||||||
|
"charge": 10,
|
||||||
|
"health": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"awakened": {
|
||||||
|
"extra_stats": {
|
||||||
|
"charge": 15,
|
||||||
|
"health": 5,
|
||||||
|
"charge_regen_pct": 0.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rarity": "tempered",
|
||||||
|
"tier": 1,
|
||||||
|
"stats": {
|
||||||
|
"charge": 10.0
|
||||||
|
},
|
||||||
|
"abilities": []
|
||||||
|
},
|
||||||
"chest": {
|
"chest": {
|
||||||
"id": "vitalweave_chest",
|
"id": "vitalweave_chest",
|
||||||
"name": "Vitalweave Chestplate",
|
"name": "Vitalweave Chestplate",
|
||||||
@@ -48,15 +81,162 @@
|
|||||||
},
|
},
|
||||||
"abilities": []
|
"abilities": []
|
||||||
},
|
},
|
||||||
"pants": null,
|
"pants": {
|
||||||
|
"id": "gravemark_greaves",
|
||||||
|
"name": "Gravemark Greaves",
|
||||||
|
"set": "gravemark",
|
||||||
|
"slot": "pants",
|
||||||
|
"base_stats": {},
|
||||||
|
"rarity_overrides": {
|
||||||
|
"tempered": {
|
||||||
|
"extra_stats": {
|
||||||
|
"charge": 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attuned": {
|
||||||
|
"extra_stats": {
|
||||||
|
"charge": 10,
|
||||||
|
"health": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"awakened": {
|
||||||
|
"extra_stats": {
|
||||||
|
"charge": 20,
|
||||||
|
"health": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rarity": "tempered",
|
||||||
|
"tier": 1,
|
||||||
|
"stats": {
|
||||||
|
"charge": 10.0
|
||||||
|
},
|
||||||
|
"abilities": []
|
||||||
|
},
|
||||||
"necklace": null
|
"necklace": null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"inventory": [],
|
"inventory": [
|
||||||
|
{
|
||||||
|
"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": "attuned",
|
||||||
|
"tier": 2,
|
||||||
|
"stats": {
|
||||||
|
"move_speed_light_pct": 0.1,
|
||||||
|
"heal_speed_pct": 0.1
|
||||||
|
},
|
||||||
|
"abilities": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "paladin_helm",
|
||||||
|
"name": "Paladin Helmet",
|
||||||
|
"set": "paladin",
|
||||||
|
"slot": "helmet",
|
||||||
|
"base_stats": {},
|
||||||
|
"rarity_overrides": {
|
||||||
|
"tempered": {
|
||||||
|
"extra_stats": {
|
||||||
|
"health": 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attuned": {
|
||||||
|
"extra_stats": {
|
||||||
|
"health": 10,
|
||||||
|
"negate_damage_chance": 0.05
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"awakened": {
|
||||||
|
"extra_stats": {
|
||||||
|
"health": 15,
|
||||||
|
"negate_damage_chance": 0.1,
|
||||||
|
"knockback_resistance": 0.25
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rarity": "tempered",
|
||||||
|
"tier": 1,
|
||||||
|
"stats": {
|
||||||
|
"health": 5.0
|
||||||
|
},
|
||||||
|
"abilities": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "necklace_flow",
|
||||||
|
"name": "Charm of Flow",
|
||||||
|
"slot": "necklace",
|
||||||
|
"item_type": "necklace",
|
||||||
|
"base_stats": {
|
||||||
|
"charge": 10
|
||||||
|
},
|
||||||
|
"rarity": "",
|
||||||
|
"tier": 0,
|
||||||
|
"abilities": [],
|
||||||
|
"stats": {
|
||||||
|
"charge": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"quests": {
|
"quests": {
|
||||||
"active": {
|
"active": {
|
||||||
"first_kills_shade": {
|
"first_kills_shade": {
|
||||||
"progress": 0
|
"progress": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"completed_one_time": [],
|
"completed_one_time": [],
|
||||||
@@ -68,11 +248,13 @@
|
|||||||
"boss_diary": {
|
"boss_diary": {
|
||||||
"slotted": {}
|
"slotted": {}
|
||||||
},
|
},
|
||||||
"defeated_bosses": [],
|
"defeated_bosses": [
|
||||||
|
"room_05.json"
|
||||||
|
],
|
||||||
"broken_doors": [],
|
"broken_doors": [],
|
||||||
"lantern_respawn": {
|
"lantern_respawn": {
|
||||||
"room_id": "room_04.json",
|
"room_id": "room_07.json",
|
||||||
"x": 560,
|
"x": 640,
|
||||||
"y": 240
|
"y": 520
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user