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>
Custom GLBs drop into 14 named slots without code changes; an empty manifest
produces today's game by construction (the fallback builders are the original
code moved into a closure, and an empty registry returns the caller's own
object by identity).
- src/assets/{slots,manifest,idb,registry,blobBody}.ts — schema + validation,
GLTF cache with per-instance material cloning, fit nodes, IndexedDB override
layer, paintability report.
- Slot hooks in createBlob, parts, cannon, greybox, puddles, ghost. Mesh-only:
no collider, physics or logic line is touched. Animated sub-parts (plate cap,
belt chevrons, fan blades, cannon pivot) stay procedural so a custom model
cannot stop them moving.
- public/assets/ — the build had NO asset copy step at all, so every asset URL
would have 404'd in production. public/ is vite's default publicDir, so this
needs no vite.config change.
- Tests: 63 headless checks + a farm-GLB audit that fires PaintSkin's own
raycast against the fitted body.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Custom-GLB drop-in system: manifest/registry with procedural fallbacks,
IndexedDB override layer for zero-deploy playtesting, and a three-panel
in-browser fitting editor. Physics/visual decoupling makes every mesh
swappable; blob.body is the one paintable special case (UV requirements).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- installZones + cannonConfigs through the single cannon path; old centre
plate/boot removed (fork versions supersede); belt+bucket to centre lane
(sunk flush — proud face curbed the line); arch punishes failed MEGA runs
- puddles: distance-based stamping (speed-independent coverage), rate 4/u,
contact tolerance 1.0 (bounce hops starved strips), fork strips widened
toward the racing line + neutral-centre red top-up strip
- under-shelf volumes filled solid in the centre (side corridors left open);
launcher catch widened to the full gap
- verified: naive hold-W finishes ~12s, ghost records+replays, MINI (0.77)
passes the tunnel with 47% pink, buffs activate (GRIP live at 29% green),
78% total coverage on a wandering run
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes the live finding that a racer can finish with 0% paint. Paint is now
ROUTE-guaranteed by ground puddles, and colours come in single-colour zones so
buffs actually cross the 20% threshold.
- paint/puddles.ts: PaintPuddle + installPuddles. Flat coloured strips (no
collider) that stamp the blob's underside via skin.splatAtPoint on a ~6/s
cadence while rolling through, emitting paint:splatted. The body doesn't
physically tumble (rotations locked), so an accumulated roll phase walks the
contact point along the travel great circle — a straight run paints a stripe
and coverage climbs instead of piling on the south pole.
- paint/coverage-math.ts: extend computeModifiers with purple MEGA (size→1.45 +
heavier) and pink MINI (size→0.62 + lighter than clean at high %). They fight:
net size uses (purple − pink) strength; mass floored so MINI stays positive.
(Lives here, not buffs.ts, because the pure mapping must stay THREE-free for
the bare-node test; BuffSystem already forwards mods.size/massMul unchanged.)
- paint/coverage-math.test.ts: +MEGA/MINI/fight cases (48 assertions pass).
- course/zones.ts: installZones lays RED/GREEN/BLUE puddles down the centre line
and the PURPLE(left)/PINK(right) fork onto the machine district — a pressure
plate (baseMass*1.25) + boot on the MEGA lane, a visibly-low pink tunnel
(underside y≈1.12) on the MINI lane. Returns cannon configs for integration.
- demo (demos/lane-g.html + src/demo/lane-g.ts): drive the zones, F = hands-free
hold-forward, 1-4 teleport to each zone, live coverage/size/mass + plate HUD.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ghost (src/ghost/):
- format.ts: pure, headless-testable core — int16 (cm position / milli scale)
quantization, base64 localStorage envelope (blobbo:ghost), linear sampling.
- recorder.ts: 15Hz fixed-step recorder, ~5min cap, saves quantized track on a
new best (time <= best, matching game.ts's pre-emit best update).
- player.ts: translucent (~0.35) desaturated ghost + eyes, slight bob, replays
in race time with interpolation; spawns on race:started, despawns on finish.
- install.ts: installGhost(world, blob) wiring both to the frozen race events.
Title (src/ui/title.ts): installTitle(world, {ready}) — CSS bubble-letter
BLOBBO wordmark with paint-drip accents, pitch, controls card, PLAY. Loading
blob-dot while a boot promise is pending. Emits ui:play exactly once inside the
user gesture (audio-unlock friendly), then fades and never returns. Shows saved
best time + "ghost ready".
Demo (demos/lane-h.html + src/demo/lane-h.ts): scripted 20s circle run — title
gates start, run 1 records, later runs replay the ghost on the same path.
Keys: G wipe ghost, R restart, P snapshot.
verify-drift.ts: numeric acceptance (esbuild+node) — max drift 0.0058m vs 0.5m
blob radius. Emits ui:play only; no edits outside owned paths.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Driven by the live fresh-eyes finding: a naive racer finishes with 0% paint —
the core mechanic is currently skippable. G makes paint route-guaranteed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First movement starts the clock; touching the pink pad (incl. its front face —
it's a 1.2-high podium, and mid-air boot crossings) finishes: banner, localStorage
best, R/Start or 6s auto-restart. Course fix: the gap jump soft-locked straight-
line drivers under the green slope — sealed the under-shelf cavity (plinth) and
added a deterministic launcher pad in the slot (restitution pachinkoed off the
shelf face; held-W air control flattens shallow arcs, so it launches high+centered).
Verified headless: naive hold-W racer finishes in ~12s, loop restarts, best persists.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Left stick is radial-deadzone analog (magnitude preserved — gentle stick =
gentle roll) feeding the same camera-relative move vector as WASD; jump goes
through the buffered-edge path as Space. Verified headless with a stubbed
navigator.getGamepads: full/half-stick speeds, single edge jump, respawn.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Integration: blob+paint+machines composed in game.ts; cannons staged along the
racing line (muzzle 26 — default 16 undershoots course distances); plate
threshold above clean-blob mass so only painted blobs trip it (paint=weight=
machine access); belt delivers riders under the bucket (shallow catch timed to
teeter drift); arch moved to a detour lane.
Cross-lane fixes found at integration, all one class — gameplay logic living
on render-frame time, which stalls in hidden tabs:
- telegraph windup -> fixed step (gates forces/dumps)
- bucket tip/pour -> fixed step (gates the splat)
- blob group sync -> fixed step in controller (paint proximity gate reads it)
world.ts grew tick()/renderOnce() + a hidden-tab watchdog interval for this.
Assets: mesh wave 1 (4 GLBs via farm: blobbo-base, toaster, spring boot,
paint bucket) + mesh_pipeline.py with outdir scp fallback for the farm's
unregistered-remote-outputs bug.
Verified headless end-to-end via tick(): move/jump/squash, cannon hits on a
moving blob, coverage->buffs (BURN 1.6x, super at 70%), mass 1.0->1.67 with
paint, plate clean-reject + painted-trip, boot launch (peak y 3.9), belt->
bucket dump (23.6% purple), arch cleanse.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Player-triggered contraptions that chain purely via world.events
('machine:signal') with a mandatory ~0.5s telegraph before any force or
paint dump (GDD readable-chaos pillar).
src/machine/:
- telegraph.ts: unmissable windup (accelerating flash + shake + scale pulse)
ticked once per world; parts await it and only then fire.
- parts.ts: PressurePlate (real body.mass() threshold), SpringBoot
(telegraph -> impulse on strike volume), SeeSaw (Rapier revolute joint
with limits + optional rest tilt), BucketDump (telegraph teeter ->
paint:request-splat + slosh), ConveyorBelt, BubbleArch (rising-edge
paint:request-cleanse + bubbles), Fan (per-step impulse so mass does the
work). Config-driven; no cross-instance references.
- surfaces.ts: normal/ice/oil/honey/mud/water registry with applySurface +
surfaceAt (downward ray) query.
demos/lane-c.html + src/demo/lane-c.ts: heavy ball runs
PressurePlate -> SpringBoot -> SeeSaw -> ice runway -> BucketDump, plus
conveyor/arch/fan and ice/honey probe lanes. Debug keys T/L/S/F/P and
per-second STATE console lines. Layout constants tuned via a headless
physics sweep so the chain completes deterministically.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixed-step world runtime, event bus wire protocol between lanes, integration
seam (game.ts) with placeholder ball. Lanes A/B/C own disjoint dirs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fall Guys x Splatoon x Micro Machines — paint-coverage-as-loadout blob racer.
GDD.md is the review artifact: feature tiers, draft 7-colour buff table,
tech recommendation (web-first three.js+Rapier), milestones, Opus 4.8 lanes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>