Seven hardening fixes on the lane-J editor, several sharing one root cause.
- The demo ran against the LIVE database. Opening demos/lane-j.html deleted
every saved model (pruneOrphans treats anything the demo's manifest omits as
garbage) and an interrupted run left a fake machine.boot override the real
game would load. The database name is now a variable, the demo uses a
throwaway one, deletes it in a finally, and fingerprints the real store
before/after so the run FAILS if it moved.
- A .glb with no mesh was accepted on every non-paintable slot: original hidden,
nothing in its place, cheerful success message, savable. The mesh check now
runs for every slot, before anything is written.
- Multi-object slots (nine puddles, every cannon barrel) hid all instances and
added one replacement. One fit node per instance now, each at its own pose and
its own footprint scale — the same contract the runtime's per-instance
attachSlot already implements.
- Storage failures rejected into void-discarded promises, freezing the UI on
"Reading…". Nothing leaves Workshop as a rejection any more: failures come
back as results carrying a sentence a person can read, and every DOM handler
goes through one guard.
- idbAvailable() was a typeof check, so private windows passed it and then blew
up, replacing the whole built UI with a raw error string. It now probes open()
once and caches the boolean; failure means a labelled read-only editor.
- An override whose GLB failed to reload was dropped from the manifest, after
which the next save deleted its bytes for good. Failed entries are kept,
flagged unloadable, surfaced in the slot list, and never pruned.
- The editor kept its own slot list, which disagreed with the runtime's in both
directions. It now imports SLOT_IDS/SLOT_LABELS from src/assets/slots and
keeps only presentation data, keyed by SlotId so describing a slot the runtime
lacks is a compile error.
Also fixes a cross-lane mismatch found on the way: the editor stored GLBs as a
{name,bytes,savedAt} wrapper while the game's reader feeds the record straight
into new Blob([buf]). Writes are raw ArrayBuffer now (reads stay tolerant of the
old shape), so a saved swap can actually load in the game.
Build passes; 312 assertions across 9 headless test files, 49 of them new.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three-panel editor page (editor.html + src/editor/): slot list, the actual
greybox course built from the game's own builders, and fit controls. Drop a
.glb on a slot, nudge offset/rotation/scale, save to IndexedDB so the live
game picks it up with no deploy, export manifest.json to commit it.
Design notes worth keeping:
- A custom asset never replaces the procedural node; it goes into a new
sibling 'fit' node and the original is hidden. Reset is instant, references
other systems captured stay alive, and the manifest transform lives on a
node nobody else writes to (feel.ts, telegraph.ts and the machine parts all
own transforms of their own).
- The stage creates physics but never steps it: no systems, no world.start(),
just renderOnce() on a rAF. Nothing here affects gameplay.
- A blob body whose UVs paint cannot stick to is refused outright rather than
swapped in — silently broken paint is worse than a missing model.
Headless verification: 148 assertions across three node test files
(manifest round-trip, GLB paintability against real parsed glTF, and the
whole drop/save/restore path over an in-memory IndexedDB).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>