mist/attack

mist, charge empty lock, attack changes, floating platform changes
This commit is contained in:
2026-10-01 10:49:35 +02:00
parent ab35845572
commit bf4e8de590
10 changed files with 688 additions and 146 deletions
+66 -9
View File
@@ -88,9 +88,10 @@
<div class="row">
<button id="downloadActiveBtn" class="good">Download Active</button>
<button id="downloadCheckedBtn">Download Checked</button>
<button id="downloadAllBtn">Download All</button>
</div>
<div class="muted" style="margin-top:6px;">
Downloads are per-room. Checked download will save one file per checked room.
Downloads are per-room. Checked download saves one file per checked room; Download All saves every loaded room.
</div>
</div>
@@ -278,7 +279,14 @@
const DEFAULT_ENEMY_TYPES = {
enemy: { rect: [40, 60], health: 3, speed: 2.2, air_speed: 2.0, detection_range: 240, contact_damage: 1, drop_profile: "normal" },
miniboss: { rect: [70, 95], health: 18, speed: 1.8, air_speed: 1.6, detection_range: 360, contact_damage: 2, drop_profile: "miniboss" },
boss: { rect: [110, 120], health: 10, speed: 1.6, air_speed: 1.4, detection_range: 500, contact_damage: 3, drop_profile: "boss" },
boss: {
rect: [110, 120], health: 10, speed: 1.6, air_speed: 1.4, detection_range: 500, contact_damage: 3, drop_profile: "boss", behavior: "shockwave",
charger: {
retreat_dist: 150, retreat_jump_vel: -14.0, retreat_speed_mult: 1.2, charge_speed_mult: 4.0,
charge_duration: 1.1, dash_min_dist: 160, dash_max_dist: 420, dash_speed_mult: 4.2,
dash_duration: 0.7, attack_cooldown: 1.8, above_y: 40
}
},
};
function seedDefaultObjectCatalog(){
@@ -435,9 +443,9 @@
for(const e of src.enemies){
if(e && typeof e === "object"){
if(Array.isArray(e.pos)){
r.enemies.push({ x:+e.pos[0]||0, y:+e.pos[1]||0, type: e.type || "enemy", layer: +(e.layer||0) });
r.enemies.push({ x:+e.pos[0]||0, y:+e.pos[1]||0, type: e.type || "enemy", layer: +(e.layer||0), behavior: e.behavior || "" });
} else if(typeof e.x === "number" || typeof e.y === "number"){
r.enemies.push({ x:+e.x||0, y:+e.y||0, type: e.type || "enemy", layer: +(e.layer||0) });
r.enemies.push({ x:+e.x||0, y:+e.y||0, type: e.type || "enemy", layer: +(e.layer||0), behavior: e.behavior || "" });
}
}
}
@@ -524,7 +532,7 @@
// ----- keep other top-level fields (future-proof) -----
r._extras = {};
if(src && typeof src === "object"){
const keep = new Set(["meta","player_spawn","bounds","platforms","enemies","npcs","exits","ability_doors","entry_spawns","props","name","display_name","room_w","room_h"]);
const keep = new Set(["meta","player_spawn","bounds","platforms","enemies","npcs","exits","ability_doors","entry_spawns","props","name","display_name","room_w","room_h","mist"]);
for(const k of Object.keys(src)){
if(!keep.has(k)) r._extras[k] = deepClone(src[k]);
}
@@ -533,6 +541,7 @@
// some editor fields for convenience
if(!r.room_w) r.room_w = src.room_w || 1600;
if(!r.room_h) r.room_h = src.room_h || 900;
r.mist = !!(src && src.mist);
return r;
}
@@ -573,6 +582,7 @@
newRoomBtn: document.getElementById("newRoomBtn"),
downloadActiveBtn: document.getElementById("downloadActiveBtn"),
downloadCheckedBtn: document.getElementById("downloadCheckedBtn"),
downloadAllBtn: document.getElementById("downloadAllBtn"),
activeRoomLabel: document.getElementById("activeRoomLabel"),
status: document.getElementById("status"),
@@ -2121,7 +2131,15 @@ Cancel = Choose whether to remove without saving`);
const roomWrap = sel ? state.rooms.get(sel.roomId) : getActiveRoom();
if(!roomWrap) return "<div class='muted'>No room loaded.</div>";
const room = roomWrap.data;
if(!sel) return "<div class='muted'>Select something to edit properties.</div>";
if(!sel){
return `
<div><b>Room Settings</b></div>
<div class="hr"></div>
<div style="margin-top:8px;">
<label><input id="roommist" type="checkbox" ${room.mist?"checked":""}/> Mist (purple haze - shade form costs no charge)</label>
</div>
`;
}
if(sel.kind==="platform"){
const p=room.platforms[sel.index];
@@ -2244,6 +2262,11 @@ Cancel = Choose whether to remove without saving`);
if(sel.kind==="enemy"){
const e=room.enemies[sel.index];
if(!e) return "<div class='muted'>Selection missing.</div>";
const eType = e.type || "enemy";
const eDef = state.enemyTypes && state.enemyTypes[eType] ? state.enemyTypes[eType] : null;
const eProf = (eDef && eDef.drop_profile) ? eDef.drop_profile : "normal";
const beh = e.behavior || ((eDef && eDef.behavior) ? eDef.behavior : "chaser");
const bossBehavior = (eProf === "boss");
return `
<div><b>Enemy Spawn</b> #${sel.index}</div>
<div class="hr"></div>
@@ -2255,6 +2278,16 @@ Cancel = Choose whether to remove without saving`);
<div class="mini">type</div>
<input id="ent" type="text" value="${e.type||"enemy"}"/>
</div>
${bossBehavior ? `
<div style="margin-top:8px;">
<div class="mini">boss AI</div>
<select id="enbeh">
<option value="" ${beh===""?"selected":""}>default (from enemy type)</option>
<option value="shockwave" ${beh==="shockwave"?"selected":""}>shockwave (slam + shockwave)</option>
<option value="charger" ${beh==="charger"?"selected":""}>charger (retreat-charge + dash)</option>
</select>
</div>
` : ""}
<div style="margin-top:8px;">
<div class="mini">layer (lower = back)</div>
<input id="enlayer" type="number" value="${e.layer||0}"/>
@@ -2371,14 +2404,18 @@ Cancel = Choose whether to remove without saving`);
function num(v){ const n = Number(v); return Number.isFinite(n) ? n : 0; }
if(!sel) return;
const pushAnd = (fn)=>{
pushHistory(roomWrap.id);
fn();
refreshUI();
};
if(!sel){
const rm = q("roommist");
if(rm) rm.addEventListener("change", ()=>pushAnd(()=>{ room.mist = rm.checked; }));
return;
}
if(sel.kind==="platform"){
const p = room.platforms[sel.index]; if(!p) return;
["px","py","pw","ph"].forEach(id=>{
@@ -2511,11 +2548,15 @@ Cancel = Choose whether to remove without saving`);
}));
} else if(sel.kind==="enemy"){
const e = room.enemies[sel.index]; if(!e) return;
const enx=q("enx"), eny=q("eny"), ent=q("ent"), enlayer=q("enlayer");
const enx=q("enx"), eny=q("eny"), ent=q("ent"), enlayer=q("enlayer"), enbeh=q("enbeh");
if(enx) enx.addEventListener("change", ()=>pushAnd(()=>{ e.x=num(enx.value); }));
if(eny) eny.addEventListener("change", ()=>pushAnd(()=>{ e.y=num(eny.value); }));
if(ent) ent.addEventListener("change", ()=>pushAnd(()=>{ e.type=ent.value.trim()||"enemy"; }));
if(enlayer) enlayer.addEventListener("change", ()=>pushAnd(()=>{ e.layer = Math.round(num(enlayer.value)); }));
if(enbeh) enbeh.addEventListener("change", ()=>pushAnd(()=>{
const v = enbeh.value;
if(v) e.behavior = v; else delete e.behavior;
}));
const del=q("delSel"); if(del) del.addEventListener("click", ()=>pushAnd(()=>{
room.enemies.splice(sel.index,1);
state.selection=null;
@@ -3338,6 +3379,7 @@ Cancel = Choose whether to remove without saving`);
const enemies = (d.enemies || []).map(e => {
const out = { type: e.type || "enemy", pos: [e.x, e.y] };
if(e.layer) out.layer = e.layer;
if(e.behavior) out.behavior = e.behavior;
return out;
});
@@ -3400,6 +3442,7 @@ Cancel = Choose whether to remove without saving`);
// include entry spawns (new feature) for your spawn linking
if(d.entry_spawns && typeof d.entry_spawns === "object") out.entry_spawns = d.entry_spawns;
if(props.length) out.props = props;
if(d.mist) out.mist = true;
// carry over any extra keys that were in the original
if(d._extras && typeof d._extras === "object"){
@@ -3757,6 +3800,20 @@ els.compareRoomSelect.addEventListener("change", ()=>{
setStatus(`Downloaded ${i} checked room(s).`);
});
els.downloadAllBtn.addEventListener("click", async ()=>{
const rooms = [...state.rooms.values()];
if(!rooms.length){ setStatus("No rooms loaded."); return; }
let i=0;
for(const r of rooms){
downloadText(r.filename || (r.data.name + ".json"), serializeRoom(r));
r.dirty = false;
i++;
await new Promise(res=>setTimeout(res, 250));
}
refreshRoomsList();
setStatus(`Downloaded ${i} room(s).`);
});
// canvas pointer handlers
// middle-mouse pan (and prevent browser autoscroll / sidebar scrolling)
function startMousePan(e, canvasKey){