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