Commit Graph

46 Commits

Author SHA1 Message Date
type-two
1fa069729a feat(ui): paint identity polish — quiet HUD, loud finish
The HUD now whispers and the finish line celebrates. PaintHUD hides entirely
while you're clean (the body is the display — GDD pillar 2; an all-zero
seven-row table was dev noise) and shows rows only for colours you actually
carry. Added the missing MEGA/MINI buff labels — purple/pink have been live
since Lane G but the buff line never learned their names.

The finish banner now states your colour identity — "you finished 53% GREEN",
tinted and glowing in that colour ("squeaky clean" if you somehow finish
white) — and fires a confetti burst of falling paint chips whose colours are
weighted by YOUR coverage mix. The run's identity is the celebration.

Verified via world.tick: clean blob -> no HUD; green douse + cannon red ->
exactly those two rows + GRIP label; teleport finish -> banner "you finished
53% GREEN" + 36 mix-tinted chips. tsc + build green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 17:41:41 +10:00
type-two
37a363cf13 feat(machine): domino chain — knock it here, it fires over there
createDominoChain: a run of real dynamic slabs standing in a row. Roll into
the first and physics topples the rest; when the LAST tile (red-striped so the
payoff end reads) tips past 45 degrees the chain emits its `emits` signal
exactly once. A domino run is a Rube-Goldberg wire: knock it here, something
fires over there. Passive physics like the see-saw, so no telegraph — the
visible topple IS the telegraph. race:respawn stands every tile back up.

Placed in Toaster Alley on the right shoulder: detour into the row and the
completed chain triggers a signal-only GREEN cannon volley (interval 0 = never
auto-fires) that paints you from across the course — a third, silliest way to
earn the gate's green (~10%, a top-up; mine and bubbles stay the main routes).

Verified deterministically with world.tick() (no more wall-clock waits in the
throttled tab): blob knocks tile 1 -> all 8 topple -> signal emitted once ->
both cannons hit (2 green splats, 9.9%) -> respawn re-arms all 8 upright.
tsc + build green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 17:31:45 +10:00
type-two
194f3e22d3 feat(ui): course select on the title + per-course ghosts
The title screen now shows a course-select row: one card per course with its
emoji, name, your best time, and a ghost badge. The current course is
highlighted "you are here"; the others are links. COURSE_META in course/spec
is the one source of truth for the rotation — the title cards and the in-game
switcher both read it, so they can't drift.

Fixes a real cross-course bug found while scoping this: ghosts were stored
under one global key, so your Breakfast Rush ghost would replay through Butter
Run's walls. save/load/has/time now take an optional course key (threaded
installGhost -> recorder/player); the default course keeps the bare legacy key
so nobody's existing ghost is orphaned. The title's best/ghost line is
course-aware for the same reason.

Browser-verified with seeded storage: cards show per-course bests and ghost
badges independently, current-course card highlights, clicking a card loads
that course, breakfast's data untouched from butter. tsc + build green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 17:22:48 +10:00
type-two
e06e17296e feat(course): Butter Run — the oil-luge course
Third course (?course=butter), pure data. Surfaces are the star: two long
butter/oil strips form the fast centre line where steering barely works
(slip: scrub x0.12, brake x0.08 — commit and ride), while the safe side lanes
weave through staggered slalom walls. A CROSSWIND fan blows across the gap
between the strips; the fan applies equal impulse to every body, so mass is
the counter — pop the purple weight-bubbles before the luge (MEGA = heavy) and
the wind barely moves you, run in light and it shoves you into the slalom.
Paint = weight = route, no new mechanics. Red start cannons hand out BURN
speed; two checkpoint pairs (pre/post-luge); flat floor, no fall-outs.

