TURNCRAFT/docs/SIDEB_S_handoff.md
type-two 33232fdeba SIDE B: the crate worlds — three dive-in pockets, stamps, golden slipmat
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>
2026-08-16 16:22:11 +10:00

129 lines
7.2 KiB
Markdown
Raw Permalink 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.

# 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 812 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.30.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.98.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.561.02glitterbase 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).