Add layout-prefixed spawn IDs, safe per-spawn rename, and renumber-all (playlist refs kept in sync)
This commit is contained in:
@@ -100,6 +100,29 @@ let scene = await api('/api/scene');
|
|||||||
scene.anchors ||= []; scene.buildings ||= []; scene.spawns ||= []; scene.paths ||= [];
|
scene.anchors ||= []; scene.buildings ||= []; scene.spawns ||= []; scene.paths ||= [];
|
||||||
scene.table = Object.assign({ width: 120, depth: 100, offsetX: 0, offsetZ: 0, show: true }, scene.table || {});
|
scene.table = Object.assign({ width: 120, depth: 100, offsetX: 0, offsetZ: 0, show: true }, scene.table || {});
|
||||||
|
|
||||||
|
// Active layout slug drives spawn/path ID prefixes (e.g. small-demo -> "sd-sp1").
|
||||||
|
// Fetched here and refreshed whenever a layout is saved or loaded.
|
||||||
|
let activeLayoutSlug = null;
|
||||||
|
try { activeLayoutSlug = (await api('/api/layouts')).activeSlug || null; } catch { activeLayoutSlug = null; }
|
||||||
|
|
||||||
|
// Short prefix from a slug: initials of its words. "small-demo" -> "sd",
|
||||||
|
// "full-build" -> "fb". Null slug (unsaved scene) -> null (use legacy names).
|
||||||
|
function layoutPrefix() {
|
||||||
|
if (!activeLayoutSlug) return null;
|
||||||
|
const initials = activeLayoutSlug.split('-').filter(Boolean).map(w => w[0]).join('');
|
||||||
|
return initials || activeLayoutSlug.slice(0, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next free "<prefix>-sp<n>" (or "spawn-<n>" when no layout is active),
|
||||||
|
// scanning existing spawn IDs so numbers don't collide after deletes.
|
||||||
|
function nextSpawnId() {
|
||||||
|
const p = layoutPrefix();
|
||||||
|
const base = p ? `${p}-sp` : 'spawn-';
|
||||||
|
const used = new Set(scene.spawns.map(s => s.id));
|
||||||
|
let n = 1; while (used.has(base + n)) n++;
|
||||||
|
return base + n;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- three.js setup ----------
|
// ---------- three.js setup ----------
|
||||||
const cvs = document.getElementById('gl');
|
const cvs = document.getElementById('gl');
|
||||||
const renderer = new THREE.WebGLRenderer({ canvas: cvs, antialias: true });
|
const renderer = new THREE.WebGLRenderer({ canvas: cvs, antialias: true });
|
||||||
@@ -307,7 +330,7 @@ document.querySelectorAll('[data-add]').forEach(b => b.onclick = () => {
|
|||||||
points: [[t.x - 25, 25, t.z], [t.x + 25, 25, t.z], [t.x, 25, t.z + 30]] });
|
points: [[t.x - 25, 25, t.z], [t.x + 25, 25, t.z], [t.x, 25, t.z + 30]] });
|
||||||
sel = { kind: 'pathpoint', index: scene.paths.length - 1, sub: 0 };
|
sel = { kind: 'pathpoint', index: scene.paths.length - 1, sub: 0 };
|
||||||
} else {
|
} else {
|
||||||
scene.spawns.push({ id: 'spawn-' + (scene.spawns.length + 1), position: [t.x, 25, t.z], enabled: true });
|
scene.spawns.push({ id: nextSpawnId(), position: [t.x, 25, t.z], enabled: true });
|
||||||
sel = { kind: 'spawn', index: scene.spawns.length - 1 };
|
sel = { kind: 'spawn', index: scene.spawns.length - 1 };
|
||||||
}
|
}
|
||||||
buildAll(); panel();
|
buildAll(); panel();
|
||||||
@@ -353,6 +376,7 @@ async function refreshLayouts() {
|
|||||||
scene = r.scene;
|
scene = r.scene;
|
||||||
scene.anchors ||= []; scene.buildings ||= []; scene.spawns ||= []; scene.paths ||= [];
|
scene.anchors ||= []; scene.buildings ||= []; scene.spawns ||= []; scene.paths ||= [];
|
||||||
scene.table = Object.assign({ width: 120, depth: 100, offsetX: 0, offsetZ: 0, show: true }, scene.table || {});
|
scene.table = Object.assign({ width: 120, depth: 100, offsetX: 0, offsetZ: 0, show: true }, scene.table || {});
|
||||||
|
activeLayoutSlug = (r.layouts && r.layouts.activeSlug) || slug;
|
||||||
sel = null; gizmo.detach(); buildAll(); panel();
|
sel = null; gizmo.detach(); buildAll(); panel();
|
||||||
status.textContent = 'Loaded layout';
|
status.textContent = 'Loaded layout';
|
||||||
refreshLayouts();
|
refreshLayouts();
|
||||||
@@ -374,13 +398,53 @@ document.getElementById('layoutSave').onclick = async () => {
|
|||||||
try {
|
try {
|
||||||
// persist current edits into the live scene, then snapshot as a layout
|
// persist current edits into the live scene, then snapshot as a layout
|
||||||
scene = await api('/api/scene', 'PUT', scene);
|
scene = await api('/api/scene', 'PUT', scene);
|
||||||
await api('/api/layouts', 'POST', { name });
|
const saved = await api('/api/layouts', 'POST', { name });
|
||||||
|
activeLayoutSlug = saved.activeSlug || activeLayoutSlug;
|
||||||
document.getElementById('layoutName').value = '';
|
document.getElementById('layoutName').value = '';
|
||||||
status.textContent = 'Saved layout "' + name + '"';
|
status.textContent = 'Saved layout "' + name + '"';
|
||||||
refreshLayouts();
|
refreshLayouts();
|
||||||
} catch (e) { alert('Save failed: ' + e.message); }
|
} catch (e) { alert('Save failed: ' + e.message); }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ---------- spawn ID rename (keeps playlist references in sync) ----------
|
||||||
|
// Given a { oldId: newId } map, rewrite scene.spawns, then load the playlist
|
||||||
|
// config, remap track.spawnPoint (bare spawn refs only) and resident.spawnId,
|
||||||
|
// and save both. Persists immediately so live viewers stay consistent.
|
||||||
|
async function applySpawnRename(map) {
|
||||||
|
const changed = Object.keys(map).filter(k => map[k] && map[k] !== k);
|
||||||
|
if (!changed.length) return;
|
||||||
|
for (const s of scene.spawns) if (map[s.id]) s.id = map[s.id];
|
||||||
|
scene = await api('/api/scene', 'PUT', scene);
|
||||||
|
try {
|
||||||
|
const cfg = await api('/api/playlist');
|
||||||
|
let touched = false;
|
||||||
|
for (const t of cfg.tracks || []) {
|
||||||
|
// spawnPoint is 'random', a bare spawn id, or 'path:<id>' — only remap bare spawn ids
|
||||||
|
if (t.spawnPoint && !String(t.spawnPoint).startsWith('path:') && map[t.spawnPoint]) {
|
||||||
|
t.spawnPoint = map[t.spawnPoint]; touched = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const r of cfg.residents || []) {
|
||||||
|
if (r.spawnId && map[r.spawnId]) { r.spawnId = map[r.spawnId]; touched = true; }
|
||||||
|
}
|
||||||
|
if (touched) await api('/api/playlist', 'PUT', cfg);
|
||||||
|
} catch (e) { console.warn('playlist remap skipped:', e.message); }
|
||||||
|
buildAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renumber every spawn to the active layout's prefix: sp-prefix-1, -2, … in
|
||||||
|
// current array order. Rewrites playlist refs via applySpawnRename.
|
||||||
|
async function renumberSpawns() {
|
||||||
|
if (!scene.spawns.length) return alert('No spawns to renumber.');
|
||||||
|
const p = layoutPrefix();
|
||||||
|
const base = p ? `${p}-sp` : 'spawn-';
|
||||||
|
const preview = scene.spawns.map((s, i) => `${s.id} → ${base}${i + 1}`).join('\n');
|
||||||
|
if (!confirm(`Renumber ${scene.spawns.length} spawn(s) to "${base}N"?\n\n${preview}\n\nPlaylist references will be updated too.`)) return;
|
||||||
|
const map = {}; scene.spawns.forEach((s, i) => { map[s.id] = `${base}${i + 1}`; });
|
||||||
|
try { await applySpawnRename(map); status.textContent = 'Renumbered spawns'; panel(); }
|
||||||
|
catch (e) { alert('Renumber failed: ' + e.message); }
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- property panel ----------
|
// ---------- property panel ----------
|
||||||
const side = document.getElementById('side');
|
const side = document.getElementById('side');
|
||||||
function field(label, val, oncommit, opts) {
|
function field(label, val, oncommit, opts) {
|
||||||
@@ -463,12 +527,16 @@ function panel(rebuild = true) {
|
|||||||
loop = circles forever · pingpong = there and back. Tap other spheres to edit them;
|
loop = circles forever · pingpong = there and back. Tap other spheres to edit them;
|
||||||
the cone marks walk direction. Delete below removes the whole path.</p>`;
|
the cone marks walk direction. Delete below removes the whole path.</p>`;
|
||||||
} else {
|
} else {
|
||||||
h += textField('id', o.id, v => o.id = v);
|
h += `<div class="row"><label>id</label><input id="spawnId" value="${o.id}"><button id="spawnRename">Rename</button></div>`;
|
||||||
h += field('x (cm)', o.position[0], v => o.position[0] = v);
|
h += field('x (cm)', o.position[0], v => o.position[0] = v);
|
||||||
h += field('y (cm)', o.position[1], v => o.position[1] = v);
|
h += field('y (cm)', o.position[1], v => o.position[1] = v);
|
||||||
h += field('z (cm)', o.position[2], v => o.position[2] = v);
|
h += field('z (cm)', o.position[2], v => o.position[2] = v);
|
||||||
h += field('enabled', o.enabled === false ? 'no' : 'yes', v => o.enabled = v === 'yes', { select: ['yes', 'no'] });
|
h += field('enabled', o.enabled === false ? 'no' : 'yes', v => o.enabled = v === 'yes', { select: ['yes', 'no'] });
|
||||||
h += `<p class="badge">Drag the green (Y) arrow to set spawn height — ghosts wander around this point in 3D.</p>`;
|
h += `<div class="row"><button id="spawnRenumber">Renumber all spawns</button></div>`;
|
||||||
|
h += `<p class="badge">Editing the id is a failsafe — hit Rename to change it safely; any playlist
|
||||||
|
references (track spawn points, pinned residents) are updated to match. Renumber all spawns retags
|
||||||
|
every spawn to this layout's prefix (${layoutPrefix() ? layoutPrefix() + '-sp1, -sp2…' : 'spawn-1, -2…'}).
|
||||||
|
Drag the green (Y) arrow to set spawn height — ghosts wander around this point in 3D.</p>`;
|
||||||
}
|
}
|
||||||
h += `<div class="row" style="margin-top:12px"><button id="del" class="danger">Delete</button></div>`;
|
h += `<div class="row" style="margin-top:12px"><button id="del" class="danger">Delete</button></div>`;
|
||||||
side.innerHTML = h;
|
side.innerHTML = h;
|
||||||
@@ -478,6 +546,21 @@ function panel(rebuild = true) {
|
|||||||
sel = null; gizmo.detach(); buildAll(); panel();
|
sel = null; gizmo.detach(); buildAll(); panel();
|
||||||
};
|
};
|
||||||
wirePathExtras();
|
wirePathExtras();
|
||||||
|
wireSpawnExtras();
|
||||||
|
}
|
||||||
|
|
||||||
|
function wireSpawnExtras() {
|
||||||
|
const renameBtn = document.getElementById('spawnRename');
|
||||||
|
const renumBtn = document.getElementById('spawnRenumber');
|
||||||
|
if (renameBtn) renameBtn.onclick = async () => {
|
||||||
|
const o = dataOf(sel);
|
||||||
|
const newId = document.getElementById('spawnId').value.trim();
|
||||||
|
if (!newId || newId === o.id) return;
|
||||||
|
if (scene.spawns.some(s => s !== o && s.id === newId)) return alert('That id is already used by another spawn.');
|
||||||
|
try { await applySpawnRename({ [o.id]: newId }); status.textContent = 'Renamed spawn'; panel(); }
|
||||||
|
catch (e) { alert('Rename failed: ' + e.message); }
|
||||||
|
};
|
||||||
|
if (renumBtn) renumBtn.onclick = renumberSpawns;
|
||||||
}
|
}
|
||||||
|
|
||||||
function wirePathExtras() {
|
function wirePathExtras() {
|
||||||
|
|||||||
Reference in New Issue
Block a user