The three that stopped a pack shipping:
- a mesh-less GLB (armature-only export) fired onSwap anyway, hiding the
primitive and adding nothing — an invisible prop with a live collider.
Mesh count is now checked BEFORE the scene is touched, in one guard that
covers every consumer including slotObject/blob.face.
- the fulfilment path had no try/catch, so a throw became an unhandled
rejection. Split in two: a build failure bails out before onSwap can hide
anything; an onSwap failure keeps the replacement parented, because
consumers hide their primitive on onSwap's first line and removing the fit
node there would manufacture the one forbidden state.
- preload() awaited every url forever. Each is now raced against a 10s
deadline with allSettled semantics: a stalled host costs one warning and a
fallback prop, not a game that never boots.
Also:
- instantiate/instanceSync never throw — createBlob's call site is frozen and
unguarded, so a throw there is a black screen.
- ghost and blob share fitBodyToRadius(); a borrowed blob.body was rendering
the ghost 2.17x oversized with the farm mesh.
- skinned blob.body is rejected loudly instead of silently half-working; the
idle-clip mixer is gone (it animated an orphaned skeleton in the fixed step).
- paintableInfo counts UV islands: blobbo-base.glb passes every other check
and still paints wrong at 1140 charts. Warning, not rejection.
- slots.ts gains cannon.base, course.finish, course.tunnel, course.tramp, all
hooked except tramp (built in frozen game.ts).
- manifest ignores _-prefixed metadata keys instead of calling them typos.
Empty-manifest parity verified byte-for-byte: scripts/sacred-parity.check.ts
fingerprints the built scene and reports 49df4f20 on main and on this branch.
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>