# LANE-JUICE — Audio Engine & UI Widgets (Phase 1, parallel) **Executor:** Opus 4.8. **Branch:** `lane/juice` (from main after Phase 0 merge). **Owns:** `src/audio/`, `src/ui/` + tests. **Prereq reading:** `docs/GAME_DESIGN.md` §5 → `docs/CONTRACTS.md` §4 (beat + audio events) → `LANEHANDOVER.md`. You build the two things every other lane will lean on: the sound of the game and the reusable UI widgets. Everything must work in your own demo scenes with zero imports from door/floor code. ## Deliverable `JuiceDemoScene` (menu key J): buttons that exercise every widget and every SFX, plus the techno engine playing with a big on-screen low-pass cutoff slider and a DOOR/FLOOR toggle. Beat indicator flashes on `beat:tick` and visibly matches the audio. ## Phase-0 reality (read before coding) Phase 0 landed 2026-07-19 — see CLAUDE.md "Dev notes" for the API map and the browser-pane verification trick. Juice-relevant specifics: - `core/StubBeatClock.ts` is the stub you'll replace: it's scene-update-driven and emits `beat:tick {beatIndex, audioTimeMs}`. Your TechnoEngine must emit the same event/payload (audio-clock authoritative) so consumers don't change. - `EventBus.on` returns an unsubscribe fn — widgets should keep them and detach in `destroy()` (see `HudStub` for the pattern your `MeterHud` replaces). - `data/outfits.ts` exports `PALETTE` (name → hex) — use it for widget colours so UI and dolls share one palette. - `door:phoneTheatre` is NOT in the EventMap yet — adding it is a worked example of the CONTRACT CHANGE REQUEST flow; file it in your first handover. - Audio can't be heard in the Claude browser pane — verify SFX/engine by ear in a real browser, and note in your handover what was ear-checked vs only logic-checked. - License note: per docs/ASSETS.md this is a private sandbox — real audio samples are ALLOWED as layers/ambience if synthesis fights you, but the location-filter architecture (music through the lowpass, SFX around it) is non-negotiable. ## Part A — Audio (`src/audio/`) — raw WebAudio, no libraries ### 1. `TechnoEngine.ts` — the looping track Fully synthesized, no samples: kick (sine drop 150→50Hz, ~8ms attack) four-on-the- floor at 126–132 BPM (config), off-beat hi-hat (filtered noise burst), a 2-bar 16th-note bassline (detuned saw through its own low-pass), an 8-bar pad that fades in/out on a slow cycle. Schedule with the **look-ahead pattern** (timer checks every 25ms, schedules nodes ~100ms ahead on `AudioContext.currentTime`) — never schedule audio from rAF or Phaser update. - **Beat authority:** emit `beat:tick {beatIndex, audioTimeMs}` on the EventBus AT SCHEDULING TIME with the scheduled audio timestamp (consumers compare against it — this is what makes the toilet rhythm game honest). On integration your engine replaces `core/StubBeatClock` — same event, so consumers don't change. - **The location filter (the signature):** master bus → `BiquadFilterNode` (lowpass). `audio:location: 'door'` → cutoff ~250Hz + slight volume dip + a touch of muffled reverb-ish feel (a short convolver is optional; the filter alone is 90% of it). `'floor'` → sweep open to ~18kHz over ~600ms. That sweep IS the "walking through the door" moment — make the ramp feel like a door opening, exponential not linear. - AudioContext unlock on first user gesture (browser autoplay policy); engine boots silent until then, beat events start with audio, not before. ### 2. `Sfx.ts` — synthesized one-shots All procedural (noise bursts, filtered clicks, pitch envelopes). Required set, each with a named function: `stampSlam` (heavy thunk + paper snap) · `stampInk` (squelch) · `clickerClunk` (mechanical double-click) · `doorBang` (for the toilet game — must cut through the muffled mix) · `radioStatic` + `radioChirp` · `phoneBuzz` · `ropeUnhook` (chain jingle) · `denyCrowdOoh` (small crowd murmur from filtered noise) · `rainLoop` (ambient, door scene). Route SFX AROUND the location filter (UI sounds are always crisp; only the music is muffled). ## Part B — UI widgets (`src/ui/`) — reusable Phaser containers Each widget: self-contained container class, configurable position, emits/consumes bus events, demo'd in JuiceDemoScene. Style: chunky pixel UI, high contrast, slightly grubby (this club is not clean). 1. **`Stamp.ts`** — THE interaction. `stamp(scene, {text: 'DENIED', onSlam})`: raised stamp follows a short arc, SLAMS with 4px screen shake, ink ring + splatter decal that stays on the patron, dust particles, `stampSlam` + `stampInk` sfx, tiny hang before release. Tune until it feels like closing a argument. Also a green `ADMITTED`-capable variant (config text/colour). 2. **`Phone.ts`** — bottom-corner phone: buzz animation + `phoneBuzz` on `dazza:text`, message preview toast, click → slide-up thread history with typing-dots for incoming. Also the "look at phone" idle interaction (emits `door:phoneTheatre` — add to EventMap as a juice-domain event). 3. **`DialogueBox.ts`** — bottom-third dialogue with typed-out text (per-char tick sfx), speaker tag, 2–4 choice buttons, drunk-typo render mode (letters wobble/swap for drunk speakers). Consumed by both door tests and floor cut-offs. 4. **`Clicker.ts`** — the capacity tally: chunky physical counter, big IN button, small OUT button, mechanical digit-roll animation, `clickerClunk`. Emits `door:clicker`. 5. **`MeterHud.ts`** — Vibe/Aggro/Hype bars listening to `meters:changed`: vibe as a neon sign that flickers when low, aggro as a crowd-temperature strip that pulses when high, heat as 3 licence-stamp slots. Replaces core's HudStub at integration. ## Constraints - No imports from `scenes/door/` or `scenes/floor/`. Widgets learn everything from constructor config + bus events. - New event names go in the EventMap via CONTRACT CHANGE REQUEST if outside the audio domain (`beat:*`, `audio:*` are yours to extend freely). - Pure-logic tests where they exist (beat scheduling math: given BPM + lookahead, assert scheduled beat times; typo-renderer determinism). Audio nodes themselves need only the demo scene — note in handover what was verified by ear. ## Definition of done JuiceDemoScene exercises everything; beat flash visibly locks to audio; the door→floor filter sweep feels right (describe your tuning in the handover); `lint`/`build`/`test` clean; SESSION block appended to LANEHANDOVER.md; branch pushed.