Course switcher now cycles breakfast -> toaster -> butter -> breakfast (the
see-saw lab stays URL-only — it's a tuning rig, not a course).

Verified: course builds clean (strips/walls/posts/bubbles/finish all present,
centre lane trap-free), the controller's ground ray resolves the strips to
{surface:'oil'} friction 0.04 (the exact input the previously-verified slip
branch consumes), no console errors, tsc + build green. Note: motion *feel*
not measurable in the test harness (background tab throttles the sim to ~1
step/s) — the slip code path is identical to Breakfast Rush's verified patch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 17:14:22 +10:00
type-two
639de3adf8 feat(race): checkpoints — a fall costs time, not the run
New course/checkpoints.ts: slim post-pairs flanking the lane that light gold
when you roll between them. Falling into the void mid-run now soft-respawns you
at the last pair crossed with the CLOCK STILL RUNNING and your PAINT INTACT —
time is the penalty, paint is progress already earned. R / gamepad Start /
post-finish remain full restarts to the line (and re-arm every pair via
race:respawn). Re-crossing an earlier pair moves the respawn point there.
'race:checkpoint' rings the existing audio tick.

Breakfast Rush gets two pairs (gap-jump landing shelf, fork entrance) — both
deliberately on dry open ground: a mid-course pair was tried and cut because
the slope's volume ejects a respawner and the river's rinse strips the paint a
checkpoint exists to preserve. Toaster Alley gets one (past the mine); spec
courses declare theirs via `checkpoints:`.

Also moved the fall check from onFrame to a fixed-step system — it gates
gameplay, and onFrame stalls in hidden tabs (same lesson as telegraph/bucket).

Browser-verified: crossing lights the pair + fires the event, fall respawns at
the pair with clock running (4.78→4.98s) and red paint identical (15.3%), R
resets fully. tsc + build green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 16:55:00 +10:00
type-two
e71176d067 feat(machine): paint bubbles — drifting soap bubbles that burst into paint
John's original idea from the pitch: soap bubbles that hold paint. A
createPaintBubbles field spawns slow-drifting tinted bubbles; touch one and it
pops — droplet burst, a paint douse on whoever popped it, a soft upward boop —
then re-forms elsewhere after a beat. Ambient drift is the always-on telegraph;
the tint is the payload (colour language matches gates/tunnels). Bubbles expire
and re-form quietly so the field stays alive without popping in sync.

Placed twice: GREEN field in Toaster Alley between mine and gate (~18% per pop,
so the gate wants 2-3 bubbles — the recovery route if you missed the mine), and
BLUE field over Breakfast Rush's milk-river approach (pop blue -> waterproof ->
your paint survives the rinse; the bubble teaches the counter to the hazard it
floats above). Wired as spec kind 'bubbles' so any data course can use them.

