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>
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>
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>