not-tonight/lanes/LANE_JUICE.md
type-two 5086a325d8 Design docs, build plan, contracts, and lane instructions for NOT TONIGHT
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:48:44 +10:00

90 lines
5.1 KiB
Markdown
Raw 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.
## 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.