TURNCRAFT/docs/SIDEB_E2_handoff.md
type-two 33232fdeba SIDE B: the crate worlds — three dive-in pockets, stamps, golden slipmat
Three records in the under-table crate are pressed with whole scenes. E beside
one dives you into its pocket above the booth walls; complete its sound-first
quest to press a STAMP; all three raise the golden slipmat + a permanent bonus
percussion stem. Dust bunnies move in under the table.

- Worlds (Lane W): ACID WAREHOUSE / DUB CHAMBER / DISCO LOFT — sealed themed
  pockets, portal rings, exit pads; deterministic, zero light leaks (incl. the
  flythrough path), meshes unchanged at 469, build ~760 ms.
- Machines (Lane M): portal dive/exit/safety (membership-based escape catch),
  tune-the-303 hold-sweep pedestals, carry-the-charge wobbling spring bridge,
  beat-Simon dance floor with glow tiles; stamps store + relay {t:'stamp'}
  (default-closed, persisted, join snapshot, fuzzed); golden slipmat; stamps
  survive the reset ritual.
- Ambience (Lane S): dust bunnies (wander/flee, zero steady-state allocs),
  vinyl-warp portal iris, HUD stamp tray, stamp confetti + golden shimmer.
- Audio (Lane E2): per-world grooves (acid 303 w/ fader-driven bands, dub
  feedback-echo + spring, disco pentatonic tiles) over a boothGain
  duck-and-handback that can never restore stale state; portal/stamp/golden
  SFX; bonus stem.

Post-integration review (2 seam finders, 10 findings, all fixed + verified):
stamp events carry origin (local/remote/replay) so consumers celebrate
proportionately instead of wall-clock guessing — kills phantom join
celebrations, first-person confetti for peers' wins, and audio/visual grace
mismatches; reconnect re-uploads locally-won stamps the server missed;
relay saveState survives disk errors; pocket escape via mined floor counts as
a real exit at any altitude; exit-pad speed gate + dwell stops mid-quest
yank-outs; dive cooldown stops exit/re-dive ping-pong; locked acid bands
need deliberate holds (tap-spam dead); disco foot band excludes jump apex.

Verified: whole-tree typecheck + build clean; per-lane live verification
(27/27 worldgen assertions, relay fuzz, 18-phase audio matrix, fps unchanged);
integrated smoke: dive/exit/safety/stamps/golden + all origin paths live.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 16:22:11 +10:00

8.0 KiB
Raw Permalink Blame History

SIDE B — Lane E2 handoff (audio: world grooves + SIDE B sound)

