room overhaul
more editor changes, new rooms
This commit is contained in:
@@ -9,9 +9,9 @@
|
||||
--bg:#0b0f14; --panel:#111827; --panel2:#0f172a; --fg:#e5e7eb; --muted:#9ca3af;
|
||||
--accent:#60a5fa; --accent2:#34d399; --warn:#f59e0b; --danger:#ef4444; --border:#1f2937;
|
||||
}
|
||||
html,body{height:100%; margin:0; background:var(--bg); color:var(--fg); font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;}
|
||||
.wrap{display:grid; grid-template-columns: 360px 1fr; height:100vh; min-height:100vh;}
|
||||
.left{background:var(--panel); border-right:1px solid var(--border); padding:12px; overflow:auto;}
|
||||
html,body{height:100%; margin:0; overflow:hidden; background:var(--bg); color:var(--fg); font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial;}
|
||||
.wrap{display:grid; grid-template-columns: 360px 1fr; height:100vh; min-height:0; overflow:hidden;}
|
||||
.left{background:var(--panel); border-right:1px solid var(--border); padding:12px; overflow-y:auto; height:100vh; box-sizing:border-box;}
|
||||
.section{background:rgba(255,255,255,0.03); border:1px solid var(--border); border-radius:12px; padding:10px; margin-bottom:12px;}
|
||||
.section h3{margin:0 0 8px 0; font-size:14px; color:var(--fg); display:flex; align-items:center; gap:8px;}
|
||||
.row{display:flex; gap:8px; flex-wrap:wrap; align-items:center;}
|
||||
@@ -32,7 +32,7 @@
|
||||
.badge{font-size:11px; padding:2px 6px; border-radius:999px; border:1px solid var(--border); color:var(--muted);}
|
||||
.badge.dirty{border-color:rgba(245,158,11,0.6); color:#fbbf24;}
|
||||
.kbd{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:11px; padding:2px 6px; border:1px solid var(--border); border-radius:6px; color:var(--muted);}
|
||||
.right{position:relative; display:flex; flex-direction:column; min-width:0; height:100%;}
|
||||
.right{position:relative; display:flex; flex-direction:column; min-width:0; height:100%; overflow:hidden;}
|
||||
/* Floating tool palette pinned to the right edge of the canvas area */
|
||||
#toolbar{
|
||||
position:absolute; right:10px; top:70px; z-index:60;
|
||||
@@ -105,6 +105,16 @@
|
||||
Grid snap (20px) on placement / drag
|
||||
</label>
|
||||
</div>
|
||||
<div class="row" style="margin-top:8px;">
|
||||
<label class="muted" style="flex:1;">New platform type:</label>
|
||||
<select id="platformGroup" style="flex:1; min-width:120px;">
|
||||
<option value="ground">Ground</option>
|
||||
<option value="floating">Floating</option>
|
||||
<option value="hazard">Hazard (red - damage)</option>
|
||||
<option value="moving">Moving (path)</option>
|
||||
<option value="trigger">Trigger (name)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="muted" style="margin-top:8px;">
|
||||
<b>Layers:</b> every object (platforms, houses, NPCs, enemies, props, doors, exits) has a <b>layer</b> in its Properties — lower = further back, higher = further forward. The canvas and game draw back-to-front by (layer, category), and the select tool respects layers.
|
||||
</div>
|
||||
@@ -258,6 +268,7 @@
|
||||
house: { type: "house" },
|
||||
diary: { type: "diary" },
|
||||
player: { type: "player" },
|
||||
quest_point: { type: "quest_giver" },
|
||||
};
|
||||
// Type fallbacks used when no object def provides a radius.
|
||||
const TYPE_DEFAULT_RADII = { light_source: 180, save_point: 90 };
|
||||
@@ -328,7 +339,22 @@
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeRoom(json, fallbackName, filename){
|
||||
function normPlatformEntry(rect, group){
|
||||
if(Array.isArray(rect)){
|
||||
return { x:+rect[0]||0, y:+rect[1]||0, w:+rect[2]||0, h:+rect[3]||0, group, layer: rect.length>=5 ? (+rect[4]||0) : 0 };
|
||||
}
|
||||
if(rect && typeof rect === "object"){
|
||||
const out = { x:+rect.x||0, y:+rect.y||0, w:+rect.w||0, h:+rect.h||0, group: rect.group || group || "ground", layer: +(rect.layer||0) };
|
||||
if(Array.isArray(rect.path)) out.path = deepClone(rect.path);
|
||||
if(rect.speed !== undefined) out.speed = +rect.speed;
|
||||
if(rect.name) out.name = String(rect.name);
|
||||
if(Array.isArray(rect.respawn)) out.respawn = deepClone(rect.respawn);
|
||||
return out;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeRoom(json, fallbackName, filename){
|
||||
// Supports both:
|
||||
// A) editor-native rooms (name, platforms:[{x,y,w,h}], exits:[{x,y,w,h}...])
|
||||
// B) your game rooms (meta, platforms:{ground:[[x,y,w,h],...], ...}, exits:[{rect:[...]}], enemies:[{pos:[x,y]}])
|
||||
@@ -367,27 +393,23 @@
|
||||
const arr = platObj[group];
|
||||
if(!Array.isArray(arr)) continue;
|
||||
for(const rect of arr){
|
||||
if(!Array.isArray(rect) || rect.length < 4) continue;
|
||||
r.platforms.push({ x:+rect[0]||0, y:+rect[1]||0, w:+rect[2]||0, h:+rect[3]||0, group, layer: rect.length>=5 ? (+rect[4]||0) : 0 });
|
||||
const np = normPlatformEntry(rect, group);
|
||||
if(np) r.platforms.push(np);
|
||||
}
|
||||
}
|
||||
// if nothing came through, try also accepting editor-native arrays inside game rooms
|
||||
if(Array.isArray(src.platforms)){
|
||||
for(const p of src.platforms){
|
||||
if(p && typeof p === "object"){
|
||||
r.platforms.push({ x:+p.x||0, y:+p.y||0, w:+p.w||0, h:+p.h||0, group: p.group || "ground", layer: +(p.layer||0) });
|
||||
}
|
||||
const np = normPlatformEntry(p, p && p.group ? p.group : "ground");
|
||||
if(np) r.platforms.push(np);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// editor-native
|
||||
if(Array.isArray(src.platforms)){
|
||||
for(const p of src.platforms){
|
||||
if(p && typeof p === "object"){
|
||||
r.platforms.push({ x:+p.x||0, y:+p.y||0, w:+p.w||0, h:+p.h||0, group: p.group || "ground", layer: +(p.layer||0) });
|
||||
} else if(Array.isArray(p) && p.length>=4){
|
||||
r.platforms.push({ x:+p[0]||0, y:+p[1]||0, w:+p[2]||0, h:+p[3]||0, group:"ground", layer: p.length>=5 ? (+p[4]||0) : 0 });
|
||||
}
|
||||
const np = normPlatformEntry(p, p && p.group ? p.group : "ground");
|
||||
if(np) r.platforms.push(np);
|
||||
}
|
||||
} else if(src.platforms && typeof src.platforms === "object"){
|
||||
// allow grouped platforms even in editor rooms
|
||||
@@ -395,8 +417,8 @@
|
||||
const arr = src.platforms[group];
|
||||
if(!Array.isArray(arr)) continue;
|
||||
for(const rect of arr){
|
||||
if(!Array.isArray(rect) || rect.length < 4) continue;
|
||||
r.platforms.push({ x:+rect[0]||0, y:+rect[1]||0, w:+rect[2]||0, h:+rect[3]||0, group, layer: rect.length>=5 ? (+rect[4]||0) : 0 });
|
||||
const np = normPlatformEntry(rect, group);
|
||||
if(np) r.platforms.push(np);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -444,7 +466,7 @@
|
||||
const type = p.type || p.kind || "light_source";
|
||||
const object_id = p.object_id || p.objectId || null;
|
||||
const radius = +p.radius || getDefaultRadiusForType(type, object_id);
|
||||
r.props.push({ x, y, type, radius, object_id, layer: +(p.layer||0) });
|
||||
r.props.push({ x, y, type, radius, object_id, layer: +(p.layer||0), quest_id: p.quest_id || p.questId || "" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -535,7 +557,8 @@
|
||||
enemyPlaceType: "enemy",
|
||||
npcPlaceType: "vendor",
|
||||
showAggro: false,
|
||||
objectPlaceId: null
|
||||
objectPlaceId: null,
|
||||
waitingPathPt: null
|
||||
};
|
||||
|
||||
const els = {
|
||||
@@ -957,6 +980,24 @@ Cancel = Choose whether to remove without saving`);
|
||||
|
||||
const room = roomWrap.data;
|
||||
|
||||
// Adding a path point to a selected moving platform?
|
||||
if(state.waitingPathPt){
|
||||
if(ev.button === 0){
|
||||
ev.preventDefault();
|
||||
const wp = state.waitingPathPt;
|
||||
const wrap = state.rooms.get(wp.roomId);
|
||||
const plat = wrap && wrap.data.platforms && wrap.data.platforms[wp.index];
|
||||
if(plat){
|
||||
if(!Array.isArray(plat.path)) plat.path = [[plat.x, plat.y]];
|
||||
plat.path.push(snapPt(wx, wy));
|
||||
}
|
||||
state.waitingPathPt = null;
|
||||
refreshUI();
|
||||
setStatus("Path point added.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(ev.button === 1){
|
||||
// middle mouse pan handled by mousedown
|
||||
return;
|
||||
@@ -1075,7 +1116,8 @@ Cancel = Choose whether to remove without saving`);
|
||||
const [sx1,sy1]=snapPt(wx,wy);
|
||||
|
||||
// Start as a 1x1 rect at the anchor; resizing can go in any direction.
|
||||
const p = {x: sx1, y: sy1, w: 1, h: 1};
|
||||
const p = {x: sx1, y: sy1, w: 1, h: 1, group: state.platformGroup || "ground"};
|
||||
if(p.group === "moving") p.path = [[sx1, sy1]];
|
||||
room.platforms.push(p);
|
||||
|
||||
state.selection = {kind:"platform", index: room.platforms.length-1, roomId: roomWrap.id};
|
||||
@@ -1491,7 +1533,12 @@ Cancel = Choose whether to remove without saving`);
|
||||
};
|
||||
const layerable = [];
|
||||
for(const p of (room.props || [])) if(p) layerable.push({ z: +(p.layer||0), cat: 0, kind: "prop", p });
|
||||
for(const p of (room.platforms || [])) layerable.push({ z: +(p.layer||0), cat: 1, kind: "platform", p });
|
||||
for(const p of (room.platforms || [])){
|
||||
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 === "hazard")) layerable.push({ z: +(p.layer||0), cat: 1, kind: "hazard", 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 ex of (room.exits || [])) if(ex) layerable.push({ z: +(ex.layer||0), cat: 3, kind: "exit", ex });
|
||||
for(const e of (room.enemies || [])) if(e) layerable.push({ z: +(e.layer||0), cat: 4, kind: "enemy", e });
|
||||
@@ -1509,6 +1556,89 @@ Cancel = Choose whether to remove without saving`);
|
||||
ctx.strokeStyle = "rgba(96,165,250,0.75)";
|
||||
ctx.lineWidth = 2*dpr;
|
||||
ctx.strokeRect(sx,sy, sx2-sx, sy2-sy);
|
||||
} else if(it.kind === "moving"){
|
||||
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(80,180,220,0.30)";
|
||||
ctx.fillRect(sx,sy, sx2-sx, sy2-sy);
|
||||
ctx.strokeStyle = "rgba(150,220,255,0.9)";
|
||||
ctx.lineWidth = 2*dpr;
|
||||
ctx.strokeRect(sx,sy, sx2-sx, sy2-sy);
|
||||
// draw the movement path (dashed line + waypoint markers)
|
||||
const path = p.path || [];
|
||||
if(path.length >= 2){
|
||||
ctx.strokeStyle = "rgba(150,220,255,0.5)";
|
||||
ctx.lineWidth = 1.5*dpr;
|
||||
ctx.setLineDash([4*dpr, 4*dpr]);
|
||||
ctx.beginPath();
|
||||
path.forEach((pt, pi)=>{
|
||||
const [xx,yy] = worldToScreen(pt[0], pt[1], canvas);
|
||||
if(pi===0) ctx.moveTo(xx,yy); else ctx.lineTo(xx,yy);
|
||||
});
|
||||
// loop back to start
|
||||
const [exx,eyy] = worldToScreen(path[0][0], path[0][1], canvas);
|
||||
ctx.lineTo(exx, eyy);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
for(const pt of path){
|
||||
const [xx,yy] = worldToScreen(pt[0], pt[1], canvas);
|
||||
ctx.fillStyle = "rgba(150,220,255,0.9)";
|
||||
ctx.beginPath(); ctx.arc(xx, yy, 3*dpr, 0, Math.PI*2); ctx.fill();
|
||||
}
|
||||
// ghost outline of the platform at its final waypoint
|
||||
const lastPt = path[path.length-1];
|
||||
if(lastPt){
|
||||
const [gx,gy] = worldToScreen(lastPt[0], lastPt[1], canvas);
|
||||
const gw = sx2 - sx, gh = sy2 - sy;
|
||||
ctx.strokeStyle = "rgba(150,220,255,0.45)";
|
||||
ctx.lineWidth = 2*dpr;
|
||||
ctx.setLineDash([5*dpr, 4*dpr]);
|
||||
ctx.strokeRect(gx, gy, gw, gh);
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
}
|
||||
} else if(it.kind === "trigger"){
|
||||
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(220,190,60,0.30)";
|
||||
ctx.fillRect(sx,sy, sx2-sx, sy2-sy);
|
||||
ctx.strokeStyle = "rgba(255,230,120,0.9)";
|
||||
ctx.lineWidth = 2*dpr;
|
||||
ctx.strokeRect(sx,sy, sx2-sx, sy2-sy);
|
||||
const label = p.name ? ("Trigger: " + p.name) : "Trigger";
|
||||
ctx.fillStyle = "rgba(229,231,235,0.9)";
|
||||
ctx.font = `${11*dpr}px system-ui`;
|
||||
ctx.fillText(label, sx+4*dpr, sy-4*dpr);
|
||||
} else if(it.kind === "hazard"){
|
||||
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(239,68,68,0.35)";
|
||||
ctx.fillRect(sx,sy, sx2-sx, sy2-sy);
|
||||
ctx.strokeStyle = "rgba(239,68,68,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("Hazard", sx+4*dpr, sy-4*dpr);
|
||||
// linked respawn marker
|
||||
if(Array.isArray(p.respawn) && p.respawn.length>=2){
|
||||
const [rx,ry] = worldToScreen(p.respawn[0], p.respawn[1], canvas);
|
||||
ctx.strokeStyle = "rgba(52,211,153,0.9)";
|
||||
ctx.lineWidth = 2*dpr;
|
||||
ctx.beginPath(); ctx.arc(rx, ry, 6*dpr, 0, Math.PI*2); ctx.stroke();
|
||||
ctx.fillStyle = "rgba(52,211,153,0.9)";
|
||||
ctx.beginPath(); ctx.arc(rx, ry, 3*dpr, 0, Math.PI*2); ctx.fill();
|
||||
ctx.strokeStyle = "rgba(52,211,153,0.5)";
|
||||
ctx.setLineDash([3*dpr,3*dpr]);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo((sx+sx2)/2, (sy+sy2)/2);
|
||||
ctx.lineTo(rx, ry);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
} else if(it.kind === "door"){
|
||||
const d = it.d;
|
||||
const [sx,sy] = worldToScreen(d.x, d.y, canvas);
|
||||
@@ -1807,26 +1937,17 @@ Cancel = Choose whether to remove without saving`);
|
||||
const ex = exits[i];
|
||||
if(!ex || ex.to !== dstWrap.data.name) continue;
|
||||
|
||||
// End the line at the corresponding DOOR in the compare room (the exit that
|
||||
// leads back to this room), so it lands accurately on the doorway.
|
||||
// End the line at the ENTRY point in the compare room (spawn / entry spawn),
|
||||
// i.e. exit -> entry, not exit -> exit.
|
||||
const sid = ex.spawn_id || null;
|
||||
let pos = null;
|
||||
for(const dex of (dstWrap.data.exits || [])){
|
||||
if(dex && dex.to === srcWrap.data.name){
|
||||
pos = [dex.x + dex.w/2, dex.y + dex.h/2];
|
||||
break;
|
||||
}
|
||||
if(sid === "__player_spawn__"){
|
||||
pos = dstWrap.data.player_spawn || null;
|
||||
} else if(sid){
|
||||
pos = (dstWrap.data.entry_spawns && dstWrap.data.entry_spawns[sid]) ? dstWrap.data.entry_spawns[sid] : null;
|
||||
}
|
||||
// Fallback: the exit's spawn / entry spawn in the compare room.
|
||||
if(!pos){
|
||||
const sid = ex.spawn_id || null;
|
||||
if(sid === "__player_spawn__"){
|
||||
pos = dstWrap.data.player_spawn || null;
|
||||
} else if(sid){
|
||||
pos = (dstWrap.data.entry_spawns && dstWrap.data.entry_spawns[sid]) ? dstWrap.data.entry_spawns[sid] : null;
|
||||
}
|
||||
if(!pos && Array.isArray(ex.spawn) && ex.spawn.length >= 2){
|
||||
pos = ex.spawn;
|
||||
}
|
||||
if(!pos && Array.isArray(ex.spawn) && ex.spawn.length >= 2){
|
||||
pos = ex.spawn;
|
||||
}
|
||||
if(!pos) continue;
|
||||
|
||||
@@ -1998,6 +2119,39 @@ Cancel = Choose whether to remove without saving`);
|
||||
<div><div class="mini">h</div><input id="ph" type="number" value="${p.h}"/></div>
|
||||
<div><div class="mini">layer (lower = back)</div><input id="pl" type="number" value="${p.layer||0}"/></div>
|
||||
</div>
|
||||
<div style="margin-top:8px;">
|
||||
<div class="mini">type</div>
|
||||
<select id="pg">
|
||||
<option value="ground" ${p.group==="ground"||!p.group?"selected":""}>Ground</option>
|
||||
<option value="floating" ${p.group==="floating"?"selected":""}>Floating</option>
|
||||
<option value="hazard" ${p.group==="hazard"?"selected":""}>Hazard (red - damage)</option>
|
||||
<option value="moving" ${p.group==="moving"?"selected":""}>Moving (path)</option>
|
||||
<option value="trigger" ${p.group==="trigger"?"selected":""}>Trigger (name)</option>
|
||||
</select>
|
||||
</div>
|
||||
${(p.group||"ground") === "trigger" ? `
|
||||
<div style="margin-top:8px;">
|
||||
<div class="mini">name</div>
|
||||
<input id="ptname" type="text" value="${p.name||""}" placeholder="e.g. opens_bridge"/>
|
||||
</div>` : ""}
|
||||
${(p.group||"ground") === "hazard" ? `
|
||||
<div style="margin-top:8px;">
|
||||
<div class="mini">respawn point (hazard teleports here)</div>
|
||||
<select id="hzrespawn">
|
||||
<option value="">(none - room default)</option>
|
||||
${Object.keys(room.entry_spawns||{}).map(k=>`<option value="${k}">Entry: ${k}</option>`).join("")}
|
||||
</select>
|
||||
</div>` : ""}
|
||||
${(p.group||"ground") === "moving" ? `
|
||||
<div style="margin-top:8px;">
|
||||
<div class="mini">path (waypoints - click Add, then the canvas)</div>
|
||||
<button id="addPathPt" class="primary" style="width:100%;">Add path point</button>
|
||||
<div id="pathList" style="margin-top:6px; max-height:120px; overflow:auto;"></div>
|
||||
</div>
|
||||
<div style="margin-top:8px;">
|
||||
<div class="mini">speed (px/s)</div>
|
||||
<input id="ptspeed" type="number" value="${p.speed||120}"/>
|
||||
</div>` : ""}
|
||||
<div class="row" style="margin-top:8px;">
|
||||
<button id="delSel" class="danger">Delete</button>
|
||||
</div>
|
||||
@@ -2079,9 +2233,10 @@ Cancel = Choose whether to remove without saving`);
|
||||
<div><div class="mini">y</div><input id="npy" type="number" value="${n.y}"/></div>
|
||||
</div>
|
||||
<div style="margin-top:8px;">
|
||||
<div class="mini">type</div>
|
||||
<div class="mini">type / name</div>
|
||||
<input id="npt" type="text" value="${n.type||"vendor"}"/>
|
||||
</div>
|
||||
<div class="muted" style="margin-top:6px;">Any name works (e.g. quest_giver_gwen). vendor / upgrader / necklace_vendor / combat_master have game behavior; custom names are inert until coded.</div>
|
||||
<div style="margin-top:8px;">
|
||||
<div class="mini">layer (lower = back)</div>
|
||||
<input id="npl" type="number" value="${n.layer||0}"/>
|
||||
@@ -2096,7 +2251,8 @@ Cancel = Choose whether to remove without saving`);
|
||||
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 - press F to save)", butterfly_jar:"Butterfly-in-jar (collectible)", diary:"Boss diary (interact)", house:"Standalone house", player:"Player size reference (no in-game effect)"})[p.type||"light_source"] || "";
|
||||
const hint = ({light_source:"Butterfly (charge regen 2x)", save_point:"Lantern (save point - press F to save)", butterfly_jar:"Butterfly-in-jar (collectible)", diary:"Boss diary (interact)", house:"Standalone house", player:"Player size reference (no in-game effect)", quest_giver:"Quest point (name it below)"})[p.type||"light_source"] || "";
|
||||
const isQuest = (p.type||"") === "quest_giver";
|
||||
return `
|
||||
<div><b>Prop</b> #${sel.index}${hint ? ` <span class="mini">· ${hint}</span>` : ""}</div>
|
||||
<div class="hr"></div>
|
||||
@@ -2108,6 +2264,11 @@ Cancel = Choose whether to remove without saving`);
|
||||
<div class="mini">type</div>
|
||||
<input id="prtype" type="text" value="${p.type||"light_source"}"/>
|
||||
</div>
|
||||
${isQuest ? `
|
||||
<div style="margin-top:8px;">
|
||||
<div class="mini">quest id (name)</div>
|
||||
<input id="prqid" type="text" value="${p.quest_id||""}" placeholder="e.g. quest_save_the_well"/>
|
||||
</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)"/>
|
||||
@@ -2188,6 +2349,67 @@ Cancel = Choose whether to remove without saving`);
|
||||
}));
|
||||
});
|
||||
const pl=q("pl"); if(pl) pl.addEventListener("change", ()=>pushAnd(()=>{ p.layer = Math.round(num(pl.value)); }));
|
||||
const pg=q("pg"); if(pg) pg.addEventListener("change", ()=>pushAnd(()=>{
|
||||
p.group = pg.value || "ground";
|
||||
if(p.group === "moving" && !Array.isArray(p.path)) p.path = [[p.x, p.y]];
|
||||
if(p.group !== "trigger" && p.name !== undefined && p.name !== "") delete p.name;
|
||||
}));
|
||||
const ptname=q("ptname"); if(ptname) ptname.addEventListener("change", ()=>pushAnd(()=>{
|
||||
const v = ptname.value.trim();
|
||||
if(v){ p.name = v; } else { delete p.name; }
|
||||
}));
|
||||
const ptspeed=q("ptspeed"); if(ptspeed) ptspeed.addEventListener("change", ()=>pushAnd(()=>{ p.speed = Math.max(1, num(ptspeed.value)); }));
|
||||
const hzrespawn=q("hzrespawn");
|
||||
if(hzrespawn){
|
||||
// preselect current respawn if it matches an entry spawn
|
||||
let cur = "";
|
||||
if(Array.isArray(p.respawn)){
|
||||
const es = room.entry_spawns || {};
|
||||
for(const k of Object.keys(es)){
|
||||
const pos = es[k];
|
||||
if(Array.isArray(pos) && pos.length>=2 && Math.round(pos[0])===Math.round(p.respawn[0]) && Math.round(pos[1])===Math.round(p.respawn[1])){
|
||||
cur = k; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
hzrespawn.value = cur;
|
||||
hzrespawn.addEventListener("change", ()=>pushAnd(()=>{
|
||||
const v = hzrespawn.value;
|
||||
if(v === ""){ delete p.respawn; }
|
||||
else {
|
||||
const pos = (room.entry_spawns||{})[v];
|
||||
if(Array.isArray(pos) && pos.length>=2) p.respawn = [Math.round(pos[0]), Math.round(pos[1])];
|
||||
}
|
||||
}));
|
||||
}
|
||||
const addPathPt=q("addPathPt"); if(addPathPt) addPathPt.addEventListener("click", ()=>{
|
||||
state.waitingPathPt = { roomId: roomWrap.id, index: sel.index };
|
||||
setStatus("Click on the canvas to add a path point to this moving platform.");
|
||||
refreshProps();
|
||||
});
|
||||
const pathList=q("pathList");
|
||||
if(pathList && Array.isArray(p.path)){
|
||||
pathList.innerHTML = "";
|
||||
p.path.forEach((pt, pi)=>{
|
||||
const row = document.createElement("div");
|
||||
row.style.cssText = "display:flex; align-items:center; gap:6px; margin-bottom:4px;";
|
||||
row.innerHTML = `<span style="flex:1; font-size:12px;">${pi+1}. (${Math.round(pt[0])}, ${Math.round(pt[1])})</span>`;
|
||||
const delBtn = document.createElement("button");
|
||||
delBtn.textContent = "x";
|
||||
delBtn.style.cssText = "padding:2px 8px;";
|
||||
delBtn.addEventListener("click", ()=>pushAnd(()=>{
|
||||
p.path.splice(pi,1);
|
||||
}));
|
||||
row.appendChild(delBtn);
|
||||
pathList.appendChild(row);
|
||||
});
|
||||
if(state.waitingPathPt && state.waitingPathPt.index === sel.index){
|
||||
const hint = document.createElement("div");
|
||||
hint.className = "muted";
|
||||
hint.textContent = "Click the canvas to add point " + (p.path.length + 1);
|
||||
pathList.appendChild(hint);
|
||||
}
|
||||
}
|
||||
const del=q("delSel"); if(del) del.addEventListener("click", ()=>pushAnd(()=>{
|
||||
room.platforms.splice(sel.index,1);
|
||||
state.selection=null;
|
||||
@@ -2250,10 +2472,14 @@ 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"), probjid=q("probjid"), prrad=q("prrad"), prlayer=q("prlayer");
|
||||
const prx=q("prx"), pry=q("pry"), prtype=q("prtype"), probjid=q("probjid"), prrad=q("prrad"), prlayer=q("prlayer"), prqid=q("prqid");
|
||||
if(prx) prx.addEventListener("change", ()=>pushAnd(()=>{ p.x=num(prx.value); }));
|
||||
if(pry) pry.addEventListener("change", ()=>pushAnd(()=>{ p.y=num(pry.value); }));
|
||||
if(prlayer) prlayer.addEventListener("change", ()=>pushAnd(()=>{ p.layer = Math.round(num(prlayer.value)); }));
|
||||
if(prqid) prqid.addEventListener("change", ()=>pushAnd(()=>{
|
||||
const v = prqid.value.trim();
|
||||
if(v){ p.quest_id = v; } else { delete p.quest_id; }
|
||||
}));
|
||||
if(probjid) probjid.addEventListener("change", ()=>pushAnd(()=>{
|
||||
const v = probjid.value.trim();
|
||||
if(v){ p.object_id = v; } else { delete p.object_id; }
|
||||
@@ -2309,6 +2535,24 @@ Cancel = Choose whether to remove without saving`);
|
||||
|
||||
const room = roomWrap.data;
|
||||
|
||||
// Adding a path point to a selected moving platform?
|
||||
if(state.waitingPathPt){
|
||||
if(ev.button === 0){
|
||||
ev.preventDefault();
|
||||
const wp = state.waitingPathPt;
|
||||
const wrap = state.rooms.get(wp.roomId);
|
||||
const plat = wrap && wrap.data.platforms && wrap.data.platforms[wp.index];
|
||||
if(plat){
|
||||
if(!Array.isArray(plat.path)) plat.path = [[plat.x, plat.y]];
|
||||
plat.path.push(snapPt(wx, wy));
|
||||
}
|
||||
state.waitingPathPt = null;
|
||||
refreshUI();
|
||||
setStatus("Path point added.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(ev.button === 1){
|
||||
// middle mouse pan is handled by the mousedown listener (better cross-browser)
|
||||
return;
|
||||
@@ -2390,7 +2634,8 @@ Cancel = Choose whether to remove without saving`);
|
||||
const [sx1,sy1]=snapPt(wx,wy);
|
||||
|
||||
// Start as a 1x1 rect at the anchor; resizing can go in any direction.
|
||||
const p = {x: sx1, y: sy1, w: 1, h: 1};
|
||||
const p = {x: sx1, y: sy1, w: 1, h: 1, group: state.platformGroup || "ground"};
|
||||
if(p.group === "moving") p.path = [[sx1, sy1]];
|
||||
room.platforms.push(p);
|
||||
|
||||
state.selection = {kind:"platform", index: room.platforms.length-1, roomId: roomWrap.id};
|
||||
@@ -3000,14 +3245,27 @@ Cancel = Choose whether to remove without saving`);
|
||||
const fmt = d._format || "editor_v1";
|
||||
|
||||
if(fmt === "game_v1"){
|
||||
// rebuild grouped platforms (layer appended as 5th element when non-zero)
|
||||
// rebuild grouped platforms (layer appended as 5th element when non-zero;
|
||||
// moving/trigger platforms serialize as objects to keep their path/name/speed)
|
||||
const platforms = {};
|
||||
for(const p of (d.platforms || [])){
|
||||
const group = p.group || "ground";
|
||||
if(!platforms[group]) platforms[group] = [];
|
||||
const rect = [p.x, p.y, p.w, p.h];
|
||||
if(p.layer) rect.push(p.layer);
|
||||
platforms[group].push(rect);
|
||||
if(group === "moving" || group === "trigger" || group === "hazard"){
|
||||
const obj = { x: p.x, y: p.y, w: p.w, h: p.h };
|
||||
if(p.layer) obj.layer = p.layer;
|
||||
if(group === "moving"){
|
||||
if(Array.isArray(p.path)) obj.path = p.path;
|
||||
if(p.speed !== undefined) obj.speed = p.speed;
|
||||
}
|
||||
if(p.name) obj.name = p.name;
|
||||
if(Array.isArray(p.respawn)) obj.respawn = p.respawn;
|
||||
platforms[group].push(obj);
|
||||
} else {
|
||||
const rect = [p.x, p.y, p.w, p.h];
|
||||
if(p.layer) rect.push(p.layer);
|
||||
platforms[group].push(rect);
|
||||
}
|
||||
}
|
||||
|
||||
// enemies back to {type, pos:[x,y]} (+ layer when set)
|
||||
@@ -3052,6 +3310,7 @@ Cancel = Choose whether to remove without saving`);
|
||||
};
|
||||
if(p.object_id) out.object_id = p.object_id;
|
||||
if(p.layer) out.layer = p.layer;
|
||||
if(p.quest_id) out.quest_id = p.quest_id;
|
||||
return out;
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user