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