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

195 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 audio** — `src/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.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).