death + charge regen
death penalty and corpse, lantern to butterfly, butterfly jar, added the new stuff to level editor
This commit is contained in:
@@ -11,7 +11,7 @@
|
|||||||
"resources": {
|
"resources": {
|
||||||
"base_max_charge": 100.0,
|
"base_max_charge": 100.0,
|
||||||
"charge_drain_rate": 40.0,
|
"charge_drain_rate": 40.0,
|
||||||
"charge_refill_rate": 20.0,
|
"charge_refill_rate": 10.0,
|
||||||
"base_max_health": 10,
|
"base_max_health": 10,
|
||||||
"invincibility_duration": 1.0
|
"invincibility_duration": 1.0
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,6 +7,11 @@
|
|||||||
"group_spacing": 90
|
"group_spacing": 90
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"butterfly_jars": {
|
||||||
|
"icon_size": 16,
|
||||||
|
"icon_gap": 4
|
||||||
|
},
|
||||||
|
|
||||||
"inventory_ui": {
|
"inventory_ui": {
|
||||||
"panel": { "x": 20, "y": 20, "w": 560, "h": 545, "pad": 10 },
|
"panel": { "x": 20, "y": 20, "w": 560, "h": 545, "pad": 10 },
|
||||||
"list": { "w": 360, "line_h": 18, "header_h": 40, "pad_y": 6, "pad_right": 14 },
|
"list": { "w": 360, "line_h": 18, "header_h": 40, "pad_y": 6, "pad_right": 14 },
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
{
|
{
|
||||||
"objects": {
|
"objects": {
|
||||||
"lamp": {
|
"flow_butterfly": {
|
||||||
"type": "light_source",
|
"type": "light_source",
|
||||||
"charge_regen_range": 180
|
"charge_regen_range": 180
|
||||||
|
},
|
||||||
|
"lamp": {
|
||||||
|
"type": "save_point",
|
||||||
|
"charge_regen_range": 90
|
||||||
|
},
|
||||||
|
"flow_butterfly_jar": {
|
||||||
|
"type": "butterfly_jar"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 257 B |
Binary file not shown.
|
After Width: | Height: | Size: 385 B |
@@ -131,6 +131,21 @@
|
|||||||
],
|
],
|
||||||
"radius": 180
|
"radius": 180
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "save_point",
|
||||||
|
"pos": [
|
||||||
|
-60,
|
||||||
|
560
|
||||||
|
],
|
||||||
|
"radius": 110
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "butterfly_jar",
|
||||||
|
"pos": [
|
||||||
|
320,
|
||||||
|
560
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "quest_giver",
|
"type": "quest_giver",
|
||||||
"quest_id": "quest_first_kills",
|
"quest_id": "quest_first_kills",
|
||||||
|
|||||||
@@ -192,6 +192,39 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"ability_doors": [],
|
"ability_doors": [],
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"type": "save_point",
|
||||||
|
"pos": [
|
||||||
|
-300,
|
||||||
|
360
|
||||||
|
],
|
||||||
|
"radius": 110
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "butterfly_jar",
|
||||||
|
"pos": [
|
||||||
|
800,
|
||||||
|
360
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "light_source",
|
||||||
|
"pos": [
|
||||||
|
300,
|
||||||
|
360
|
||||||
|
],
|
||||||
|
"radius": 180
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "light_source",
|
||||||
|
"pos": [
|
||||||
|
1100,
|
||||||
|
360
|
||||||
|
],
|
||||||
|
"radius": 180
|
||||||
|
}
|
||||||
|
],
|
||||||
"entry_spawns": {
|
"entry_spawns": {
|
||||||
"entry_1": [
|
"entry_1": [
|
||||||
-420,
|
-420,
|
||||||
|
|||||||
@@ -352,7 +352,47 @@
|
|||||||
"type": "miniboss"
|
"type": "miniboss"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"props": [],
|
"props": [
|
||||||
|
{
|
||||||
|
"type": "save_point",
|
||||||
|
"pos": [
|
||||||
|
200,
|
||||||
|
320
|
||||||
|
],
|
||||||
|
"radius": 110
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "butterfly_jar",
|
||||||
|
"pos": [
|
||||||
|
420,
|
||||||
|
320
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "light_source",
|
||||||
|
"pos": [
|
||||||
|
500,
|
||||||
|
320
|
||||||
|
],
|
||||||
|
"radius": 180
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "light_source",
|
||||||
|
"pos": [
|
||||||
|
900,
|
||||||
|
2780
|
||||||
|
],
|
||||||
|
"radius": 180
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "light_source",
|
||||||
|
"pos": [
|
||||||
|
2600,
|
||||||
|
1860
|
||||||
|
],
|
||||||
|
"radius": 180
|
||||||
|
}
|
||||||
|
],
|
||||||
"exits": [
|
"exits": [
|
||||||
{
|
{
|
||||||
"x": -1160,
|
"x": -1160,
|
||||||
|
|||||||
@@ -62,6 +62,16 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"ability_doors": [],
|
"ability_doors": [],
|
||||||
|
"props": [
|
||||||
|
{
|
||||||
|
"type": "light_source",
|
||||||
|
"pos": [
|
||||||
|
-100,
|
||||||
|
360
|
||||||
|
],
|
||||||
|
"radius": 180
|
||||||
|
}
|
||||||
|
],
|
||||||
"entry_spawns": {
|
"entry_spawns": {
|
||||||
"entry_2": [
|
"entry_2": [
|
||||||
-360,
|
-360,
|
||||||
|
|||||||
@@ -96,6 +96,22 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"props": [
|
"props": [
|
||||||
|
{
|
||||||
|
"type": "save_point",
|
||||||
|
"pos": [
|
||||||
|
140,
|
||||||
|
360
|
||||||
|
],
|
||||||
|
"radius": 110
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "light_source",
|
||||||
|
"pos": [
|
||||||
|
1100,
|
||||||
|
360
|
||||||
|
],
|
||||||
|
"radius": 180
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "reward_chest",
|
"type": "reward_chest",
|
||||||
"pos": [
|
"pos": [
|
||||||
|
|||||||
@@ -49,6 +49,22 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"props": [
|
"props": [
|
||||||
|
{
|
||||||
|
"type": "save_point",
|
||||||
|
"pos": [
|
||||||
|
120,
|
||||||
|
360
|
||||||
|
],
|
||||||
|
"radius": 110
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "light_source",
|
||||||
|
"pos": [
|
||||||
|
900,
|
||||||
|
360
|
||||||
|
],
|
||||||
|
"radius": 180
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "diary",
|
"type": "diary",
|
||||||
"pos": [
|
"pos": [
|
||||||
|
|||||||
@@ -110,7 +110,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<div class="hr"></div>
|
<div class="hr"></div>
|
||||||
<h3 style="margin-top:10px;">Objects Catalog</h3>
|
<h3 style="margin-top:10px;">Objects Catalog</h3>
|
||||||
<div class="muted">Load objects.json to get default properties (lamp ranges, future objects).</div>
|
<div class="muted">Load objects.json to get default properties. Built-ins are always available: <b>flow_butterfly</b> (light source), <b>lamp</b> (save point), <b>flow_butterfly_jar</b> (collectible).</div>
|
||||||
<div class="row" style="margin-top:8px;">
|
<div class="row" style="margin-top:8px;">
|
||||||
<input id="objectsInput" type="file" accept=".json,application/json" />
|
<input id="objectsInput" type="file" accept=".json,application/json" />
|
||||||
</div>
|
</div>
|
||||||
@@ -188,6 +188,26 @@
|
|||||||
const nowId = ()=>Math.random().toString(16).slice(2) + Date.now().toString(16);
|
const nowId = ()=>Math.random().toString(16).slice(2) + Date.now().toString(16);
|
||||||
const objectCatalog = { byType: {}, byId: {} };
|
const objectCatalog = { byType: {}, byId: {} };
|
||||||
|
|
||||||
|
// Built-in object catalog so the new assets (butterfly light, lantern save point,
|
||||||
|
// butterfly-in-jar collectible) are always placeable even before loading objects.json.
|
||||||
|
const DEFAULT_OBJECT_CATALOG = {
|
||||||
|
flow_butterfly: { type: "light_source", charge_regen_range: 180 },
|
||||||
|
lamp: { type: "save_point", charge_regen_range: 90 },
|
||||||
|
flow_butterfly_jar: { type: "butterfly_jar" },
|
||||||
|
};
|
||||||
|
// Type fallbacks used when no object def provides a radius.
|
||||||
|
const TYPE_DEFAULT_RADII = { light_source: 180, save_point: 90 };
|
||||||
|
|
||||||
|
function seedDefaultObjectCatalog(){
|
||||||
|
objectCatalog.byType = {};
|
||||||
|
objectCatalog.byId = {};
|
||||||
|
for(const [id, def] of Object.entries(DEFAULT_OBJECT_CATALOG)){
|
||||||
|
objectCatalog.byId[id] = def;
|
||||||
|
if(def.type) objectCatalog.byType[def.type] = def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
seedDefaultObjectCatalog();
|
||||||
|
|
||||||
const snap = (v)=> Math.round(v / state.gridSize) * state.gridSize;
|
const snap = (v)=> Math.round(v / state.gridSize) * state.gridSize;
|
||||||
const snapPt = (x,y)=> [snap(x), snap(y)];
|
const snapPt = (x,y)=> [snap(x), snap(y)];
|
||||||
|
|
||||||
@@ -215,8 +235,9 @@
|
|||||||
|
|
||||||
function getDefaultRadiusForType(type, objectId){
|
function getDefaultRadiusForType(type, objectId){
|
||||||
const def = getObjectDefById(objectId) || getObjectDefByType(type);
|
const def = getObjectDefById(objectId) || getObjectDefByType(type);
|
||||||
const val = def && Number.isFinite(Number(def.charge_regen_range)) ? Number(def.charge_regen_range) : 180;
|
const val = def && Number.isFinite(Number(def.charge_regen_range)) ? Number(def.charge_regen_range) : 0;
|
||||||
return val > 0 ? val : 180;
|
if(val > 0) return val;
|
||||||
|
return TYPE_DEFAULT_RADII[type] || 180;
|
||||||
}
|
}
|
||||||
|
|
||||||
function roomTemplate(name="room_new"){
|
function roomTemplate(name="room_new"){
|
||||||
@@ -1332,22 +1353,48 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
ctx.fillText(typeId, sx+10*dpr, sy+4*dpr);
|
ctx.fillText(typeId, sx+10*dpr, sy+4*dpr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// props (light sources)
|
// props (light sources, save points, collectibles, etc.)
|
||||||
|
const PROP_STYLES = {
|
||||||
|
light_source: { ring: "rgba(250,204,21,0.25)", fill: "rgba(250,204,21,0.85)", label: "Butterfly" },
|
||||||
|
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" },
|
||||||
|
};
|
||||||
for(let i=0;i<(room.props||[]).length;i++){
|
for(let i=0;i<(room.props||[]).length;i++){
|
||||||
const p = room.props[i];
|
const p = room.props[i];
|
||||||
if(p.type !== "light_source") continue;
|
if(!p) continue;
|
||||||
|
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 [sx,sy] = worldToScreen(p.x, p.y, canvas);
|
const [sx,sy] = worldToScreen(p.x, p.y, canvas);
|
||||||
const r = Math.max(20, +p.radius || getDefaultRadiusForType(p.type || "light_source", p.object_id || p.objectId || null));
|
|
||||||
const v = getViewForCanvas(canvas);
|
const v = getViewForCanvas(canvas);
|
||||||
const rPx = r * (v.zoom || 1) * dpr;
|
const r = Math.max(20, +p.radius || getDefaultRadiusForType(ptype, p.object_id || p.objectId || null));
|
||||||
ctx.strokeStyle = "rgba(250,204,21,0.25)";
|
|
||||||
ctx.lineWidth = 2*dpr;
|
// radius ring (only for types that actually use it in-game)
|
||||||
ctx.beginPath(); ctx.arc(sx, sy, rPx, 0, Math.PI*2); ctx.stroke();
|
if(ptype === "light_source" || ptype === "save_point"){
|
||||||
ctx.fillStyle = "rgba(250,204,21,0.85)";
|
const rPx = r * (v.zoom || 1) * dpr;
|
||||||
ctx.beginPath(); ctx.arc(sx, sy, 6*dpr, 0, Math.PI*2); ctx.fill();
|
ctx.strokeStyle = st.ring;
|
||||||
|
ctx.lineWidth = 2*dpr;
|
||||||
|
ctx.beginPath(); ctx.arc(sx, sy, rPx, 0, Math.PI*2); ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// marker
|
||||||
|
ctx.fillStyle = st.fill;
|
||||||
|
if(ptype === "save_point"){
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(sx, sy-7*dpr);
|
||||||
|
ctx.lineTo(sx+7*dpr, sy);
|
||||||
|
ctx.lineTo(sx, sy+7*dpr);
|
||||||
|
ctx.lineTo(sx-7*dpr, sy);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.fill();
|
||||||
|
} else if(ptype === "butterfly_jar"){
|
||||||
|
ctx.fillRect(sx-6*dpr, sy-6*dpr, 12*dpr, 12*dpr);
|
||||||
|
} else {
|
||||||
|
ctx.beginPath(); ctx.arc(sx, sy, 6*dpr, 0, Math.PI*2); ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
ctx.fillStyle = "rgba(229,231,235,0.9)";
|
ctx.fillStyle = "rgba(229,231,235,0.9)";
|
||||||
ctx.font = `${11*dpr}px system-ui`;
|
ctx.font = `${11*dpr}px system-ui`;
|
||||||
ctx.fillText("Lamp", sx+10*dpr, sy+4*dpr);
|
ctx.fillText(st.label, sx+10*dpr, sy+4*dpr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// exits
|
// exits
|
||||||
@@ -1791,8 +1838,10 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
const p=room.props && room.props[sel.index];
|
const p=room.props && room.props[sel.index];
|
||||||
if(!p) return "<div class='muted'>Selection missing.</div>";
|
if(!p) return "<div class='muted'>Selection missing.</div>";
|
||||||
const prDefault = getDefaultRadiusForType(p.type || "light_source", p.object_id || p.objectId || null);
|
const prDefault = getDefaultRadiusForType(p.type || "light_source", p.object_id || p.objectId || null);
|
||||||
|
const objId = p.object_id || p.objectId || "";
|
||||||
|
const hint = ({light_source:"Butterfly (charge regen 2x)", save_point:"Lantern (save point)", butterfly_jar:"Butterfly-in-jar (collectible)"})[p.type||"light_source"] || "";
|
||||||
return `
|
return `
|
||||||
<div><b>Prop</b> #${sel.index}</div>
|
<div><b>Prop</b> #${sel.index}${hint ? ` <span class="mini">· ${hint}</span>` : ""}</div>
|
||||||
<div class="hr"></div>
|
<div class="hr"></div>
|
||||||
<div class="grid2">
|
<div class="grid2">
|
||||||
<div><div class="mini">x</div><input id="prx" type="number" value="${p.x}"/></div>
|
<div><div class="mini">x</div><input id="prx" type="number" value="${p.x}"/></div>
|
||||||
@@ -1802,6 +1851,10 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
<div class="mini">type</div>
|
<div class="mini">type</div>
|
||||||
<input id="prtype" type="text" value="${p.type||"light_source"}"/>
|
<input id="prtype" type="text" value="${p.type||"light_source"}"/>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="margin-top:8px;">
|
||||||
|
<div class="mini">object id</div>
|
||||||
|
<input id="probjid" type="text" value="${objId}" placeholder="(e.g. flow_butterfly, lamp, flow_butterfly_jar)"/>
|
||||||
|
</div>
|
||||||
<div style="margin-top:8px;">
|
<div style="margin-top:8px;">
|
||||||
<div class="mini">radius</div>
|
<div class="mini">radius</div>
|
||||||
<input id="prrad" type="number" value="${p.radius||prDefault}"/>
|
<input id="prrad" type="number" value="${p.radius||prDefault}"/>
|
||||||
@@ -1921,9 +1974,13 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
}));
|
}));
|
||||||
} else if(sel.kind==="prop"){
|
} else if(sel.kind==="prop"){
|
||||||
const p = room.props && room.props[sel.index]; if(!p) return;
|
const p = room.props && room.props[sel.index]; if(!p) return;
|
||||||
const prx=q("prx"), pry=q("pry"), prtype=q("prtype"), prrad=q("prrad");
|
const prx=q("prx"), pry=q("pry"), prtype=q("prtype"), probjid=q("probjid"), prrad=q("prrad");
|
||||||
if(prx) prx.addEventListener("change", ()=>pushAnd(()=>{ p.x=num(prx.value); }));
|
if(prx) prx.addEventListener("change", ()=>pushAnd(()=>{ p.x=num(prx.value); }));
|
||||||
if(pry) pry.addEventListener("change", ()=>pushAnd(()=>{ p.y=num(pry.value); }));
|
if(pry) pry.addEventListener("change", ()=>pushAnd(()=>{ p.y=num(pry.value); }));
|
||||||
|
if(probjid) probjid.addEventListener("change", ()=>pushAnd(()=>{
|
||||||
|
const v = probjid.value.trim();
|
||||||
|
if(v){ p.object_id = v; } else { delete p.object_id; }
|
||||||
|
}));
|
||||||
if(prtype) prtype.addEventListener("change", ()=>pushAnd(()=>{
|
if(prtype) prtype.addEventListener("change", ()=>pushAnd(()=>{
|
||||||
p.type=prtype.value.trim()||"light_source";
|
p.type=prtype.value.trim()||"light_source";
|
||||||
if(!(Number.isFinite(Number(p.radius)) && Number(p.radius) > 0)){
|
if(!(Number.isFinite(Number(p.radius)) && Number(p.radius) > 0)){
|
||||||
@@ -2834,8 +2891,9 @@ Cancel = Choose whether to remove without saving`);
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadObjectCatalog(data, filename){
|
function loadObjectCatalog(data, filename){
|
||||||
objectCatalog.byType = {};
|
// Keep the built-in defaults so the core assets are always available; a loaded
|
||||||
objectCatalog.byId = {};
|
// objects.json overrides individual definitions (same id) and adds new ones.
|
||||||
|
seedDefaultObjectCatalog();
|
||||||
|
|
||||||
if(data && typeof data === "object" && data.objects && typeof data.objects === "object"){
|
if(data && typeof data === "object" && data.objects && typeof data.objects === "object"){
|
||||||
for(const [id, def] of Object.entries(data.objects)){
|
for(const [id, def] of Object.entries(data.objects)){
|
||||||
|
|||||||
+410
-26
@@ -106,6 +106,7 @@ BINDINGS_DEFAULT = {
|
|||||||
"shatterstep": pygame.K_r,
|
"shatterstep": pygame.K_r,
|
||||||
"tether": pygame.K_x,
|
"tether": pygame.K_x,
|
||||||
"reality_break": pygame.K_c,
|
"reality_break": pygame.K_c,
|
||||||
|
"use_butterfly_jar": pygame.K_g,
|
||||||
"toggle_shade": pygame.K_LSHIFT,
|
"toggle_shade": pygame.K_LSHIFT,
|
||||||
"inventory": pygame.K_i,
|
"inventory": pygame.K_i,
|
||||||
"interact": pygame.K_f,
|
"interact": pygame.K_f,
|
||||||
@@ -165,6 +166,7 @@ SETTINGS_ACTIONS = [
|
|||||||
("Shatterstep", "shatterstep"),
|
("Shatterstep", "shatterstep"),
|
||||||
("Tether", "tether"),
|
("Tether", "tether"),
|
||||||
("Reality Break", "reality_break"),
|
("Reality Break", "reality_break"),
|
||||||
|
("Use Butterfly Jar", "use_butterfly_jar"),
|
||||||
("Toggle Shade", "toggle_shade"),
|
("Toggle Shade", "toggle_shade"),
|
||||||
("Inventory", "inventory"),
|
("Inventory", "inventory"),
|
||||||
("Interact/Trade", "interact"),
|
("Interact/Trade", "interact"),
|
||||||
@@ -246,6 +248,30 @@ def mark_door_broken(room_id: str, rect):
|
|||||||
BROKEN_DOORS.add(broken_door_key(room_id, rect))
|
BROKEN_DOORS.add(broken_door_key(room_id, rect))
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Collected butterfly jars (persisted in saves)
|
||||||
|
# ----------------------------
|
||||||
|
COLLECTED_JARS: set[str] = set()
|
||||||
|
BUTTERFLY_JARS_MAX = 3
|
||||||
|
|
||||||
|
|
||||||
|
def jar_key(room_id: str, x: int, y: int) -> str:
|
||||||
|
return f"{room_id}:{int(x)},{int(y)}"
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Lantern save-point respawn (persisted in saves)
|
||||||
|
# ----------------------------
|
||||||
|
LANTERN_RESPAWN_ROOM: str | None = None
|
||||||
|
LANTERN_RESPAWN_POS: tuple[int, int] | None = None
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Death penalty state (session-only, not persisted)
|
||||||
|
# ----------------------------
|
||||||
|
corpse = None # Corpse | None
|
||||||
|
JAR_DEATH_ROOM: str | None = None
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------
|
# ----------------------------
|
||||||
# Game config (numbers & colors)
|
# Game config (numbers & colors)
|
||||||
# ----------------------------
|
# ----------------------------
|
||||||
@@ -418,7 +444,7 @@ PLAYER_HITBOX_H = int(_cfg(["player", "hitbox_h"], 72))
|
|||||||
|
|
||||||
BASE_MAX_CHARGE = float(_phys(["resources", "base_max_charge"], 100.0))
|
BASE_MAX_CHARGE = float(_phys(["resources", "base_max_charge"], 100.0))
|
||||||
CHARGE_DRAIN_RATE = float(_phys(["resources", "charge_drain_rate"], 40.0))
|
CHARGE_DRAIN_RATE = float(_phys(["resources", "charge_drain_rate"], 40.0))
|
||||||
CHARGE_REFILL_RATE = float(_phys(["resources", "charge_refill_rate"], 20.0))
|
CHARGE_REFILL_RATE = float(_phys(["resources", "charge_refill_rate"], 10.0))
|
||||||
|
|
||||||
BASE_MAX_HEALTH = int(_phys(["resources", "base_max_health"], 10))
|
BASE_MAX_HEALTH = int(_phys(["resources", "base_max_health"], 10))
|
||||||
INVINCIBILITY_DURATION = float(_phys(["resources", "invincibility_duration"], 1.0))
|
INVINCIBILITY_DURATION = float(_phys(["resources", "invincibility_duration"], 1.0))
|
||||||
@@ -474,6 +500,10 @@ PLAYER_IDLE_BOB_SPEED = float(_anim(["player_idle", "bob_speed"], PLAYER_IDLE_BO
|
|||||||
COIN_UI_SIZE = int(_ui(["coins_hud", "icon_size"], 14))
|
COIN_UI_SIZE = int(_ui(["coins_hud", "icon_size"], 14))
|
||||||
COIN_UI_GAP = int(_ui(["coins_hud", "icon_gap"], 6))
|
COIN_UI_GAP = int(_ui(["coins_hud", "icon_gap"], 6))
|
||||||
|
|
||||||
|
# --- Butterfly jar HUD ---
|
||||||
|
BUTTERFLY_JAR_UI_SIZE = int(_ui(["butterfly_jars", "icon_size"], 16))
|
||||||
|
BUTTERFLY_JAR_UI_GAP = int(_ui(["butterfly_jars", "icon_gap"], 4))
|
||||||
|
|
||||||
pygame.init()
|
pygame.init()
|
||||||
|
|
||||||
# --- Window setup ---
|
# --- Window setup ---
|
||||||
@@ -1173,6 +1203,11 @@ TETHER_EDGE_DARK = _scale_image(TETHER_EDGE_DARK_RAW, TETHER_SPRITE_DRAW_SIZE)
|
|||||||
LAMP_PATH = OBJECTS_DIR / "lamp_1.png"
|
LAMP_PATH = OBJECTS_DIR / "lamp_1.png"
|
||||||
LAMP_RAW = _load_image_or_none(LAMP_PATH)
|
LAMP_RAW = _load_image_or_none(LAMP_PATH)
|
||||||
|
|
||||||
|
BUTTERFLY_PATH = OBJECTS_DIR / "flow_butterfly1.png"
|
||||||
|
BUTTERFLY_RAW = _load_image_or_none(BUTTERFLY_PATH)
|
||||||
|
BUTTERFLY_JAR_PATH = OBJECTS_DIR / "flow_butterfly_jar1.png"
|
||||||
|
BUTTERFLY_JAR_RAW = _load_image_or_none(BUTTERFLY_JAR_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------
|
# ----------------------------
|
||||||
@@ -1481,12 +1516,12 @@ class LightSource:
|
|||||||
self.y = int(y)
|
self.y = int(y)
|
||||||
self.radius = int(radius)
|
self.radius = int(radius)
|
||||||
self.t = random.random() * 10.0
|
self.t = random.random() * 10.0
|
||||||
if LAMP_RAW is not None:
|
if BUTTERFLY_RAW is not None:
|
||||||
self.img = LAMP_RAW
|
self.img = BUTTERFLY_RAW
|
||||||
self.rect = self.img.get_rect(midbottom=(self.x, self.y))
|
self.rect = self.img.get_rect(midbottom=(self.x, self.y))
|
||||||
else:
|
else:
|
||||||
self.img = None
|
self.img = None
|
||||||
self.rect = pygame.Rect(self.x - 8, self.y - 16, 16, 16)
|
self.rect = pygame.Rect(self.x - 16, self.y - 32, 32, 32)
|
||||||
|
|
||||||
def draw(self, surface, cx, cy):
|
def draw(self, surface, cx, cy):
|
||||||
# subtle pulse area
|
# subtle pulse area
|
||||||
@@ -1499,9 +1534,108 @@ class LightSource:
|
|||||||
surface.blit(overlay, (self.x - r - cx, self.y - r - cy))
|
surface.blit(overlay, (self.x - r - cx, self.y - r - cy))
|
||||||
|
|
||||||
if self.img is not None:
|
if self.img is not None:
|
||||||
surface.blit(self.img, (self.rect.x - cx, self.rect.y - cy))
|
bob = int(math.sin(self.t * 1.6) * 3)
|
||||||
|
surface.blit(self.img, (self.rect.x - cx, self.rect.y - cy - 6 + bob))
|
||||||
else:
|
else:
|
||||||
pygame.draw.rect(surface, (240, 230, 120), pygame.Rect(self.rect.x - cx, self.rect.y - cy, self.rect.width, self.rect.height))
|
pygame.draw.rect(surface, (240, 230, 120), pygame.Rect(self.rect.x - cx, self.rect.y - cy, self.rect.width, self.rect.height))
|
||||||
|
|
||||||
|
|
||||||
|
class SavePoint:
|
||||||
|
"""Lantern save point: auto-saves and becomes the respawn point when touched."""
|
||||||
|
def __init__(self, x: int, y: int, radius: int = 90):
|
||||||
|
self.x = int(x)
|
||||||
|
self.y = int(y)
|
||||||
|
self.radius = int(radius)
|
||||||
|
self.t = random.random() * 10.0
|
||||||
|
if LAMP_RAW is not None:
|
||||||
|
self.img = LAMP_RAW
|
||||||
|
self.rect = self.img.get_rect(midbottom=(self.x, self.y))
|
||||||
|
else:
|
||||||
|
self.img = None
|
||||||
|
self.rect = pygame.Rect(self.x - 16, self.y - 37, 32, 74)
|
||||||
|
|
||||||
|
def draw(self, surface, cx, cy):
|
||||||
|
self.t += 0.03
|
||||||
|
pulse = 0.10 + 0.05 * (0.5 + 0.5 * math.sin(self.t))
|
||||||
|
r = self.radius
|
||||||
|
overlay = pygame.Surface((r * 2, r * 2), pygame.SRCALPHA)
|
||||||
|
alpha = int(255 * pulse)
|
||||||
|
pygame.draw.circle(overlay, (150, 220, 255, alpha), (r, r), r, 2)
|
||||||
|
surface.blit(overlay, (self.x - r - cx, self.y - r - cy))
|
||||||
|
|
||||||
|
if self.img is not None:
|
||||||
|
surface.blit(self.img, (self.rect.x - cx, self.rect.y - cy))
|
||||||
|
else:
|
||||||
|
pygame.draw.rect(surface, (150, 200, 230), pygame.Rect(self.rect.x - cx, self.rect.y - cy, self.rect.width, self.rect.height))
|
||||||
|
|
||||||
|
|
||||||
|
class ButterflyJar:
|
||||||
|
"""Collectible butterfly-in-a-jar. The player can hold up to BUTTERFLY_JARS_MAX."""
|
||||||
|
def __init__(self, x: int, y: int, key: str = ""):
|
||||||
|
self.x = int(x)
|
||||||
|
self.y = int(y)
|
||||||
|
self.key = key
|
||||||
|
self.bob_t = random.random() * 10.0
|
||||||
|
if BUTTERFLY_JAR_RAW is not None:
|
||||||
|
self.img = BUTTERFLY_JAR_RAW
|
||||||
|
self.rect = self.img.get_rect(midbottom=(self.x, self.y))
|
||||||
|
else:
|
||||||
|
self.img = None
|
||||||
|
self.rect = pygame.Rect(self.x - 16, self.y - 32, 32, 32)
|
||||||
|
|
||||||
|
def update(self, dt: float):
|
||||||
|
self.bob_t += dt
|
||||||
|
|
||||||
|
def draw(self, surface, cx, cy):
|
||||||
|
bob_y = int(math.sin(self.bob_t * 2.0) * 3)
|
||||||
|
if self.img is not None:
|
||||||
|
surface.blit(self.img, (self.rect.x - cx, self.rect.y - cy + bob_y))
|
||||||
|
else:
|
||||||
|
pygame.draw.rect(surface, (200, 150, 230), (self.rect.x - cx, self.rect.y - cy + bob_y, self.rect.width, self.rect.height))
|
||||||
|
|
||||||
|
|
||||||
|
class Corpse:
|
||||||
|
"""A faded player model left where the player died, holding the coins they lost."""
|
||||||
|
def __init__(self, x: int, y: int, room_id: str, coins_light: int, coins_dark: int):
|
||||||
|
self.room_id = str(room_id)
|
||||||
|
self.x = int(x)
|
||||||
|
self.y = int(y)
|
||||||
|
self.coins_light = int(coins_light)
|
||||||
|
self.coins_dark = int(coins_dark)
|
||||||
|
self.t = random.random() * 10.0
|
||||||
|
self.rect = pygame.Rect(int(x), int(y), PLAYER_HITBOX_W, PLAYER_HITBOX_H)
|
||||||
|
self.rect.midbottom = (int(x), int(y))
|
||||||
|
self.img = self._build_faded_sprite()
|
||||||
|
|
||||||
|
def _build_faded_sprite(self):
|
||||||
|
if PLAYER_SPRITE_RAW is None:
|
||||||
|
return None
|
||||||
|
scale = PLAYER_SPRITE_SCALE
|
||||||
|
w = max(1, int(PLAYER_SPRITE_RAW.get_width() * scale))
|
||||||
|
h = max(1, int(PLAYER_SPRITE_RAW.get_height() * scale))
|
||||||
|
try:
|
||||||
|
img = pygame.transform.smoothscale(PLAYER_SPRITE_RAW, (w, h))
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
img.set_alpha(130)
|
||||||
|
return img
|
||||||
|
|
||||||
|
def draw(self, surface, cx, cy):
|
||||||
|
self.t += 0.02
|
||||||
|
bob = int(math.sin(self.t) * 2)
|
||||||
|
if self.img is not None:
|
||||||
|
draw_x = self.rect.x - cx - (self.img.get_width() - self.rect.width) // 2 + PLAYER_SPRITE_OFFSET_X
|
||||||
|
draw_y = self.rect.y - cy - (self.img.get_height() - self.rect.height) + PLAYER_SPRITE_OFFSET_Y + bob
|
||||||
|
surface.blit(self.img, (draw_x, draw_y))
|
||||||
|
else:
|
||||||
|
pygame.draw.rect(
|
||||||
|
surface,
|
||||||
|
PLAYER_LIGHT,
|
||||||
|
pygame.Rect(self.rect.x - cx, self.rect.y - cy, self.rect.width, self.rect.height),
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def spawn_boss_shockwave(boss, shockwaves_list: list[Shockwave]):
|
def spawn_boss_shockwave(boss, shockwaves_list: list[Shockwave]):
|
||||||
y = boss.rect.bottom - boss.shockwave_height
|
y = boss.rect.bottom - boss.shockwave_height
|
||||||
x = boss.rect.centerx
|
x = boss.rect.centerx
|
||||||
@@ -2148,10 +2282,22 @@ class InventoryUI:
|
|||||||
|
|
||||||
self.slot_icons = load_slot_icons()
|
self.slot_icons = load_slot_icons()
|
||||||
self.slot_icon_cache = {} # (slot, rarity) -> Surface
|
self.slot_icon_cache = {} # (slot, rarity) -> Surface
|
||||||
|
self.token_icon = None # lazily cached scaled boss-token icon
|
||||||
self.icon_size = int(_ui(["inventory_ui", "icons", "size"], 16))
|
self.icon_size = int(_ui(["inventory_ui", "icons", "size"], 16))
|
||||||
self.icon_gap = int(_ui(["inventory_ui", "icons", "gap"], 6))
|
self.icon_gap = int(_ui(["inventory_ui", "icons", "gap"], 6))
|
||||||
|
|
||||||
|
|
||||||
|
def _get_token_icon(self) -> pygame.Surface | None:
|
||||||
|
if self.token_icon is None:
|
||||||
|
if BOSS_TOKEN_SPRITE_RAW is not None:
|
||||||
|
try:
|
||||||
|
self.token_icon = pygame.transform.smoothscale(
|
||||||
|
BOSS_TOKEN_SPRITE_RAW, (self.icon_size, self.icon_size))
|
||||||
|
except Exception:
|
||||||
|
self.token_icon = None
|
||||||
|
return self.token_icon
|
||||||
|
|
||||||
|
|
||||||
def toggle(self):
|
def toggle(self):
|
||||||
self.visible = not self.visible
|
self.visible = not self.visible
|
||||||
if self.visible:
|
if self.visible:
|
||||||
@@ -2520,8 +2666,12 @@ class InventoryUI:
|
|||||||
|
|
||||||
name_fit = self._ellipsize(name, max(0, avail_for_name))
|
name_fit = self._ellipsize(name, max(0, avail_for_name))
|
||||||
|
|
||||||
# --- slot icon (tinted by rarity) ---
|
# --- slot icon (tinted by rarity), or token icon for boss tokens ---
|
||||||
icon = self._get_slot_icon(slot, rarity)
|
icon = None
|
||||||
|
if it.get("item_type") == "boss_token":
|
||||||
|
icon = self._get_token_icon()
|
||||||
|
if icon is None:
|
||||||
|
icon = self._get_slot_icon(slot, rarity)
|
||||||
icon_y = y + (self.line_h - self.icon_size) // 2
|
icon_y = y + (self.line_h - self.icon_size) // 2
|
||||||
if icon:
|
if icon:
|
||||||
surface.blit(icon, (x0, icon_y))
|
surface.blit(icon, (x0, icon_y))
|
||||||
@@ -2968,6 +3118,7 @@ class Player:
|
|||||||
|
|
||||||
self.coins_light = 0
|
self.coins_light = 0
|
||||||
self.coins_dark = 0
|
self.coins_dark = 0
|
||||||
|
self.butterfly_jars = 0
|
||||||
|
|
||||||
# --- abilities ---
|
# --- abilities ---
|
||||||
self.has_ranged = False
|
self.has_ranged = False
|
||||||
@@ -3454,6 +3605,15 @@ class Player:
|
|||||||
self.health = self.max_health
|
self.health = self.max_health
|
||||||
self.charge = self.max_charge
|
self.charge = self.max_charge
|
||||||
|
|
||||||
|
def consume_butterfly_jar(self) -> bool:
|
||||||
|
"""Consume one butterfly jar to instantly refill charge. Returns True if consumed."""
|
||||||
|
if self.butterfly_jars <= 0:
|
||||||
|
return False
|
||||||
|
self.butterfly_jars -= 1
|
||||||
|
self.charge = self.max_charge
|
||||||
|
self.charge_regen_lock = 0.0
|
||||||
|
return True
|
||||||
|
|
||||||
def _start_heal_if_possible(self, keys):
|
def _start_heal_if_possible(self, keys):
|
||||||
if self.shade:
|
if self.shade:
|
||||||
return
|
return
|
||||||
@@ -4503,12 +4663,18 @@ def draw_pause_controls(surface, x, y):
|
|||||||
f"Hold {key_name(key_for('heal'))} = heal",
|
f"Hold {key_name(key_for('heal'))} = heal",
|
||||||
f"{key_name(key_for('inventory'))} = inventory",
|
f"{key_name(key_for('inventory'))} = inventory",
|
||||||
f"{key_name(key_for('interact'))} = interact / trade",
|
f"{key_name(key_for('interact'))} = interact / trade",
|
||||||
|
f"{key_name(key_for('use_butterfly_jar'))} = butterfly jar",
|
||||||
f"{key_name(key_for('pause'))} = pause",
|
f"{key_name(key_for('pause'))} = pause",
|
||||||
"Click Close Game to exit",
|
"Click Close Game to exit",
|
||||||
]
|
]
|
||||||
|
line_h = 18
|
||||||
|
# Keep the whole list on screen: nudge upward if the requested y would overflow.
|
||||||
|
bottom = HEIGHT - 8
|
||||||
|
if y + len(lines) * line_h > bottom:
|
||||||
|
y = max(8, bottom - len(lines) * line_h)
|
||||||
for s in lines:
|
for s in lines:
|
||||||
surface.blit(font.render(s, True, (220, 220, 220)), (x, y))
|
surface.blit(font.render(s, True, (220, 220, 220)), (x, y))
|
||||||
y += 18
|
y += line_h
|
||||||
|
|
||||||
|
|
||||||
def get_pause_close_button_rect() -> pygame.Rect:
|
def get_pause_close_button_rect() -> pygame.Rect:
|
||||||
@@ -4834,6 +5000,8 @@ shatter_platforms: list[ShatterPlatform] = []
|
|||||||
shatter_aoes: list[ShatterAOE] = []
|
shatter_aoes: list[ShatterAOE] = []
|
||||||
tether_shots: list[TetherShot] = []
|
tether_shots: list[TetherShot] = []
|
||||||
light_sources: list[LightSource] = []
|
light_sources: list[LightSource] = []
|
||||||
|
save_points: list[SavePoint] = []
|
||||||
|
butterfly_jars: list[ButterflyJar] = []
|
||||||
dust_puffs: list[DustPuff] = []
|
dust_puffs: list[DustPuff] = []
|
||||||
class QuestGiver:
|
class QuestGiver:
|
||||||
"""Interactable environment object that hands out a quest."""
|
"""Interactable environment object that hands out a quest."""
|
||||||
@@ -4905,6 +5073,8 @@ def spawn_room_props(room: Room, player_obj: "Player"):
|
|||||||
reward_chest_obj = None
|
reward_chest_obj = None
|
||||||
reward_ability_obj = None
|
reward_ability_obj = None
|
||||||
lights: list[LightSource] = []
|
lights: list[LightSource] = []
|
||||||
|
save_points: list[SavePoint] = []
|
||||||
|
butterfly_jars: list[ButterflyJar] = []
|
||||||
quest_givers: list[QuestGiver] = []
|
quest_givers: list[QuestGiver] = []
|
||||||
diary_obj = None
|
diary_obj = None
|
||||||
|
|
||||||
@@ -4939,15 +5109,40 @@ def spawn_room_props(room: Room, player_obj: "Player"):
|
|||||||
elif p_type == "light_source":
|
elif p_type == "light_source":
|
||||||
radius = int(p.get("radius", 180))
|
radius = int(p.get("radius", 180))
|
||||||
lights.append(LightSource(x, y, radius))
|
lights.append(LightSource(x, y, radius))
|
||||||
|
elif p_type == "save_point":
|
||||||
|
radius = int(p.get("radius", 90))
|
||||||
|
save_points.append(SavePoint(x, y, radius))
|
||||||
|
elif p_type == "butterfly_jar":
|
||||||
|
key = jar_key(room.id, x, y)
|
||||||
|
if key not in COLLECTED_JARS:
|
||||||
|
butterfly_jars.append(ButterflyJar(x, y, key=key))
|
||||||
elif p_type == "quest_giver":
|
elif p_type == "quest_giver":
|
||||||
quest_givers.append(QuestGiver(x, y, str(p.get("quest_id", ""))))
|
quest_givers.append(QuestGiver(x, y, str(p.get("quest_id", ""))))
|
||||||
elif p_type == "diary":
|
elif p_type == "diary":
|
||||||
diary_obj = DiaryObject(x, y)
|
diary_obj = DiaryObject(x, y)
|
||||||
|
|
||||||
return reward_chest_obj, reward_ability_obj, lights, quest_givers, diary_obj
|
return reward_chest_obj, reward_ability_obj, lights, save_points, butterfly_jars, quest_givers, diary_obj
|
||||||
|
|
||||||
|
|
||||||
reward_chest, reward_ability, light_sources, quest_givers, diary_obj = spawn_room_props(ROOM, player)
|
def rebuild_room_butterfly_jars():
|
||||||
|
"""Rebuild the butterfly jar list for the current room (after jars respawn)."""
|
||||||
|
global butterfly_jars
|
||||||
|
butterfly_jars = []
|
||||||
|
for p in ROOM.props:
|
||||||
|
if not isinstance(p, dict):
|
||||||
|
continue
|
||||||
|
if str(p.get("type", "")) != "butterfly_jar":
|
||||||
|
continue
|
||||||
|
pos = p.get("pos", None)
|
||||||
|
if not (isinstance(pos, (list, tuple)) and len(pos) == 2):
|
||||||
|
continue
|
||||||
|
x, y = int(pos[0]), int(pos[1])
|
||||||
|
key = jar_key(ROOM.id, x, y)
|
||||||
|
if key not in COLLECTED_JARS:
|
||||||
|
butterfly_jars.append(ButterflyJar(x, y, key=key))
|
||||||
|
|
||||||
|
|
||||||
|
reward_chest, reward_ability, light_sources, save_points, butterfly_jars, quest_givers, diary_obj = spawn_room_props(ROOM, player)
|
||||||
|
|
||||||
def grant_full_set(set_id: str, rarity: str, equip: bool = True) -> bool:
|
def grant_full_set(set_id: str, rarity: str, equip: bool = True) -> bool:
|
||||||
slots = ["helmet", "chest", "pants"]
|
slots = ["helmet", "chest", "pants"]
|
||||||
@@ -4994,7 +5189,7 @@ def apply_room(room: Room, set_player_spawn=True):
|
|||||||
global ROOM, KILL_Y, platforms_ground, platforms_floating, ground_platforms, platforms
|
global ROOM, KILL_Y, platforms_ground, platforms_floating, ground_platforms, platforms
|
||||||
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
|
||||||
global light_sources
|
global light_sources, save_points, butterfly_jars
|
||||||
global dust_puffs
|
global dust_puffs
|
||||||
global dust_puffs
|
global dust_puffs
|
||||||
global reward_chest, reward_ability, quest_givers, diary_obj
|
global reward_chest, reward_ability, quest_givers, diary_obj
|
||||||
@@ -5026,6 +5221,8 @@ def apply_room(room: Room, set_player_spawn=True):
|
|||||||
shatter_aoes = []
|
shatter_aoes = []
|
||||||
tether_shots = []
|
tether_shots = []
|
||||||
light_sources = []
|
light_sources = []
|
||||||
|
save_points = []
|
||||||
|
butterfly_jars = []
|
||||||
dust_puffs = []
|
dust_puffs = []
|
||||||
dust_puffs = []
|
dust_puffs = []
|
||||||
ROOM_DROP_MULT = 1.0
|
ROOM_DROP_MULT = 1.0
|
||||||
@@ -5037,7 +5234,7 @@ def apply_room(room: Room, set_player_spawn=True):
|
|||||||
reward_chest = None
|
reward_chest = None
|
||||||
reward_ability = None
|
reward_ability = None
|
||||||
diary_obj = None
|
diary_obj = None
|
||||||
reward_chest, reward_ability, light_sources, quest_givers, diary_obj = spawn_room_props(ROOM, player)
|
reward_chest, reward_ability, light_sources, save_points, butterfly_jars, quest_givers, diary_obj = spawn_room_props(ROOM, player)
|
||||||
|
|
||||||
if set_player_spawn:
|
if set_player_spawn:
|
||||||
player.start_x = int(ROOM.player_spawn[0])
|
player.start_x = int(ROOM.player_spawn[0])
|
||||||
@@ -5049,6 +5246,62 @@ def apply_room(room: Room, set_player_spawn=True):
|
|||||||
def boss_alive_in_room() -> bool:
|
def boss_alive_in_room() -> bool:
|
||||||
return any(getattr(en, "type_id", "") == "boss" and en.health > 0 for en in enemies)
|
return any(getattr(en, "type_id", "") == "boss" and en.health > 0 for en in enemies)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
enemies = spawn_enemies_from_room(ROOM)
|
||||||
|
npcs = spawn_npcs_from_room(ROOM)
|
||||||
|
loot_drops = []
|
||||||
|
shockwaves = []
|
||||||
|
coin_drops = []
|
||||||
|
token_drops = []
|
||||||
|
projectiles = []
|
||||||
|
shatter_platforms = []
|
||||||
|
shatter_aoes = []
|
||||||
|
dust_puffs = []
|
||||||
|
tether_shots = []
|
||||||
|
ability_doors = spawn_ability_doors(ROOM)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_death_penalty():
|
||||||
|
"""On death: lose butterfly jars for good and drop coins into a corpse.
|
||||||
|
|
||||||
|
A corpse holds 100% of carried coins on the first death, or 50% on a repeat
|
||||||
|
death (the other 50% is lost forever and the old corpse vanishes). Void deaths
|
||||||
|
(falling out of the world) leave no recoverable corpse.
|
||||||
|
"""
|
||||||
|
global corpse, JAR_DEATH_ROOM
|
||||||
|
player.butterfly_jars = 0
|
||||||
|
JAR_DEATH_ROOM = ROOM.id
|
||||||
|
|
||||||
|
is_void_death = player.rect.top > KILL_Y
|
||||||
|
carried_l = player.coins_light
|
||||||
|
carried_d = player.coins_dark
|
||||||
|
if corpse is not None:
|
||||||
|
lost_l = int(carried_l * 0.5)
|
||||||
|
lost_d = int(carried_d * 0.5)
|
||||||
|
else:
|
||||||
|
lost_l = carried_l
|
||||||
|
lost_d = carried_d
|
||||||
|
if not is_void_death:
|
||||||
|
corpse = Corpse(player.rect.centerx, player.rect.bottom, ROOM.id, lost_l, lost_d)
|
||||||
|
player.coins_light = 0
|
||||||
|
player.coins_dark = 0
|
||||||
|
|
||||||
|
|
||||||
|
def respawn_jars_if_elsewhere() -> bool:
|
||||||
|
"""Respawn collected butterfly jars when saving at a lantern in a different room
|
||||||
|
than the one the player died in. Returns True if jars were respawned."""
|
||||||
|
global JAR_DEATH_ROOM
|
||||||
|
if JAR_DEATH_ROOM is not None and ROOM.id != JAR_DEATH_ROOM:
|
||||||
|
COLLECTED_JARS.clear()
|
||||||
|
JAR_DEATH_ROOM = None
|
||||||
|
rebuild_room_butterfly_jars()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def try_room_transition(keys):
|
def try_room_transition(keys):
|
||||||
if room_manager.transition_cooldown > 0:
|
if room_manager.transition_cooldown > 0:
|
||||||
return False
|
return False
|
||||||
@@ -5918,6 +6171,7 @@ def save_game(name: str = "auto") -> str:
|
|||||||
"shade": player.shade,
|
"shade": player.shade,
|
||||||
"coins_light": player.coins_light,
|
"coins_light": player.coins_light,
|
||||||
"coins_dark": player.coins_dark,
|
"coins_dark": player.coins_dark,
|
||||||
|
"butterfly_jars": player.butterfly_jars,
|
||||||
"has_ranged": player.has_ranged,
|
"has_ranged": player.has_ranged,
|
||||||
"reward_chest_opened": player.reward_chest_opened,
|
"reward_chest_opened": player.reward_chest_opened,
|
||||||
"equipment": {s: it for s, it in player.equipment.items()},
|
"equipment": {s: it for s, it in player.equipment.items()},
|
||||||
@@ -5931,6 +6185,10 @@ def save_game(name: str = "auto") -> str:
|
|||||||
"boss_diary": {"slotted": boss_diary.slotted},
|
"boss_diary": {"slotted": boss_diary.slotted},
|
||||||
"defeated_bosses": sorted(DEFEATED_BOSSES),
|
"defeated_bosses": sorted(DEFEATED_BOSSES),
|
||||||
"broken_doors": sorted(BROKEN_DOORS),
|
"broken_doors": sorted(BROKEN_DOORS),
|
||||||
|
"lantern_respawn": (
|
||||||
|
{"room_id": LANTERN_RESPAWN_ROOM, "x": LANTERN_RESPAWN_POS[0], "y": LANTERN_RESPAWN_POS[1]}
|
||||||
|
if LANTERN_RESPAWN_ROOM and LANTERN_RESPAWN_POS else None
|
||||||
|
),
|
||||||
}
|
}
|
||||||
path = save_path(name)
|
path = save_path(name)
|
||||||
with path.open("w", encoding="utf-8") as f:
|
with path.open("w", encoding="utf-8") as f:
|
||||||
@@ -5940,16 +6198,22 @@ def save_game(name: str = "auto") -> str:
|
|||||||
|
|
||||||
def apply_save_data(data: dict):
|
def apply_save_data(data: dict):
|
||||||
"""Apply a save's state to the current live game objects (player/inventory/quests/room)."""
|
"""Apply a save's state to the current live game objects (player/inventory/quests/room)."""
|
||||||
|
global LANTERN_RESPAWN_ROOM, LANTERN_RESPAWN_POS
|
||||||
|
global JAR_DEATH_ROOM, corpse
|
||||||
pdata = data.get("player", {}) or {}
|
pdata = data.get("player", {}) or {}
|
||||||
|
|
||||||
# persistent player fields
|
# persistent player fields
|
||||||
player.coins_light = int(pdata.get("coins_light", 0))
|
player.coins_light = int(pdata.get("coins_light", 0))
|
||||||
player.coins_dark = int(pdata.get("coins_dark", 0))
|
player.coins_dark = int(pdata.get("coins_dark", 0))
|
||||||
|
player.butterfly_jars = int(pdata.get("butterfly_jars", 0))
|
||||||
player.has_ranged = bool(pdata.get("has_ranged", False))
|
player.has_ranged = bool(pdata.get("has_ranged", False))
|
||||||
player.reward_chest_opened = bool(pdata.get("reward_chest_opened", False))
|
player.reward_chest_opened = bool(pdata.get("reward_chest_opened", False))
|
||||||
|
|
||||||
eq = pdata.get("equipment") or {}
|
eq = pdata.get("equipment") or {}
|
||||||
player.equipment = {s: (dict(it) if it else None) for s, it in eq.items()}
|
player.equipment = {s: None for s in ("helmet", "chest", "pants", "necklace")}
|
||||||
|
for s, it in eq.items():
|
||||||
|
if s in player.equipment:
|
||||||
|
player.equipment[s] = dict(it) if it else None
|
||||||
player.recalc_bonuses()
|
player.recalc_bonuses()
|
||||||
player._clamp_resources()
|
player._clamp_resources()
|
||||||
|
|
||||||
@@ -5967,10 +6231,27 @@ def apply_save_data(data: dict):
|
|||||||
DEFEATED_BOSSES.update(data.get("defeated_bosses") or [])
|
DEFEATED_BOSSES.update(data.get("defeated_bosses") or [])
|
||||||
BROKEN_DOORS.clear()
|
BROKEN_DOORS.clear()
|
||||||
BROKEN_DOORS.update(data.get("broken_doors") or [])
|
BROKEN_DOORS.update(data.get("broken_doors") or [])
|
||||||
|
# Butterfly jars respawn at their points on reload; the corpse is session-only.
|
||||||
|
COLLECTED_JARS.clear()
|
||||||
|
JAR_DEATH_ROOM = None
|
||||||
|
corpse = None
|
||||||
|
|
||||||
|
lrespawn = data.get("lantern_respawn") or {}
|
||||||
|
if lrespawn.get("room_id"):
|
||||||
|
LANTERN_RESPAWN_ROOM = normalize_room_id(str(lrespawn["room_id"]))
|
||||||
|
LANTERN_RESPAWN_POS = (int(lrespawn["x"]), int(lrespawn["y"]))
|
||||||
|
else:
|
||||||
|
LANTERN_RESPAWN_ROOM = None
|
||||||
|
LANTERN_RESPAWN_POS = None
|
||||||
|
|
||||||
|
# Reloading a save spawns the player at their latest lantern save point.
|
||||||
room_id = normalize_room_id(str(pdata.get("room_id") or "room_01.json"))
|
room_id = normalize_room_id(str(pdata.get("room_id") or "room_01.json"))
|
||||||
|
if LANTERN_RESPAWN_ROOM:
|
||||||
|
room_id = LANTERN_RESPAWN_ROOM
|
||||||
room = room_manager.load_room(room_id)
|
room = room_manager.load_room(room_id)
|
||||||
px, py = pdata.get("x"), pdata.get("y")
|
px, py = pdata.get("x"), pdata.get("y")
|
||||||
|
if LANTERN_RESPAWN_POS:
|
||||||
|
px, py = LANTERN_RESPAWN_POS
|
||||||
if isinstance(px, (int, float)) and isinstance(py, (int, float)):
|
if isinstance(px, (int, float)) and isinstance(py, (int, float)):
|
||||||
room.player_spawn = [int(px), int(py)]
|
room.player_spawn = [int(px), int(py)]
|
||||||
apply_room(room, set_player_spawn=True)
|
apply_room(room, set_player_spawn=True)
|
||||||
@@ -5996,6 +6277,7 @@ def fresh_save_data() -> dict:
|
|||||||
"shade": False,
|
"shade": False,
|
||||||
"coins_light": 0,
|
"coins_light": 0,
|
||||||
"coins_dark": 0,
|
"coins_dark": 0,
|
||||||
|
"butterfly_jars": 0,
|
||||||
"has_ranged": False,
|
"has_ranged": False,
|
||||||
"reward_chest_opened": False,
|
"reward_chest_opened": False,
|
||||||
"equipment": {},
|
"equipment": {},
|
||||||
@@ -6005,6 +6287,7 @@ def fresh_save_data() -> dict:
|
|||||||
"boss_diary": {"slotted": {}},
|
"boss_diary": {"slotted": {}},
|
||||||
"defeated_bosses": [],
|
"defeated_bosses": [],
|
||||||
"broken_doors": [],
|
"broken_doors": [],
|
||||||
|
"lantern_respawn": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -6417,6 +6700,29 @@ def _menu_draw(surface):
|
|||||||
draw_main_menu(surface, menu_index, menu_use_mouse, len(list_saves()) > 0)
|
draw_main_menu(surface, menu_index, menu_use_mouse, len(list_saves()) > 0)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_butterfly_jar_slots(surface, x, y):
|
||||||
|
"""Draw the butterfly-jar collectible slots below the coin counters.
|
||||||
|
|
||||||
|
Only shown once at least one jar has been collected. Always renders all
|
||||||
|
BUTTERFLY_JARS_MAX slots; collected ones are full-color, the rest greyed out.
|
||||||
|
"""
|
||||||
|
if player.butterfly_jars <= 0:
|
||||||
|
return
|
||||||
|
size = BUTTERFLY_JAR_UI_SIZE
|
||||||
|
for i in range(BUTTERFLY_JARS_MAX):
|
||||||
|
ix = x + i * (size + BUTTERFLY_JAR_UI_GAP)
|
||||||
|
if BUTTERFLY_JAR_RAW is not None:
|
||||||
|
icon = pygame.transform.scale(BUTTERFLY_JAR_RAW, (size, size))
|
||||||
|
surface.blit(icon, (ix, y))
|
||||||
|
if i >= player.butterfly_jars:
|
||||||
|
dim = pygame.Surface((size, size), pygame.SRCALPHA)
|
||||||
|
dim.fill((45, 45, 55, 205))
|
||||||
|
surface.blit(dim, (ix, y))
|
||||||
|
else:
|
||||||
|
col = (210, 170, 230) if i < player.butterfly_jars else (95, 95, 105)
|
||||||
|
pygame.draw.rect(surface, col, (ix, y, size, size))
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------
|
# ----------------------------
|
||||||
# Main loop
|
# Main loop
|
||||||
# ----------------------------
|
# ----------------------------
|
||||||
@@ -6428,6 +6734,7 @@ pause_menu_index = 0
|
|||||||
settings_index = 0
|
settings_index = 0
|
||||||
settings_wait_action: str | None = None
|
settings_wait_action: str | None = None
|
||||||
pause_use_mouse = False
|
pause_use_mouse = False
|
||||||
|
prev_near_save_point = False
|
||||||
while running:
|
while running:
|
||||||
dt = clock.tick(60) / 1000.0
|
dt = clock.tick(60) / 1000.0
|
||||||
|
|
||||||
@@ -6593,6 +6900,12 @@ while running:
|
|||||||
en.reality_break_glow = True
|
en.reality_break_glow = True
|
||||||
player.reality_break_flash = REALITY_BREAK_FLASH_DURATION
|
player.reality_break_flash = REALITY_BREAK_FLASH_DURATION
|
||||||
|
|
||||||
|
if key_is("use_butterfly_jar", e.key):
|
||||||
|
if player.consume_butterfly_jar():
|
||||||
|
pickup_feed.push_center("Butterfly jar consumed - Charge restored!", duration=2.0)
|
||||||
|
else:
|
||||||
|
pickup_feed.push_center("No butterfly jars left!", duration=1.5)
|
||||||
|
|
||||||
|
|
||||||
keys = pygame.key.get_pressed()
|
keys = pygame.key.get_pressed()
|
||||||
if paused:
|
if paused:
|
||||||
@@ -6644,6 +6957,12 @@ while running:
|
|||||||
ts.draw(screen, camera_x, camera_y)
|
ts.draw(screen, camera_x, camera_y)
|
||||||
for ls in light_sources:
|
for ls in light_sources:
|
||||||
ls.draw(screen, camera_x, camera_y)
|
ls.draw(screen, camera_x, camera_y)
|
||||||
|
for sp in save_points:
|
||||||
|
sp.draw(screen, camera_x, camera_y)
|
||||||
|
for bj in butterfly_jars:
|
||||||
|
bj.draw(screen, camera_x, camera_y)
|
||||||
|
if corpse is not None and corpse.room_id == ROOM.id:
|
||||||
|
corpse.draw(screen, camera_x, camera_y)
|
||||||
|
|
||||||
player.draw(screen, camera_x, camera_y)
|
player.draw(screen, camera_x, camera_y)
|
||||||
for npc in npcs:
|
for npc in npcs:
|
||||||
@@ -6694,6 +7013,11 @@ while running:
|
|||||||
font.render(str(player.coins_dark), True, (240, 240, 240)),
|
font.render(str(player.coins_dark), True, (240, 240, 240)),
|
||||||
(coin_x + COIN_UI_SIZE + COIN_UI_GAP, coin_y - 1)
|
(coin_x + COIN_UI_SIZE + COIN_UI_GAP, coin_y - 1)
|
||||||
)
|
)
|
||||||
|
draw_butterfly_jar_slots(
|
||||||
|
screen,
|
||||||
|
WIDTH - int(_ui(["coins_hud", "x_start_offset"], 220)),
|
||||||
|
coin_y + COIN_UI_SIZE + COIN_UI_GAP + 4,
|
||||||
|
)
|
||||||
|
|
||||||
overlay = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
|
overlay = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
|
||||||
overlay.fill((0, 0, 0, 150))
|
overlay.fill((0, 0, 0, 150))
|
||||||
@@ -6734,6 +7058,26 @@ while running:
|
|||||||
if (dx * dx + dy * dy) <= (ls.radius * ls.radius):
|
if (dx * dx + dy * dy) <= (ls.radius * ls.radius):
|
||||||
player.charge_regen_mult = 2.0
|
player.charge_regen_mult = 2.0
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Lantern save point: touching a lantern sets the respawn point + auto-saves.
|
||||||
|
near_save_point = False
|
||||||
|
for sp in save_points:
|
||||||
|
px, py = player.rect.midbottom
|
||||||
|
dx = px - sp.x
|
||||||
|
dy = py - sp.y
|
||||||
|
if (dx * dx + dy * dy) <= (sp.radius * sp.radius):
|
||||||
|
near_save_point = True
|
||||||
|
LANTERN_RESPAWN_ROOM = ROOM.id
|
||||||
|
LANTERN_RESPAWN_POS = (sp.x, sp.y)
|
||||||
|
break
|
||||||
|
if near_save_point and not prev_near_save_point:
|
||||||
|
_do_save_game()
|
||||||
|
pickup_feed.push_center("Lantern - game saved", duration=2.0)
|
||||||
|
# Butterfly jars lost on death respawn after saving at a lantern in a different room.
|
||||||
|
if respawn_jars_if_elsewhere():
|
||||||
|
pickup_feed.push_center("Butterfly jars have respawned!", duration=2.5)
|
||||||
|
prev_near_save_point = near_save_point
|
||||||
|
|
||||||
collision_platforms = platforms + [d.rect for d in ability_doors if not d.open]
|
collision_platforms = platforms + [d.rect for d in ability_doors if not d.open]
|
||||||
player_collision_platforms = collision_platforms + [p.rect for p in shatter_platforms if p.alive]
|
player_collision_platforms = collision_platforms + [p.rect for p in shatter_platforms if p.alive]
|
||||||
|
|
||||||
@@ -6792,6 +7136,7 @@ while running:
|
|||||||
|
|
||||||
# Respawn on death/void
|
# Respawn on death/void
|
||||||
if player.rect.top > KILL_Y or player.health <= 0:
|
if player.rect.top > KILL_Y or player.health <= 0:
|
||||||
|
apply_death_penalty()
|
||||||
if ACTIVE_REFIGHT is not None:
|
if ACTIVE_REFIGHT is not None:
|
||||||
# Died in a diary refight -> return to the diary in town.
|
# Died in a diary refight -> return to the diary in town.
|
||||||
ACTIVE_REFIGHT = None
|
ACTIVE_REFIGHT = None
|
||||||
@@ -6803,21 +7148,22 @@ while running:
|
|||||||
player.shatterstep_landing_pending = False
|
player.shatterstep_landing_pending = False
|
||||||
player.shatterstep_landing_damage = 0.0
|
player.shatterstep_landing_damage = 0.0
|
||||||
else:
|
else:
|
||||||
player.respawn(int(ROOM.player_spawn[0]), int(ROOM.player_spawn[1]))
|
# Respawn at the latest lantern save point if one has been touched.
|
||||||
enemies = spawn_enemies_from_room(ROOM)
|
if LANTERN_RESPAWN_ROOM and LANTERN_RESPAWN_POS:
|
||||||
npcs = spawn_npcs_from_room(ROOM)
|
lx, ly = LANTERN_RESPAWN_POS
|
||||||
loot_drops = []
|
if LANTERN_RESPAWN_ROOM != ROOM.id:
|
||||||
shockwaves = []
|
room = room_manager.load_room(LANTERN_RESPAWN_ROOM)
|
||||||
coin_drops = []
|
room.player_spawn = [int(lx), int(ly)]
|
||||||
token_drops = []
|
apply_room(room, set_player_spawn=True)
|
||||||
projectiles = []
|
player.respawn(int(lx), int(ly))
|
||||||
shatter_platforms = []
|
else:
|
||||||
shatter_aoes = []
|
player.respawn(int(lx), int(ly))
|
||||||
dust_puffs = []
|
reset_room_combat_state()
|
||||||
tether_shots = []
|
else:
|
||||||
|
player.respawn(int(ROOM.player_spawn[0]), int(ROOM.player_spawn[1]))
|
||||||
|
reset_room_combat_state()
|
||||||
player.shatterstep_landing_pending = False
|
player.shatterstep_landing_pending = False
|
||||||
player.shatterstep_landing_damage = 0.0
|
player.shatterstep_landing_damage = 0.0
|
||||||
ability_doors = spawn_ability_doors(ROOM)
|
|
||||||
|
|
||||||
# Update enemies
|
# Update enemies
|
||||||
for en in enemies:
|
for en in enemies:
|
||||||
@@ -6916,6 +7262,9 @@ while running:
|
|||||||
for t in token_drops:
|
for t in token_drops:
|
||||||
t.update(dt, collision_platforms)
|
t.update(dt, collision_platforms)
|
||||||
|
|
||||||
|
for bj in butterfly_jars:
|
||||||
|
bj.update(dt)
|
||||||
|
|
||||||
if reward_ability:
|
if reward_ability:
|
||||||
reward_ability.update(dt)
|
reward_ability.update(dt)
|
||||||
|
|
||||||
@@ -6995,6 +7344,19 @@ while running:
|
|||||||
else:
|
else:
|
||||||
player.coins_dark += 1
|
player.coins_dark += 1
|
||||||
coin_drops.remove(c)
|
coin_drops.remove(c)
|
||||||
|
for bj in butterfly_jars[:]:
|
||||||
|
if player.rect.colliderect(bj.rect):
|
||||||
|
if player.butterfly_jars < BUTTERFLY_JARS_MAX:
|
||||||
|
player.butterfly_jars += 1
|
||||||
|
if bj.key:
|
||||||
|
COLLECTED_JARS.add(bj.key)
|
||||||
|
pickup_feed.push_center(
|
||||||
|
f"Butterfly jar collected ({player.butterfly_jars}/{BUTTERFLY_JARS_MAX})",
|
||||||
|
duration=2.5,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
pickup_feed.push_center("You can't carry any more butterfly jars!", duration=2.0)
|
||||||
|
butterfly_jars.remove(bj)
|
||||||
for t in token_drops[:]:
|
for t in token_drops[:]:
|
||||||
if player.rect.colliderect(t.rect):
|
if player.rect.colliderect(t.rect):
|
||||||
token = make_boss_token(t.boss_id)
|
token = make_boss_token(t.boss_id)
|
||||||
@@ -7002,6 +7364,16 @@ while running:
|
|||||||
pickup_feed.push_plain(f"Collected {token['name']}!")
|
pickup_feed.push_plain(f"Collected {token['name']}!")
|
||||||
token_drops.remove(t)
|
token_drops.remove(t)
|
||||||
|
|
||||||
|
# Corpse: touching it grants back the coins lost on death.
|
||||||
|
if corpse is not None and corpse.room_id == ROOM.id:
|
||||||
|
if player.rect.colliderect(corpse.rect):
|
||||||
|
recovered = corpse.coins_light + corpse.coins_dark
|
||||||
|
player.coins_light += corpse.coins_light
|
||||||
|
player.coins_dark += corpse.coins_dark
|
||||||
|
if recovered > 0:
|
||||||
|
pickup_feed.push_center(f"Recovered {recovered} coins from your corpse!", duration=2.5)
|
||||||
|
corpse = None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Camera follow
|
# Camera follow
|
||||||
@@ -7020,6 +7392,10 @@ while running:
|
|||||||
|
|
||||||
for ls in light_sources:
|
for ls in light_sources:
|
||||||
ls.draw(screen, camera_x, camera_y)
|
ls.draw(screen, camera_x, camera_y)
|
||||||
|
for sp in save_points:
|
||||||
|
sp.draw(screen, camera_x, camera_y)
|
||||||
|
for bj in butterfly_jars:
|
||||||
|
bj.draw(screen, camera_x, camera_y)
|
||||||
|
|
||||||
for p in platforms:
|
for p in platforms:
|
||||||
pygame.draw.rect(
|
pygame.draw.rect(
|
||||||
@@ -7094,6 +7470,8 @@ while running:
|
|||||||
# entities
|
# entities
|
||||||
for npc in npcs:
|
for npc in npcs:
|
||||||
npc.draw(screen, camera_x, camera_y)
|
npc.draw(screen, camera_x, camera_y)
|
||||||
|
if corpse is not None and corpse.room_id == ROOM.id:
|
||||||
|
corpse.draw(screen, camera_x, camera_y)
|
||||||
player.draw(screen, camera_x, camera_y)
|
player.draw(screen, camera_x, camera_y)
|
||||||
for en in enemies:
|
for en in enemies:
|
||||||
en.draw(screen, camera_x, camera_y)
|
en.draw(screen, camera_x, camera_y)
|
||||||
@@ -7212,6 +7590,12 @@ while running:
|
|||||||
(coin_x + COIN_UI_SIZE + COIN_UI_GAP, coin_y - 1)
|
(coin_x + COIN_UI_SIZE + COIN_UI_GAP, coin_y - 1)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
draw_butterfly_jar_slots(
|
||||||
|
screen,
|
||||||
|
WIDTH - int(_ui(["coins_hud", "x_start_offset"], 220)),
|
||||||
|
coin_y + COIN_UI_SIZE + COIN_UI_GAP + 4,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
inv_ui.draw(screen)
|
inv_ui.draw(screen)
|
||||||
|
|||||||
@@ -1,28 +1,106 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"saved_at": "2026-09-28T09:46:05",
|
"saved_at": "2026-09-28T10:53:55",
|
||||||
"player": {
|
"player": {
|
||||||
"room_id": "room_02.json",
|
"room_id": "room_town_gloomreach.json",
|
||||||
"x": -45,
|
"x": 120,
|
||||||
"y": 360,
|
"y": 360,
|
||||||
"health": 10,
|
"health": 9,
|
||||||
"charge": 100.0,
|
"charge": 64.71,
|
||||||
"shade": false,
|
"shade": false,
|
||||||
"coins_light": 23,
|
"coins_light": 84,
|
||||||
"coins_dark": 2,
|
"coins_dark": 41,
|
||||||
"has_ranged": false,
|
"butterfly_jars": 2,
|
||||||
"reward_chest_opened": false,
|
"has_ranged": true,
|
||||||
"equipment": {}
|
"reward_chest_opened": true,
|
||||||
|
"equipment": {
|
||||||
|
"helmet": null,
|
||||||
|
"chest": null,
|
||||||
|
"pants": null,
|
||||||
|
"necklace": null
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"inventory": [],
|
"inventory": [
|
||||||
|
{
|
||||||
|
"id": "necklace_placeholder",
|
||||||
|
"name": "Plain Necklace",
|
||||||
|
"slot": "necklace",
|
||||||
|
"item_type": "necklace",
|
||||||
|
"base_stats": {
|
||||||
|
"health": 2
|
||||||
|
},
|
||||||
|
"rarity": "",
|
||||||
|
"tier": 0,
|
||||||
|
"abilities": [],
|
||||||
|
"stats": {
|
||||||
|
"health": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_type": "boss_token",
|
||||||
|
"boss_id": "boss_01",
|
||||||
|
"name": "Gloom Warden's Token",
|
||||||
|
"tier": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
"quests": {
|
"quests": {
|
||||||
"active": {},
|
"active": {
|
||||||
|
"quest_first_kills": {
|
||||||
|
"progress": 12
|
||||||
|
}
|
||||||
|
},
|
||||||
"completed_one_time": [],
|
"completed_one_time": [],
|
||||||
"tokens": 0
|
"tokens": 0
|
||||||
},
|
},
|
||||||
"boss_diary": {
|
"boss_diary": {
|
||||||
"slotted": {}
|
"slotted": {}
|
||||||
},
|
},
|
||||||
"defeated_bosses": [],
|
"defeated_bosses": [
|
||||||
"broken_doors": []
|
"room_boss_01.json"
|
||||||
|
],
|
||||||
|
"broken_doors": [
|
||||||
|
"room_07.json:-980,2180,80,600",
|
||||||
|
"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": 120,
|
||||||
|
"y": 360
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user