Lanes A (engine), B (player), C (worldgen), D (machines/quest), E (audio/fx/ui) as landed, each with HANDOFF.md + update docs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
155 lines
8.5 KiB
Markdown
155 lines
8.5 KiB
Markdown
# LANE E — Update (Audio / FX / UI)
|
|
|
|
**Status: ✅ Complete, typechecks clean (whole 5-lane repo), verified live in the
|
|
browser, adversarially reviewed (6 findings — all fixed).**
|
|
|
|
Lane E owns `src/audio/**`, `src/fx/**`, `src/ui/**`, `src/demo/audioDemo.ts`,
|
|
`demo-audio.html`. Built strictly against the contract layer in `src/core/` —
|
|
nothing there was edited, no `setBlock` anywhere, no imports of other lanes'
|
|
concrete code, no rAF loop outside the demo, no new runtime deps (three only),
|
|
all audio synthesized, all textures procedural.
|
|
|
|
---
|
|
|
|
## What I built
|
|
|
|
### Audio (`src/audio/`) — "sound is the win state"
|
|
- **`synth.ts`** — low-level voices: kick (pitch-drop sine + click), hats,
|
|
clap, detuned-saw bass, resonant chord stabs, square/sine lead, noise
|
|
sweeps, plus procedural white-noise and vinyl-crackle buffers. No samples.
|
|
- **`scheduler.ts`** — a drift-free lookahead transport (25 ms tick, notes
|
|
scheduled on `AudioContext.currentTime`, not `setInterval` timing). A single
|
|
`rate` models the Technics spin-up/brake: it eases torque-ily toward its
|
|
target and scales tempo **and** per-voice detune together. Beat/bar events
|
|
are queued and fired at their audible time so LED pulses stay in sync.
|
|
- **`groove.ts`** — the endless 118 BPM, 8-bar F-minor deep-house loop in five
|
|
stems (drums → bass → chords → lead → sweeps). `setStemCount(0..5)` stacks
|
|
them musically; muted stems are not scheduled (near-zero cost). Sidechain-
|
|
ducked bass.
|
|
- **`sfx.ts`** — every diegetic one-shot: footsteps per surface category,
|
|
impact-scaled land, break/place, fader zip, button clunk, RCA
|
|
"clunk-clunk-CLICK", fuse zap, tonearm creak, needle drop.
|
|
- **`AudioEngine.ts`** — the WebAudio graph, positional sound from two
|
|
PannerNodes at the deck spindles (+ an always-on low-passed dry bass bed),
|
|
the analyser for VU `getLevels()`, the full bus wiring, and the win audio
|
|
timeline (silence → +1.8 s needle-drop → full mix slams in behind an opening
|
|
lowpass — all scheduled on the ctx clock). **Nothing sounds before `init()`
|
|
on a user gesture** (autoplay policy).
|
|
|
|
### FX (`src/fx/`) — the booth comes alive
|
|
- **`particles.ts`** — `DustField` (ambient brownian motes) and `Burst` (a
|
|
round-robin pool powering break puffs, win confetti, and 45-rpm rim
|
|
sparkle). Preallocated typed arrays, mutated in place — **zero per-frame
|
|
allocation**. One procedural glow texture, no assets.
|
|
- **`overlays.ts`** — emissive sprite overlays we own (never mutating another
|
|
lane's materials): mixer VU towers keyed to `getLevels()`, a bright sprite
|
|
that runs the signal-path polyline on each repair, and a patch-bay blink.
|
|
- **`layout.ts`** — every overlay anchor derived from `constants.ts` `LAYOUT`.
|
|
- **`FxSystem.ts`** — the global LED pulse via Lane A's injected
|
|
`setEmissiveBoost` (dead-booth base 0.35 → steps toward 1.0 over the five
|
|
repairs → pulses to ~1.8 on beats), plus the win visual timeline (1.8 s dark
|
|
→ drop flash + confetti → living-booth steady state).
|
|
|
|
### UI (`src/ui/Hud.ts`) — DOM overlay
|
|
Crosshair, 9-slot hotbar (tint swatches + counts + active ring, via an injected
|
|
`getHotbar()`), 5-node quest tracker, event subtitle line, the **start splash
|
|
that gates the AudioContext + pointer lock**, a pause overlay, and the win
|
|
banner. `pointer-events:none` except the gate screens, so it never eats
|
|
gameplay input. Dynamic text via `textContent` (no injection risk).
|
|
|
|
### Demo (`demo-audio.html` + `src/demo/audioDemo.ts`)
|
|
Runs with **zero code from other lanes**: a mock stage (dark tabletop + two
|
|
glowing "record" discs at the real deck spindle positions, orbit camera) and a
|
|
full control panel exercising every acceptance criterion, including a quest
|
|
simulator that previews the entire signal→stem→light arc and the win sequence.
|
|
A mock `setEmissiveBoost` flashes the discs on every beat via the real FX code
|
|
path; a mock hotbar feeds the HUD.
|
|
|
|
---
|
|
|
|
## Acceptance criteria — all met
|
|
|
|
| # | Criterion | Status |
|
|
|---|---|---|
|
|
| 1 | Groove runs indefinitely with zero drift; stems stack 0→5 | ✅ lookahead scheduler; gated stems |
|
|
| 2 | Spin-up/brake bends tempo+pitch together; pitch fader detunes | ✅ single `rate` → tempo+detune; `setPitch` |
|
|
| 3 | Walking between decks pans/attenuates | ✅ two PannerNodes + listener follow; dry bed remains |
|
|
| 4 | Every SFX distinct, non-clipping; footsteps vary by surface | ✅ per-category footstep bodies |
|
|
| 5 | Beat pulse, VU, dust, break puffs at 60 fps | ✅ zero-alloc hot paths, verified live |
|
|
| 6 | Full win sequence previews from the quest simulator | ✅ verified end-to-end in browser |
|
|
| 7 | No audio before user gesture; typecheck clean; HANDOFF | ✅ start-splash gate; clean; 3 HANDOFFs |
|
|
|
|
---
|
|
|
|
## Verification
|
|
|
|
- `npm run typecheck` — **clean across the entire repo** (all five lanes are
|
|
now present; the whole tree compiles under strict mode).
|
|
- **Browser smoke test** (`/demo-audio.html`): start gate → booth alive (discs,
|
|
VU towers, dust, hotbar) → all five repairs light the tracker, run the LED
|
|
signal-path trace, show subtitles, and step the booth brighter → WIN plays
|
|
the dark→needle-drop→slam sequence with the "THE MIX IS LIVE" banner.
|
|
**Zero console errors** across the whole session.
|
|
|
|
## Adversarial review (multi-agent) — 6 findings, all fixed
|
|
|
|
Ran a 5-dimension review (contract / audio / fx / ui / demo) with an
|
|
adversarial verify pass. 10 raw findings → 4 refuted as false positives → **6
|
|
confirmed, all now fixed**:
|
|
|
|
1. **[med] Hud** — pause "resume" reused `onStart`, re-running first-start side
|
|
effects. → Added a separate `onResume` callback (re-lock only).
|
|
2. **[low] FxSystem** — `game:win` wasn't idempotent (audio was), so a repeated
|
|
event desynced visuals vs audio. → Guarded with an already-won check.
|
|
3. **[low] groove** — `audio:beat.energy` was a bare constant vs the contract's
|
|
"low-band energy". → Now scaled by the drums channel gain.
|
|
4. **[low] Hud** — the win-banner `setTimeout` leaked (not cleared in
|
|
`dispose`). → Tracked and cleared.
|
|
5. **[low] demo** — control panel `z-index` sat above the start splash,
|
|
defeating the gesture gate. → Panel is hidden until start.
|
|
6. **[low] demo** — resume force-restarted Deck A and desynced its button. →
|
|
`hasStarted` guard + button label sync + pure `onResume`.
|
|
|
|
Fix #1 changed the `Hud` public API (added optional `onResume`) — noted in the
|
|
HANDOFFs. All fixes re-typechecked and re-verified live.
|
|
|
|
---
|
|
|
|
## Notes for integration (friction to confirm — details in `src/audio/HANDOFF.md`)
|
|
|
|
1. **`fader:move` → channel mapping**: I map `faderId` containing `pitch` →
|
|
pitch bend, else the first digit `N` → stem channel `N-1`. Confirm Lane D's
|
|
`faderId`s (crossfader / named channels) and extend if needed.
|
|
2. **Channel↔stem mapping**: 5 stems vs 4 mixer channels + crossfader —
|
|
`setChannelGain(ch, v)` treats `ch` as a stem index 0..4.
|
|
3. **`player:landed`/`player:step`** carry no surface category / position; I use
|
|
the last stepped surface and listener-centered playback.
|
|
4. **`setEmissiveBoost` semantics**: I drive it as an absolute intensity
|
|
(0.35 → 1.0 base, ~1.8 peak). If Lane A's hook is a multiplier, tune
|
|
`FxSystem` constants.
|
|
5. **VU tower positions** are derived from `LAYOUT.mixer`. If Lane C placed
|
|
physical LED towers elsewhere, align `src/fx/layout.ts` `VU_TOWERS`.
|
|
6. **Hotbar model** — no core type; `Hud` exports `HotbarModel`/`HotbarSlot`,
|
|
consumed via injected `getHotbar()`. Adapt Lane D's hotbar to this shape.
|
|
7. **`game:win`** forces deck playback in audio; ensure Lane D also spins the
|
|
real platters at the finale so audio and visuals agree.
|
|
|
|
Wiring is exactly as `docs/INTEGRATION.md` step 8 describes:
|
|
`AudioEngine` + `FxSystem({ scene, setEmissiveBoost })` + `Hud`, audio init
|
|
behind the start-overlay click, `fx.update(dt)` + `audio.updateListener(player)`
|
|
each frame.
|
|
|
|
## Repo / git
|
|
The working tree is **not yet a git repo** locally, and the fresh remote
|
|
(`ssh://git@100.71.119.27:222/monster/TURNCRAFT.git`) is empty. I did **not**
|
|
`git init`/commit/push — that's a cross-lane integration decision, so I left it
|
|
for Fable/the integrator to sequence (init, land all five lanes, run
|
|
`docs/INTEGRATION.md`, then push).
|
|
|
|
## Suggested next steps for Fable
|
|
1. Integrate per `docs/INTEGRATION.md` (all five lanes' `HANDOFF.md` first,
|
|
resolve the friction items above — especially #1, #4, #6).
|
|
2. Wire `src/main.ts` step 8 and run the smoke tests.
|
|
3. Once integrated, do a full-game soak (the demo already proves Lane E holds
|
|
60 fps and never drifts in isolation).
|