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

127 lines
8.0 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.

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