TURNCRAFT/docs/LANE_E_update.md
jing 9c668d340c Headshell Workshop + Glow-Up phase: assembly minigame, art pass, review fixes
- Workshop (WORKSHOP_CARTRIDGE): five-stage cartridge assembly at Deck A —
  seat/square, crimp four tag-wires, torque screws, ride-the-arm counterweight
  balance, needle-drop diagnostic with per-fault audio + scope. Relay-synced
  (per-field co-op merge: held screw + carried wire survive remote state).
- Glow-Up (G1-G5): 32px atlas with per-voxel variants, selective LED bloom
  (quality-gated), screen-print decal system + party flyers, mixer/PCB worldgen
  density pass, record groove-sheen side texture.
- 10 confirmed multi-agent review fixes, incl. co-op screw-stomp soft-lock,
  ride-snap collider-identity (magnet feet / eaten record-fling), workshop SFX
  exact-match map (rca_seated hijack), beam ride colliders to the head,
  double-crimp guard, WIRING INCOMPLETE diagnosis mode, skate-abort timer,
  completedState wiring, per-tick material churn, trackingHeavy platter drag.
- Crossfader playtest fix: slew-limited sled (3.4 v/s) + ribbed grip caps with
  amber index — the sled reads as a heavy handle, not a teleporting wall.
- Demo harnesses: machinesDemo hold-key wiring, playerDemo seesaw phase
  continuity, audioDemo incomplete fault button.
- Workshop sync: exact 1.0 screw endpoint gets its own emit signature.

Verified: typecheck + vite build clean, live solo quest smoke, two-client
co-op relay smoke (simultaneous torque, no rewind, exact convergence).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 21:32:07 +10:00

11 KiB
Raw Blame History

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.


Round 2 — Headshell Workshop, Lane E slice (2026-07-14)

Fable asked me to take the Lane E (audio/fx/ui) portion of WORKSHOP_CARTRIDGE.md while other agents handle the machine/interaction core. Done, additive, staying strictly in my three directories + demo. Zero core edits (I did not touch src/core/events.ts — Lane D added the workshop:* events per its brief; I only subscribe to them).

Coordination note: the Lane D workshop session was building src/machines/workshop/** concurrently on the shared tree. I read their actual code and aligned to it rather than my brief-guesses — so integration is by bus events, not direct calls, my error vocabulary matches theirs (the 6th mode is skate, not rotationWrong), and I removed my src/fx/scope.ts because their machine already renders the scope block. No file-ownership overlap.

What I added (all verified live in demo-audio.html → "Headshell Workshop"):

  • Diagnostic audiosrc/audio/testSignal.ts playTestSignal(errors): a true-stereo kick+bass loop with one audible chain per fault (L/R ping-pong, phase invert, azimuth 12 dB + distortion, 80 ms gate stutter, 1.2 kHz muffle, skate-abort). AudioEngine reacts to workshop:test.
  • 9 workshop SFX (sfx.ts): crimp, wireOn/Off, screwTick/Clunk, tiltCreak, weightDetent, bubbleLock, skate — driven by workshop:torque (ratchet) and machine:interact action keywords.
  • HUD — radial torque gauge on workshop:torque (hides on value:null), fault subtitle on workshop:msg (Hud.showTorque/hideTorque also public).
  • FX — screw confetti on workshop completion (signal:repair node stylus), also fx.screwBurst(x,y,z).

Two things for Fable/Lane D to close the loop:

  1. trackingHeavy — Lane E does the audio lowpass; Lane D should also sag the platter target rpm ~15% (platter.ts) for the full "drag".
  2. Optional: Lane D can emit machine:interact {action:'crimp'|'screwClunk'|…} for the per-step SFX; the torque/test events already cover the main cues.

Whole tree typechecks clean with Lane D's Workshop.ts present; zero console errors exercising every error mode, the torque gauge, screw confetti, and all 9 SFX. Full API surface is in src/{audio,fx,ui}/HANDOFF.md (Round 2).


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).