From fedb43817c4019a216492bf0e612cf0635af0dd8 Mon Sep 17 00:00:00 2001 From: type-two Date: Sat, 18 Jul 2026 11:31:41 +1000 Subject: [PATCH] Lane docs F (audio), G (colour zones/puddles/MEGA-MINI), H (ghost/title) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- lanes/LANE-F-audio.md | 44 +++++++++++++++++++++++++ lanes/LANE-G-zones.md | 65 +++++++++++++++++++++++++++++++++++++ lanes/LANE-H-ghost-title.md | 47 +++++++++++++++++++++++++++ 3 files changed, 156 insertions(+) create mode 100644 lanes/LANE-F-audio.md create mode 100644 lanes/LANE-G-zones.md create mode 100644 lanes/LANE-H-ghost-title.md diff --git a/lanes/LANE-F-audio.md b/lanes/LANE-F-audio.md new file mode 100644 index 0000000..1aef961 --- /dev/null +++ b/lanes/LANE-F-audio.md @@ -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). diff --git a/lanes/LANE-G-zones.md b/lanes/LANE-G-zones.md new file mode 100644 index 0000000..3608ef6 --- /dev/null +++ b/lanes/LANE-G-zones.md @@ -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. diff --git a/lanes/LANE-H-ghost-title.md b/lanes/LANE-H-ghost-title.md new file mode 100644 index 0000000..be22c40 --- /dev/null +++ b/lanes/LANE-H-ghost-title.md @@ -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.