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

7.2 KiB
Raw Permalink Blame History

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

// 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.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).