Everything in src/audio/** only. npm run typecheck clean. AudioEngine still self-wires to the bus, so the integrator changes nothing in src/main.ts for this lane — construct AudioEngine exactly as before.

Files touched

file change
src/audio/worldGrooves.ts new — the three pocket grooves (acid 118 / dub 74 half-time / disco 122), each on its own lookahead Transport with spin-up/brake; quest hooks (acid bands + locks, dub echo/carry/feed, disco cues/steps, stamp glory)
src/audio/worldSynth.ts new — SIDE B voices: the 303, shimmer/lock chime, dub sub/rimshot/skank/spring-boing/feed-burst, disco oct-bass/string stab/cue blip/string swell
src/audio/AudioEngine.ts boothGain bus (every booth source funnels through it; pockets duck THIS), worldBus (+ analyser tap so pocket grooves drive the VU), world enter/exit + stamp + stamp:all handlers, SIDE B machine:interact/fader:move routing, new playSfx names
src/audio/groove.ts bonus percussion stem (shaker + conga clave, own gain outside the 5 fader channels, schedule-gated) + setEmitBeats (beat/bar suppression during dives)
src/audio/scheduler.ts additive bpm constructor param (booth default 118 unchanged)
src/audio/synth.ts additive shaker, conga voices
src/audio/sfx.ts additive portalWhoosh, stampThunk, stampFanfare, goldenRiser, bunnySqueak (+ SfxName entries portalIn/portalOut/stampThunk/goldenRiser/bunny)
src/demo/audioDemo.ts SIDE B panel (world buttons, three acid faders + locks, dub pickup/feed, 4×4 disco tiles + step echo, stamps, bunny/airhorn/blow-fuse) + a marked DEMO MOCK automation handle (window.DEMO)

Duck-and-handback model (why a reset can't wedge it)

All booth music (stems via panners + dry bed and the zero-repair heartbeat) funnels through one boothGain. world:enter ducks that gain to 0 in 300 ms and spins up the pocket groove on worldBus; world:exit reverses. The booth graph stays live underneath the duck — repairs, the reset ritual power-down, a win, remote stem changes all keep flowing — so the handback never restores a snapshot; it reveals the booth's current state. Verified: dead booth (heartbeat returns), won booth (full mix returns), and quest:reset fired while inside a pocket (power-down runs muffled under the groove; exit lands on the post-reset heartbeat).

Events consumed (exact expectations)

  • world:enter { world } / world:exit { world }world in acid|dub|disco (anything else is ignored). Enter = portal whoosh-down + duck + groove spin-up. Exit = reverse whoosh + brake + handback. Re-entering the same world while inside it is a no-op; entering a second world hard-switches.
  • fader:move { faderId: 'acid_0'|'acid_1'|'acid_2', value: 0..1 } — that band's 303 cutoff, exponential across per-band offset ranges (1102600 / 1603800 / 2405600 Hz), so the sweep is loudly audible anywhere in 0..1 and Lane M's hidden targets can sit anywhere. No booth channel is touched (the old digit→channel map is bypassed for acid*) and no fader-zip spam. Values are remembered across dives and pre-init.
  • machine:interact actions (verified against Lane M's src/machines/worlds/* in-tree — their interactAt() both casts a runtime index property in AND suffixes machineId; either alone satisfies me):
    • acid_lock (their machineId acid_ped<i>, index prop) — lock chime + that band's shimmer layer (16th-offbeat pings). Locks reset on every world:enter acid (targets re-seed per entry). acid_knob and acid_tuned are swallowed silently — the cutoff sweep / stamp glory IS the sound.
    • dub_pickup / dub_drop — echo wet 0.32 ↔ 0.8 over ~0.8 s ("carry the wet"). Their chasm-fall dub_drop is handled.
    • dub_feed — the big SPROING: bandpassed burst into the echo, feedback 0.55 → 0.8 for ~2 s → back, wet settles down after.
    • disco_cue (machineId disco_tile_<0..15>) — pentatonic blip, pitch rises with index (F-minor pentatonic, 2+ octaves).
    • disco_step (same scheme) — the player's step echoes that tile a fifth down, softer. (Lane M already emits this in their WIP — confirmed.)
    • disco_miss — a gentle descending "aw" (THE FLOOR FORGIVES — never a buzzer); disco_lit swallowed (the stamp swell follows immediately).
    • portal_dive / portal_exit — swallowed: the world:enter/world:exit whooshes carry the moment (prevents a stray generic button clunk).
    • bunny_flee — shy squeak, throttled to ≥1.1 s apart (Lane S already caps at 1/s globally).
  • stamp:got { world, count, total } — press-THUNK + fanfare in that world's key + in-groove glory (acid: 4 bars of claps + octave doubling; dub: big boing; disco: filtered string swell). Deduped per world per session — join-snapshot re-emits are silent. count===total also arms the golden unlock (below), so stamp:all is not strictly required.
  • stamp:all {} — golden riser (once per session), then the bonus percussion stem (shaker + conga) enters the booth mix at the riser's bloom and stays permanently (survives quest:reset — stamps live in the records, not the booth). It rides whenever the booth mix is audible (gated on stemCount > 0 so a dead booth never shakes alone) and sits outside the five fader channels.

Also still consumed as before: platter:state, signal:repair, game:win, quest:reset, player:emote, workshop:*, block:*, player:*.

Events emitted

  • audio:beat { energy } / audio:bar { bar }while inside a pocket these come from the pocket groove's grid (acid 118 / dub 74 / disco 122); the booth groove's emission is suppressed for the duration so two tempos never fight. Lane M's disco sequence should simply listen to audio:beat as briefed — inside the loft it will be the loft's 122 BPM pulse, including in a dead booth.

What I parse (index conventions)

The typed bus payload for machine:interact has no index field, so I resolve the index in this order: a runtime index property if cast in; else trailing digits of machineId; else trailing digits of action; else a sane fallback (first unlocked band / tile 0). Lane M's worlds/common.ts interactAt() supplies BOTH the runtime prop and machineId digits — fully compatible; nothing further needed.

Lane M coordination — all asks already satisfied in their WIP (verified in-tree)

  1. disco_step (player steps a tile → fifth-down echo): not in the briefed vocab, but Lane M's discoQuest.ts already emits it. If that ever changes, steps just go silent — nothing breaks.
  2. Boot handshake for stamps: their Stamps.apply() is idempotent and re-emits stamp:got per stamped world on join snapshots (and stamp:all when the third lands, join included). Exactly what I assume: I dedupe thunks per world per session, the golden riser fires once per session, and any pre-gesture (pre-audio.init) stamp events arm the bonus stem silently. A won crate on a fresh boot = bonus stem armed, no riser replay.
  3. dub_drop on a chasm fall: their dubQuest.ts emits it. Wet falls back as intended.

Notes for the integrator

  • Nothing in src/main.ts changes; window.TURNCRAFT.audio gains no new required calls. New public surface: playSfx names portalIn/portalOut/stampThunk/goldenRiser/bunny (demo/testing sugar).
  • The win timeline / reset ritual / heartbeat behaviors are byte-compatible when no SIDE B event ever fires.
  • Verified live in demo-audio.html (SIDE B panel drives the real bus events) and in the real game on the turncraft-laneB config (port 5184) by emitting the same events from the console — see the demo's header comment for the by-ear checklist. Zero console errors from audio code. (During shared-tree testing a ws://localhost:8433 error appears when the relay isn't running — that's NetClient/Lane M territory, not audio.)