TURNCRAFT/docs/LANE_E_update.md
jing 5a39e3a947 TURNCRAFT: contracts, docs, and all five lane deliverables (pre-integration)
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>
2026-07-13 20:50:56 +10:00

8.5 KiB

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.tsDustField (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 typecheckclean 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] FxSystemgame:win wasn't idempotent (audio was), so a repeated event desynced visuals vs audio. → Guarded with an already-won check.
  3. [low] grooveaudio: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 faderIds (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).