death + charge regen

death penalty and corpse, lantern to butterfly, butterfly jar, added the new stuff to level editor
This commit is contained in:
2026-09-28 21:01:30 +02:00
parent 17d4e9cdf3
commit 2a86881108
14 changed files with 723 additions and 61 deletions
+75 -17
View File
@@ -110,7 +110,7 @@
</label>
<div class="hr"></div>
<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;">
<input id="objectsInput" type="file" accept=".json,application/json" />
</div>
@@ -188,6 +188,26 @@
const nowId = ()=>Math.random().toString(16).slice(2) + Date.now().toString(16);
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 snapPt = (x,y)=> [snap(x), snap(y)];
@@ -215,8 +235,9 @@
function getDefaultRadiusForType(type, objectId){
const def = getObjectDefById(objectId) || getObjectDefByType(type);
const val = def && Number.isFinite(Number(def.charge_regen_range)) ? Number(def.charge_regen_range) : 180;
return val > 0 ? val : 180;
const val = def && Number.isFinite(Number(def.charge_regen_range)) ? Number(def.charge_regen_range) : 0;
if(val > 0) return val;
return TYPE_DEFAULT_RADII[type] || 180;
}
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);
}
// 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++){
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 r = Math.max(20, +p.radius || getDefaultRadiusForType(p.type || "light_source", p.object_id || p.objectId || null));
const v = getViewForCanvas(canvas);
const rPx = r * (v.zoom || 1) * dpr;
ctx.strokeStyle = "rgba(250,204,21,0.25)";
ctx.lineWidth = 2*dpr;
ctx.beginPath(); ctx.arc(sx, sy, rPx, 0, Math.PI*2); ctx.stroke();
ctx.fillStyle = "rgba(250,204,21,0.85)";
ctx.beginPath(); ctx.arc(sx, sy, 6*dpr, 0, Math.PI*2); ctx.fill();
const r = Math.max(20, +p.radius || getDefaultRadiusForType(ptype, p.object_id || p.objectId || null));
// radius ring (only for types that actually use it in-game)
if(ptype === "light_source" || ptype === "save_point"){
const rPx = r * (v.zoom || 1) * dpr;
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.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
@@ -1791,8 +1838,10 @@ Cancel = Choose whether to remove without saving`);
const p=room.props && room.props[sel.index];
if(!p) return "<div class='muted'>Selection missing.</div>";
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 `
<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="grid2">
<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>
<input id="prtype" type="text" value="${p.type||"light_source"}"/>
</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 class="mini">radius</div>
<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"){
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(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(()=>{
p.type=prtype.value.trim()||"light_source";
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){
objectCatalog.byType = {};
objectCatalog.byId = {};
// Keep the built-in defaults so the core assets are always available; a loaded
// objects.json overrides individual definitions (same id) and adds new ones.
seedDefaultObjectCatalog();
if(data && typeof data === "object" && data.objects && typeof data.objects === "object"){
for(const [id, def] of Object.entries(data.objects)){