Browser-verified: 7 bubbles drift, pop grants 18.1% green + GRIP path works,
popped bubble hides + respawns, no console errors; tsc + build green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 04:59:41 +10:00
type-two
2cf2d15623 feat(seesaw): teetery pre-tilted tipping-plank in the see-saw lab
Best-shot "teetery" tuning (John's call; fine-tune the feel later). The lab
see-saw now rests PRE-TILTED (~9° down toward the exit): you roll off a raised
deck onto its high end and down the plank, your weight pitches it steeper as you
cross the pivot, and you drop off the low end to the floor. Pre-tilt makes the
teeter read; a strong fan whose beam covers the whole plank carries the blob
over the pivot hump; the downhill line keeps it from wedging.

createSeeSaw now snaps the plank back to its rest tilt on race:respawn (a
free-tipping plank has no restoring force, so it would otherwise stay wherever
it settled and stop being boardable). Verified: rests -8.6deg, pitches to
-10.8deg mid-crossing, clears the pivot and drops off the far end.

Still lab-only (?course=seesaw), not in a shipped course.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 00:23:11 +10:00
type-two
85c29111fa feat(machine): see-saw tipAxis option + a tuning lab course
createSeeSaw gains `tipAxis: 'x'|'z'` (default 'x' = unchanged). 'z' rocks the
plank fore↔aft so you can cross it in a Z-flowing course — the missing piece
that kept the see-saw out of every course (it was X-crossing only).

Adds ?course=seesaw, a throwaway lab for tuning the see-saw in isolation:
raised deck flush into the plank's near end (no ramp to misalign), teeter
across, drop off the far end onto the floor (a drop is never a wedge), fan for
momentum. Browser-verified it completes end-to-end.

Feel is still open (John's call): at crossing speed the plank barely pitches
(reads as a stable wobble-bridge). Knobs left exposed — maxTilt/restTilt on the
part, plank density/damping in the factory, fan force in the lab — to tune
"does it feel good" together. NOT wired into any shipped course.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 16:00:55 +10:00
type-two
67921925f5 feat(course): data-driven courses + a second course (Toaster Alley)
A course is now a plain data object (boxes + machines + cannons + spawn/finish)
built by buildCourseFromSpec, which dispatches to the greybox box builder and
the Lane C machine factories. New courses are DATA, not a game.ts fork; load one
with ?course=<name>. Added COURSES.toaster = Toaster Alley, a flat, reliably
completable second level that puts the never-placed FAN into play (tailwind) and
reuses the mine→gate loop from a spec.

game.ts refactor: Breakfast Rush's tuned build moves verbatim into
buildBreakfastRush (returns its finish box); installGame picks the course by
?course=, parametrises the race loop's finish + respawn spawn, keeps a per-course
best time, and adds a small on-screen course switcher. Breakfast Rush is
untouched behaviourally (greybox now builds after the blob — colliders still
exist before the first physics step).

Deliberately NOT done: porting the finely-tuned Breakfast Rush to a spec (no
gameplay gain, real regression risk), and placing a see-saw (it's a supported
spec kind but wants an elevated bridge = deliberate level design).

Browser-verified: Toaster Alley completes end-to-end (best 4.73s saved under its
own key, green mine→gate loop opens the line); Breakfast Rush unchanged (spawns
on the plateau, crosses the gap-jump, puddles paint it). tsc + vite build green;
all 10 test suites pass (360 assertions).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 15:19:09 +10:00
type-two
333f11b9db feat(blob): honour surface tags — oil luge, water rinse, drag
The controller now resolves the surface under the blob from the ground
collider's userData.surface (the course already tags them) and reacts:
- slip (oil/ice): steering-scrub + brake throttled ~0.1x → you luge across,
  and even a grippy green blob slides (Min friction combine beats grip);
- drag (honey/mud/water): horizontal speed bleeds off (sticky/heavy feel);
- rinse (water): a small periodic paint:request-cleanse — an underwater
  shortcut costs you your loadout (GDD §5.5).

The surfaces.ts framework existed but nothing in the game consumed the
behaviour tags; this wires it, per that file's own "the controller queries the
surface" contract. Added a demo oil slick (applySurface 'oil' + Min combine) on
the machine-pad centre lane. Browser-verified: on oil the blob slides ~2.7u vs
0.42u on normal ground; resting on the milk river rinsed red 56.7% → 35.7% in
2s. honey-climb and mud-antipaint left as marked ponytail TODOs. build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 14:59:20 +10:00
type-two
856683832b feat(machine): colour-keyed gate — paint decides your route
createColorGate: a portcullis that stays shut unless an approaching blob
carries enough of a keyed colour (integration injects `qualifies()`, so the
part never imports the paint lane), then slides into the floor and closes
behind. GDD §5.4 colour-keyed shortcut — closes the core loop solo: the course
paints you, and being painted opens your route.

Placed a GREEN gate on the centre lane before the finish with short flank walls
(clear of the fork exits): roll over the green reward mine upstream → arrive
green → the gate opens for a straight shot; arrive un-green → detour around.

Door animation lives in the FIXED step, not onFrame — the door is a collider,
and onFrame stalls in a hidden tab (the lesson telegraph.ts/BucketDump already
learned; hit and fixed here). Browser-verified all three cases: opens on green
(59-74%), stays shut for a non-green blob in the zone, closes when the blob
leaves. typecheck + build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 14:44:06 +10:00
type-two
fef136dfbc feat(machine): paint-mine — first-through floor panels
A new createPaintMine contraption: a floor panel that arms, and on step-on
TELEGRAPHs (~0.35s) then bursts — douses whoever's on it with a cluster of
paint splats (reliably crossing the buff threshold) and pops them with an
impulse. `once` (default) makes it a first-through-only trap that reads as
spent afterward and re-arms on race:respawn, so the solo time-trial stays
repeatable. Reward vs hazard is pure config.

Placed two on the centre line of Breakfast Rush: a GREEN reward (launches you
forward, ~59% grip douse) and a PURPLE hazard (grows you MEGA and shoves you
off the fast line). Reuses the existing trigger (dynamicBodiesInBox),
telegraph, and paint:request-splat wire — no new systems. Browser-verified:
coat + pop + spent-look + re-arm all fire; typecheck + build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 14:21:05 +10:00
type-two
ad93d55de9 Custom-assets pill in game, deploy size guard
- game.ts: a forgotten Workshop swap otherwise reads as a broken game with no
  way back; the pill says what is on and links to the Workshop. hasOverrides()
  existed in idb.ts and nothing called it.
- deploy.sh: fail if dist/ exceeds 10MB. stage-farm-assets.sh copies 43MB of
  raw GLBs into public/, vite copies public/ into dist/, and deploy rsyncs all
  of dist/ — I hit exactly this while browser-testing (47MB build).

Browser-verified end to end: stock boot clean (44 scene children, blob r=0.5,
no console errors), Workshop lists all 18 slots, real .glb drop swaps the stage,
Save writes a raw ArrayBuffer manifest, the game loads it (120k-tri boot renders
in-game), pill appears, Clear restores the stock scene exactly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 12:39:09 +10:00
type-two
2823c93382 Wire the Workshop: registry boot, course.tramp slot, editor in the production build
- 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>
2026-07-19 12:32:23 +10:00
type-two
d318b4f031 Merge fix-editor: demo DB isolation, mesh-less rejection, per-instance clones, storage-failure handling, private-browsing degrade, canonical slot import
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 12:26:44 +10:00
type-two
624caf08da fix(editor): stop the workshop destroying the user's work
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>
2026-07-19 12:13:07 +10:00
Claude
e34773d35f harden asset runtime: guard the swap path, bound the boot, reconcile slots
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.
2026-07-19 12:12:07 +10:00
type-two
f827f6f861 Merge lane-i: asset runtime (manifest, registry, slot hooks, procedural fallbacks)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 11:54:11 +10:00
type-two
367e700934 Lane J: workshop editor — drop-in GLB fitting on the real course
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>
2026-07-19 11:40:25 +10:00
Claude
324d7d4568 Lane I: asset runtime — manifest, registry, slot hooks, IndexedDB overrides
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>
2026-07-19 11:37:08 +10:00
type-two
cabc60def8 Workshop plan + lane docs I (asset runtime) and J (editor)
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>
2026-07-18 15:30:09 +10:00
type-two
0f2aca542b Wave 2 integration: zones staged, fork machines own the district, tuning pass
- 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>
2026-07-18 12:18:30 +10:00
type-two
8875663c18 Merge lane-g: paint puddles, colour zones, MEGA/MINI scale buffs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 12:01:59 +10:00
type-two
3699a1635a Lane G: colour zones, paint puddles, MEGA/MINI scale buffs
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>
2026-07-18 12:00:54 +10:00
type-two
43d2b0049a Wire audio/ghost/title into game, race:respawn event, launcher x-centering
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 11:51:38 +10:00
type-two
4494924c8c Add lane-h demo to build inputs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 11:48:48 +10:00
type-two
874fe076cb Merge lane-h: ghost replay (record/save/replay best run) + title screen
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 11:48:46 +10:00
type-two
c0c3b880eb Add lane-f demo to build inputs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 11:48:02 +10:00
type-two
ae8a93c417 Lane H: ghost replay + title screen
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>
2026-07-18 11:47:34 +10:00
type-two
1e2e49518f Lane F: procedural WebAudio engine + soundboard demo
Event-driven comedy SFX, all synthesized (no assets, no deps):
- engine.ts: lazy AudioContext (dormant until ui:play / first gesture),
  master chain at ~-12dBFS through a soft limiter, persisted mute
  (blobbo:muted), 10-voice polyphony cap with steal-nearest-to-finish.
- sounds.ts: squish (impact-scaled wet splat), boing (springy sweep),
  splat (per-colour pentatonic bloop, PALETTE order), sparkle (bubbly
  arpeggio), clank+windup (0.5s accelerating ratchet matching the machine
  telegraph), tick (starter blip), fanfare (major arpeggio + new-best
  flourish), optional ambient conveyor hum. All ±10% pitch-randomized,
  all <400ms except fanfare.
- install.ts: subscribes the frozen wire events, returns { engine },
  mounts the bottom-right mute button.
- demos/lane-f.html + src/demo/lane-f.ts: soundboard driving the real
  installAudio wiring; impact slider, colour picker, splat-storm stress.

Verified: tsc strict + vite build pass; 36-check headless harness proves
dormancy pre-unlock, single-context creation, every sound synthesizes,
voice cap holds under a 60-splat storm, and mute persists. Caught+fixed a
clank() infinite loop (geometric tick interval converged below windup).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 11:46:28 +10:00
type-two
fedb43817c Lane docs F (audio), G (colour zones/puddles/MEGA-MINI), H (ghost/title)
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>
2026-07-18 11:31:41 +10:00
type-two
dd94161b8e Race loop: timer, finish pad, best time, auto-restart + gap-recovery launcher
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>
2026-07-18 01:23:07 +10:00
type-two
a966e65bde Gamepad support: analog stick + d-pad move, A/B jump, Start respawn, connect toast
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>
2026-07-17 21:39:00 +10:00
type-two
a5f48b9405 Deploy to partly.party/blobbo: vite base + deploy.sh (games-doctrine pattern)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 21:32:00 +10:00
type-two
d4d41208f8 M0 playable slice: lanes integrated, machine chain staged, hidden-tab hardening
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>
2026-07-17 20:50:16 +10:00
type-two
6979c0db38 Merge lane-c: telegraph, 7 contraptions, surface registry
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 20:33:19 +10:00
type-two
8aaa11cdc1 Lane C: Rube-Goldberg machine toolkit — telegraphs, parts, surfaces, chain demo
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>
2026-07-17 20:26:52 +10:00
type-two
827b39be2e Merge lane-b: paint skin, coverage, buffs, cannons, HUD (drop tracked build junk)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 20:18:46 +10:00
type-two
72d8c7ca80 Merge lane-a: blob controller, feel layer, camera, greybox course
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 20:18:28 +10:00
type-two
c169bf73aa Untrack node_modules and dist (foundation commit slip)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 20:14:45 +10:00
type-two
eed2eb2646 Lane B (Paint): splat pipeline, coverage compute, buff/threshold framework
The core invention: paint lands on the blob body where projectiles hit, live
per-colour coverage % is computed from a quantized UV mask, and coverage drives
buffs per GDD §6 MVP subset (RED speed+embers, GREEN grip, BLUE waterproof,
total→mass, 20% activate / 70% super+glow).

- src/paint/skin.ts       PaintSkin: Canvas-2D mask on mesh UVs → CanvasTexture;
                          splatAtPoint raycasts impact→face UV; dual soft-canvas
                          + hard quantized-index mask so coverage is exact
                          integer bucketing (never getImageData); 250ms cache;
                          scrub-hole cleanse; U-seam wrap.
- src/paint/coverage-math.ts  pure, unit-tested: countBuckets, coverage report,
                          buffStrength, computeModifiers.
- src/paint/cannon.ts     PaintCannon: gravity-arc glob projectiles (manual
                          swept sphere test), target-leading ballistic solve,
                          fires on interval AND machine:signal; emits paint:splatted.
- src/paint/buffs.ts      BuffSystem: coverage→modifiers each step + ember trail
                          (additive Points) + super-state emissive pulse.
- src/paint/hud.ts        PaintHUD: per-colour bars + active buff kit (throttled).
- src/paint/index.ts      exports + installPaint() wiring the inbound Lane C
                          protocol (request-splat proximity-gated / request-cleanse).
- src/demo/lane-b.ts      stub ball orbiting a RED and GREEN cannon; HUD live;
                          debug keys for fire/signal/flood/cleanse/pause.
- src/paint/coverage-math.test.ts  32 assertions (node --experimental-strip-types).

build: tsc strict + vite pass. No edits outside owned paths; contracts frozen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 20:13:30 +10:00
type-two
4b08ba65ac Lane A: blob controller, feel layer, follow camera, greybox
- src/blob/createBlob.ts: rotation-locked dynamic ball + clean UV-sphere body
  mesh (untouched UVs for Lane B) + cosmetic eyes; implements frozen Blob.
- src/blob/controller.ts: fixed-step System. Camera-relative WASD via impulses,
  buffered+coyote jump, grounded raycast, emits blob:jumped/blob:landed. Reads
  blob.modifiers every frame: speedMul, jumpMul, massMul, grip, size.
- src/blob/feel.ts: the comedy engine. Underdamped springs for landing squash,
  jump stretch, inertial jelly lean, idle breathing + run bob; base-pivoted so
  squash plants on the floor. Driven by the frozen wire events. glow -> emissive.
- src/blob/camera.ts: soft-follow third-person cam with position lag, yaw ease
  behind travel, and FOV kick at speed.
- src/course/greybox.ts: Breakfast Rush greybox — start plateau, ramp, gap jump,
  slope, translucent milk river (surface:water), machine drop area, finish pad.
- src/demo/lane-a.ts: boots world+greybox+blob, HUD + once/sec state log, debug
  keys 1/2/3 (speed/jump/size) plus 4/5/6 (mass/grip/glow), 0 reset, R respawn.

npm run build passes (tsc strict + vite). Owned paths only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 20:11:55 +10:00
type-two
969f602dd4 Foundation: vite+ts+three+rapier scaffold, frozen contracts, lane docs A-E
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>
2026-07-17 19:55:43 +10:00
type-two
2cf8211caa Concept wave 1 complete: 8/8 images off the farm
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 18:21:04 +10:00
type-two
7a02a09b1f Ignore pycache
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 18:16:24 +10:00
type-two
1506c12759 GDD v0.1: full design doc, idea capture, MODELBEAST farm client + concept wave 1
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>
2026-07-17 18:15:56 +10:00