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

6.4 KiB
Raw Blame History

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.