Commit Graph

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