Commit Graph

16 Commits

Author SHA1 Message Date
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
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
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
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
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
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
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