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>
8.0 KiB
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 }—worldinacid|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 (110–2600 / 160–3800 / 240–5600 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 foracid*) and no fader-zip spam. Values are remembered across dives and pre-init.machine:interactactions (verified against Lane M'ssrc/machines/worlds/*in-tree — theirinteractAt()both casts a runtimeindexproperty in AND suffixes machineId; either alone satisfies me):acid_lock(their machineIdacid_ped<i>, index prop) — lock chime + that band's shimmer layer (16th-offbeat pings). Locks reset on everyworld:enteracid (targets re-seed per entry).acid_knobandacid_tunedare 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-falldub_dropis 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(machineIddisco_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_litswallowed (the stamp swell follows immediately).portal_dive/portal_exit— swallowed: theworld:enter/world:exitwhooshes 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===totalalso arms the golden unlock (below), sostamp:allis 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 (survivesquest: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 toaudio:beatas 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)
disco_step(player steps a tile → fifth-down echo): not in the briefed vocab, but Lane M'sdiscoQuest.tsalready emits it. If that ever changes, steps just go silent — nothing breaks.- Boot handshake for stamps: their
Stamps.apply()is idempotent and re-emitsstamp:gotper stamped world on join snapshots (andstamp:allwhen 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. dub_dropon a chasm fall: theirdubQuest.tsemits it. Wet falls back as intended.
Notes for the integrator
- Nothing in
src/main.tschanges;window.TURNCRAFT.audiogains no new required calls. New public surface: playSfx namesportalIn/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 theturncraft-laneBconfig (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 aws://localhost:8433error appears when the relay isn't running — that's NetClient/Lane M territory, not audio.)