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>
This commit is contained in:
type-two 2026-07-18 11:31:41 +10:00
parent dd94161b8e
commit fedb43817c
3 changed files with 156 additions and 0 deletions

44
lanes/LANE-F-audio.md Normal file
View File

@ -0,0 +1,44 @@
# Lane F — Audio (procedural, event-driven)
**Owns:** `src/audio/`, `demos/lane-f.html`, `src/demo/lane-f.ts`.
**Never touches:** contracts, world, game, main, package.json, other lanes' dirs.
No new deps, no audio asset files — **everything synthesized with WebAudio**
(oscillators, noise buffers, filters, envelopes). Comedy > realism.
## Mission
BLOBBO is silent. Sound is half the comedy of a wobble game. Build a tiny
procedural sound engine that reacts to the frozen wire events only.
## Deliverables (`src/audio/`)
1. `engine.ts``AudioEngine`: lazy `AudioContext` (create/resume ONLY after
the `ui:play` event or first user gesture — browsers block earlier), master
gain, mute toggle (persist in localStorage `blobbo:muted`), and a small
polyphony cap (~8 voices) so event storms don't clip.
2. `sounds.ts` — synthesized one-shots, all pitch-randomized ±10% per play:
- **squish** (blob:landed — gain and pitch scale with `impact`; big drops = big splat)
- **boing** (blob:jumped — springy sine sweep; also used by the gap launcher)
- **splat** (paint:splatted target:'blob' — wet noise burst; slightly
different pitch per colour, use PALETTE order for a pentatonic-ish mapping)
- **sparkle** (paint:request-cleanse — bubbly arpeggio)
- **clank+windup** (machine:signal — ratchet tick accelerating ~0.5s, matches the telegraph)
- **fanfare** (race:finished — dumb triumphant 4-note; extra flourish if new best)
- **tick** (race:started — single starter blip)
- optional ambient: very quiet lo-fi room tone / conveyor hum when near machines — only if cheap.
3. `install.ts``installAudio(world)`: subscribes to the events above on
`world.events`, returns `{ engine }`. Listens for `ui:play` to unlock.
A tiny 🔈/🔇 DOM button bottom-right toggles mute.
4. Demo `demos/lane-f.html` + `src/demo/lane-f.ts`: sound board — a button per
sound + sliders for the impact parameter. Verifies every sound in isolation.
## Acceptance
- `npm run build` passes. No AudioContext created before first gesture/`ui:play`
(verify: constructing the engine logs nothing and `ctx === null` until unlock).
- Every listed event produces a distinct, non-clipping sound; rapid splat storms
(10/s) stay clean via the voice cap.
- Mute persists across reloads.
- Sounds are SHORT (<400ms except fanfare) and mixed well below pain (master ~-12dB).
## Events you consume (never emit gameplay events)
`blob:landed {impact}`, `blob:jumped`, `paint:splatted {color,target}`,
`paint:request-cleanse`, `machine:signal {id}`, `race:started`,
`race:finished {time,best}`, `ui:play` (from Lane H's title screen).

65
lanes/LANE-G-zones.md Normal file
View File

@ -0,0 +1,65 @@
# Lane G — Colour zones, paint puddles, MEGA/MINI
**Owns:** `src/paint/puddles.ts`, `src/paint/buffs.ts` (extend — you inherit
Lane B's file), `src/course/zones.ts`, `demos/lane-g.html`, `src/demo/lane-g.ts`.
**Never touches:** contracts, world, game, main, package.json, `src/blob/`,
`src/machine/` (USE machine parts via import — just don't edit them), other
lane dirs. No new deps.
## Why this lane exists (live-play finding, 2026-07-18)
A naive racer currently finishes the course with **0% paint** — ballistic
cannons miss fast movers and nothing on the ground paints you. The core
mechanic is skippable. Fix: paint must be guaranteed by ROUTE, not by aim, and
colours must come in ZONES so buffs actually activate (20% threshold is
unreachable when three colours dilute each other — also observed live).
## Deliverables
1. `src/paint/puddles.ts``PaintPuddle`: a flat coloured strip on the track
(mesh + config `{position, size, color}`). While the blob's ground contact
is inside the strip, stamp its UNDERSIDE via `skin.splatAtPoint` at the
contact point on a cadence (~6 stamps/s, small radius) — rolling through
paint coats your belly. Registered via an `installPuddles(world, blob, skin,
configs)` entry point (integration calls it). Emit `paint:splatted
{color, target:'blob'}` per stamp (audio hooks it).
2. `src/paint/buffs.ts` — extend the coverage→modifier mapping (GDD §6):
- **purple MEGA**: `size` 1.0→1.45 + extra `massMul` (heavy). Strength scales
20%→70% like the others.
- **pink MINI**: `size` 1.0→0.62, LESS mass (lighter than clean at high %).
- purple and pink fight: net size uses (purple pink) strength.
- Existing red/green/blue unchanged. Keep the pure-function style +
UPDATE `src/paint/coverage-math.test.ts` for the new mappings.
3. `src/course/zones.ts``installZones(world, blob, skin)`: builds the colour
zones ON the existing greybox (import nothing from game.ts; use absolute
course coordinates — the course spans z 30 → -70, see src/course/greybox.ts):
- **RED zone** (ramp approach, z≈20..8): red puddle strip + the existing
red cannon position gets a second red cannon (crossfire). BURN speed helps
the climb.
- **GREEN zone** (shelves + slope, z≈8..-24): green puddles on both shelves;
GRIP helps the slope and gravity-flip future.
- **BLUE zone** (milk river, z≈-24..-37): blue puddle lines the riverbanks;
waterproof = your paint survives the water.
- **PURPLE/PINK fork** (machine district, z≈-37..-58): LEFT lane (x<0):
purple puddle → MEGA → heavy enough to trip the pressure plate → boot yeet
(the course TEACHES paint=weight). RIGHT lane (x>3): pink puddle → MINI →
a **low tunnel** (build it: a slab roof over the track at y≈0.9, only a
MINI blob fits under) that shortcuts toward the finish. Normal-size blobs
bonk and must take another lane.
- Cannons: relocate/add so each zone's cannons fire ITS colour (configs
returned so integration can place them; or place them yourself in
installZones — your call, document it).
4. Demo `demos/lane-g.html` + `src/demo/lane-g.ts`: a test ball rolling through
each zone in sequence with live coverage/modifier logging; prove: puddles
coat reliably, purple grows + trips a test plate, pink shrinks + fits the
tunnel while a clean ball bonks.
## Acceptance
- `npm run build` + updated coverage-math tests pass.
- A straight-line hold-forward run through the zones ends with ≥40% total
coverage and at least one active buff (puddles guarantee it — verify in demo).
- Purple demo ball trips a `massThreshold: baseMass*1.25` plate; clean ball doesn't.
- Pink demo ball (≥35% pink) passes under the tunnel; clean ball cannot.
- No edits outside owned paths.
## Contracts
Emit `paint:splatted` per puddle stamp. Existing events unchanged. `blob.modifiers.size`
is already honored by the controller (collider + visual scale) — do not touch the blob.

View File

@ -0,0 +1,47 @@
# Lane H — Ghost replay + title screen
**Owns:** `src/ghost/`, `src/ui/`, `demos/lane-h.html`, `src/demo/lane-h.ts`.
**Never touches:** contracts, world, game, main, package.json, other lanes' dirs.
No new deps.
## Mission
Two pieces that turn a tech slice into a GAME you return to:
a ghost of your best run to race against, and a title card that explains the
game in five seconds (and unlocks audio).
## Deliverables
1. `src/ghost/recorder.ts``GhostRecorder`: on `race:started`, record the
blob root's position + uniform scale at 15Hz into a flat Float32Array-backed
buffer (cap ~5 min). On `race:finished`, if this run is the new best, save
quantized (int16 centimeters is plenty) + the time to localStorage
`blobbo:ghost` (base64). Keep total under ~200KB.
2. `src/ghost/player.ts``GhostPlayer`: if a saved ghost exists, on
`race:started` spawn a translucent ghost blob (simple sphere + eyes is fine,
opacity ~0.35, no physics — kinematic visual only) and replay it in race
time with linear interpolation between samples. Despawn on finish/respawn.
The ghost must be OBVIOUSLY not-you (ghostly white/desaturated, slight bob).
3. `src/ghost/install.ts``installGhost(world, blob)` wiring both to events.
4. `src/ui/title.ts``installTitle(world)`: full-screen overlay on boot:
- big BLOBBO wordmark (styled CSS bubble text is fine — thick white letters,
rainbow paint-drip accents, Comic-adjacent but tasteful),
- one-line pitch: "Race. Get painted. Paint is power.",
- controls card: WASD/stick · Space/A jump · R/Start restart,
- a fat **PLAY** button. On click/keypress/gamepad-button: fade out,
emit **`ui:play`** on world.events (audio unlocks on this), start allowed.
- While Rapier/world is still booting, show the same overlay with a wobbling
"loading…" blob dot instead of PLAY (installTitle can be handed a promise).
- Show saved best time + "👻 ghost ready" if `blobbo:ghost` exists.
5. Demo `demos/lane-h.html` + `src/demo/lane-h.ts`: fake a 20s scripted "run"
(a ball on a circle), record it, finish, then restart and race the ghost.
Title screen in front. Debug key G wipes the saved ghost.
## Acceptance
- `npm run build` passes; demo shows title → play → run 1 records → run 2 shows
interpolated ghost racing the circle; localStorage survives reload; wipe works.
- `ui:play` emitted exactly once per page life; overlay never reappears mid-game.
- Ghost replay drift vs recorded path < one blob radius at all sample points
(it's the SAME deterministic path in the demo — verify numerically).
- No edits outside owned paths.
## Events
Consume `race:started`, `race:finished {time,best}`. Emit `ui:play` only.