not-tonight/lanes/LANE_JUICE.md
2026-07-19 16:20:53 +10:00

110 lines
6.4 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.

# 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 126132 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, 24 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.