Three records in the under-table crate are pressed with whole scenes. E beside
one dives you into its pocket above the booth walls; complete its sound-first
quest to press a STAMP; all three raise the golden slipmat + a permanent bonus
percussion stem. Dust bunnies move in under the table.
- Worlds (Lane W): ACID WAREHOUSE / DUB CHAMBER / DISCO LOFT — sealed themed
pockets, portal rings, exit pads; deterministic, zero light leaks (incl. the
flythrough path), meshes unchanged at 469, build ~760 ms.
- Machines (Lane M): portal dive/exit/safety (membership-based escape catch),
tune-the-303 hold-sweep pedestals, carry-the-charge wobbling spring bridge,
beat-Simon dance floor with glow tiles; stamps store + relay {t:'stamp'}
(default-closed, persisted, join snapshot, fuzzed); golden slipmat; stamps
survive the reset ritual.
- Ambience (Lane S): dust bunnies (wander/flee, zero steady-state allocs),
vinyl-warp portal iris, HUD stamp tray, stamp confetti + golden shimmer.
- Audio (Lane E2): per-world grooves (acid 303 w/ fader-driven bands, dub
feedback-echo + spring, disco pentatonic tiles) over a boothGain
duck-and-handback that can never restore stale state; portal/stamp/golden
SFX; bonus stem.
Post-integration review (2 seam finders, 10 findings, all fixed + verified):
stamp events carry origin (local/remote/replay) so consumers celebrate
proportionately instead of wall-clock guessing — kills phantom join
celebrations, first-person confetti for peers' wins, and audio/visual grace
mismatches; reconnect re-uploads locally-won stamps the server missed;
relay saveState survives disk errors; pocket escape via mined floor counts as
a real exit at any altitude; exit-pad speed gate + dwell stops mid-quest
yank-outs; dive cooldown stops exit/re-dive ping-pong; locked acid bands
need deliberate holds (tap-spam dead); disco foot band excludes jump apex.
Verified: whole-tree typecheck + build clean; per-lane live verification
(27/27 worldgen assertions, relay fuzz, 18-phase audio matrix, fps unchanged);
integrated smoke: dive/exit/safety/stamps/golden + all origin paths live.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
129 lines
7.2 KiB
Markdown
129 lines
7.2 KiB
Markdown
# SIDE B — Lane S handoff (fx/ui ambience)
|
||
|
||
Dust bunnies, the vinyl-warp portal iris, and the HUD stamp tray + stamp
|
||
celebrations. All bus-driven, no imports from any other lane, nothing outside
|
||
`src/fx/**` + `src/ui/**` touched. **Not committed** (integrator commits).
|
||
|
||
## Files
|
||
|
||
| file | change |
|
||
|---|---|
|
||
| `src/fx/bunnies.ts` | NEW — `DustBunnies`: 4 fluff-ball critters on the under-table floor |
|
||
| `src/fx/portalIris.ts` | NEW — `PortalIris`: full-screen 2D-canvas groove-ring iris |
|
||
| `src/fx/FxSystem.ts` | additive — `attachAmbience()`, iris + bunnies update, stamp confetti, `stamp:all` emissive shimmer |
|
||
| `src/ui/Hud.ts` | additive — stamp tray (3 accent dots, top-right under the signal lamps) |
|
||
| `src/fx/HANDOFF.md`, `src/ui/HANDOFF.md` | SIDE B sections appended |
|
||
|
||
## Integrator wiring — ONE line in `src/main.ts`
|
||
|
||
```ts
|
||
// after the player is constructed (mirrors the fx.setBeacons pattern):
|
||
fx.attachAmbience(world, player);
|
||
```
|
||
|
||
That's it. Everything else self-wires to the bus in the existing
|
||
`FxSystem`/`Hud` constructors already called from `main.ts`. Without the line,
|
||
bunnies simply don't spawn (and stamp confetti falls back to the world's
|
||
`entry` anchor); iris + tray + shimmer work regardless.
|
||
|
||
## Events
|
||
|
||
**Consumed (new this phase):** `world:enter`, `world:exit` (iris),
|
||
`stamp:got` (tray dot + pulse, accent confetti at the player),
|
||
`stamp:all` (gold tray rings, booth-wide emissive shimmer via the existing
|
||
`setEmissiveBoost` path, gold confetti at the three crate portal discs).
|
||
|
||
**Emitted:** `machine:interact { machineId: 'bunny', action: 'bunny_flee' }` —
|
||
on the wander→flee transition only, globally rate-limited to 1/s (a 10 s
|
||
continuous chase produces 1 emit, not 10 — the squeak marks the spook, per
|
||
brief "at most 1/s"). Lane E2 maps it to a squeak.
|
||
|
||
## Behaviour details
|
||
|
||
- **Bunnies** (`DustBunnies`): 4 critters, each a single mesh of 8–12 jittered
|
||
dust-grey quads (soft glow-texture alpha, normal blending — 4 draw calls
|
||
total). Homes: 2 in the record-crate area, 1 mixer PCB room, 1 deck-A PCB
|
||
room, leashed to 14 voxels so they stay findable. Wander ≈0.3–0.8 v/s with
|
||
idle pauses; spook at 5 voxels (player must be within 4 y — tabletop players
|
||
don't spook them); flee exactly 8 voxels with ease-out (measured ramp
|
||
7→5.2→3.6→2.4→1.3 v/s), then settle (tremble) and resume. Movement uses a
|
||
`world.isSolid` lookahead: ground must be reachable within ±1 voxel
|
||
(overhangs are never ground — that's what keeps them out of the crate's
|
||
edge-standing records), keep-out rings are steered around, fleeing tries
|
||
±45°/±90° deflections before giving up. If someone builds a block into a
|
||
bunny it pops on top.
|
||
- **Keep-out zones:** the three `WORLD_DEFS[k].portalStand` ±6 rings, the
|
||
parts-bin tray and the fuse-box spot. The last two mirror
|
||
`worldgen/anchors.ts` arithmetic (`M.minX+15/M.minZ+10`, `M.minX+24/M.maxZ-8`)
|
||
derived from `LAYOUT` — same convention as `fx/layout.ts` VU towers. If Lane
|
||
C ever moves the `UNDER` anchors, update `KEEP_OUT` in `bunnies.ts`.
|
||
- **Iris** (`PortalIris`): 400 ms ease-in collapse → 90 ms full black → 400 ms
|
||
release; groove rings drift INTO the aperture on `world:enter`, OUT on
|
||
`world:exit`; accent lip + rings tinted from `WORLD_DEFS[world].accent`
|
||
(warm-white fallback for unknown keys). Pure screen overlay on a
|
||
`position:fixed` canvas at **z-index 9 — under the HUD (z 10)**, so
|
||
start/pause overlays and subtitles are never covered; `pointer-events:none`;
|
||
the camera (incl. `TURNCRAFT_CINE` / the flythrough) is never touched.
|
||
Timeline runs on `performance.now()` inside `fx.update()` (no private rAF
|
||
loop); idle = `display:none` + a single boolean check per tick. Retrigger
|
||
mid-animation resumes from the current coverage (no pop). Canvas re-syncs
|
||
its size on every trigger (guards embedded panes that boot with a 0-sized
|
||
window).
|
||
- **Stamp tray** (`Hud`): three 15 px record dots (with a centre "spindle
|
||
hole") at `top:64px; right:14px`, right under the signal-lamp tracker, in
|
||
`WORLD_KEYS` order with `WORLD_DEFS` accents. Dim ring when not got; accent
|
||
fill + glow when got; scale-pulse on a live `stamp:got`; gold rings on all
|
||
three via `.tc-stamps.all` on `stamp:all`.
|
||
- **`stamp:all` shimmer:** 2.6 s additive boost wave (fast ramp, ~2 Hz glitter
|
||
oscillation, ease-out) through the SAME injected `setEmissiveBoost` path the
|
||
LED pulse uses — composes with (never replaces) the win/reset timelines.
|
||
|
||
## Join-replay coordination (Lane M — please confirm timing)
|
||
|
||
Lane M re-emits `stamp:got` (and `stamp:all` when applicable) from the join
|
||
snapshot to seed state. Both `FxSystem` and `Hud` treat stamp events arriving
|
||
**< 4 s after construction** as replays: state applies (dots light, gold ring
|
||
sticks) but celebrations are suppressed (no pulse, no confetti, no shimmer).
|
||
A genuine stamp can't happen 4 s into a session, so there's no false-suppress
|
||
risk; if the relay handshake can complete LATER than ~4 s after page load, the
|
||
window constant lives in `FxSystem.isJoinReplay()` and `Hud.bornAt` uses —
|
||
bump both. **Verified against Lane M's real relay during this session:** their
|
||
join replay lit all three dots + gold silently; a live re-emit afterwards
|
||
pulsed and threw confetti.
|
||
|
||
## Flag for the integrator — disco `disco_cue` lighting
|
||
|
||
`SIDEB_MACHINES.md` M2 says Lane M emits `machine:interact
|
||
{action:'disco_cue', index}` "so Lane S can light them", but
|
||
`SIDEB_WORLDS.md` says "Lane M lights them" and my brief (`SIDEB_SOCIAL.md`)
|
||
scopes Lane S to bunnies/iris/tray only — and the contract
|
||
`machine:interact` payload has no `index` field. **No disco-tile lighting
|
||
exists in Lane S.** If Lane M's handoff defines an encoding (e.g. index in
|
||
the action string), a small FX reaction can be added on request.
|
||
|
||
## Verification (live game, port 5205, real code paths)
|
||
|
||
- Typecheck: exit 0 (whole tree).
|
||
- Bunnies, 60 sim-seconds sweep: 0 solid-cell penetrations, 0 keep-out
|
||
violations (closest approach 6.14), max ground y=4 (rides copper traces,
|
||
never climbs structures), all 4 wandered 3.9–8.4 voxels. Flee: exactly
|
||
8.00 voxels, ease-out ramp above, settle → wander. Emits: 1 per spook;
|
||
10 s continuous chase → 1 emit. **Zero steady-state allocation: 0 KB heap
|
||
delta over 3600 warm ticks.**
|
||
- Iris phases probed pixel-level on the overlay canvas: 210 ms centre open /
|
||
edges black, 430 ms full black, 700 ms releasing, 900 ms hidden +
|
||
`display:none`; compositing over the game verified by screenshot; HUD stays
|
||
above it.
|
||
- Tray: dots at top:64/right:14 with correct accents; got/pulse/all classes
|
||
exercised by hand-emitted events AND by Lane M's real relay replay
|
||
(silent) + live emits (pulsed). Visually confirmed lit in-game.
|
||
- `stamp:all`: shimmer curve measured through the boost path
|
||
(0.56→1.02→glitter→base over 2.6 s); gold confetti seen over the portal
|
||
discs.
|
||
- **fps unchanged:** warm A/B with forced GPU sync — bunnies visible
|
||
1.124 ms/frame vs hidden 1.094 ms (~890 vs ~914 fps-equiv; delta = 4 tiny
|
||
draw calls); `fx.update` 7.6 µs/tick warm with everything attached; iris
|
||
actively closing ≈ 1.08 ms/frame. All ~9× inside the 100 fps budget.
|
||
- Zero console errors from fx/ui across the session (only the pre-existing
|
||
relay-offline WebSocket retry noise, which predates this phase).
|