5.1 KiB
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 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 replacescore/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).
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+stampInksfx, tiny hang before release. Tune until it feels like closing a argument. Also a greenADMITTED-capable variant (config text/colour).Phone.ts— bottom-corner phone: buzz animation +phoneBuzzondazza:text, message preview toast, click → slide-up thread history with typing-dots for incoming. Also the "look at phone" idle interaction (emitsdoor:phoneTheatre— add to EventMap as a juice-domain event).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.Clicker.ts— the capacity tally: chunky physical counter, big IN button, small OUT button, mechanical digit-roll animation,clickerClunk. Emitsdoor:clicker.MeterHud.ts— Vibe/Aggro/Hype bars listening tometers: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/orscenes/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.