- src/main.ts: await initAssets() before the world is built (createBlob captures
blob.mesh by reference). Safe: preload races each url against a deadline with
allSettled, so a stalled asset is a slow boot on built-ins, never a hang.
- src/game.ts: course.tramp hook (hides the MATERIAL — the custom model is a
child, so hiding the mesh would hide it too). Collider untouched.
- vite.config.ts: editor.html as a build input. Without it the Workshop existed
only under vite dev — invisible in the production deploy it is built for.
- editor cannon.barrel: anchor the preview at the cannon group's origin (where
the runtime attaches it) while still hiding the pivot's tube. Preview was
0.45m above where the game renders it — WYSIWYG off by a measurable amount.
- manifest-io.test: drop the stale NOT_YET_IN_THE_RUNTIME guard now that the
runtime adopted all four ids; the check is strict both ways, which is what
stops the two halves drifting again.
Verified: 10/10 test suites (360 assertions), sacred-parity fingerprint
49df4f20 unchanged from pre-workshop main, dist/editor.html + dist/assets/
manifest.json ship.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>