- 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>
195 lines
11 KiB
Markdown
195 lines
11 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.
|
||
|
||
---
|
||
|
||
## 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).
|