TURNCRAFT/docs/SIDEB_M_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

11 KiB
Raw Permalink Blame History

SIDE B — Lane M handoff (portals, world quests, stamps, relay)

Brief: docs/briefs/SIDEB_MACHINES.md Status: All items delivered and verified live in the real game against a real local relay AND against Lane W's landed pocket geometry (their build arrived mid-session; every position below is aligned to their actual voxels, not brief guesses). npm run typecheck clean; zero console errors in the final build; demo-machines.html still runs standalone. Not committed — integrator commits (shared tree).


Files touched (all inside Lane M ownership)

file what
src/machines/worlds/common.ts new — pocket bounds/nearest-world helpers, structural teleport/isFlying access to Lane B's player (no cross-lane import), interactAt() (the index-carrying machine:interact emitter), mulberry32
src/machines/worlds/stamps.ts newStamps store (Set): idempotent apply() emits stamp:got / stamp:all + HUD toasts (suppressed by quiet for join replay); Stamps.coerceKey default-closed wire validation
src/machines/worlds/portal.ts new — three dive plates at the crate discs (E → world:enter + teleport to entry), exit pads (stand 0.25 s → world:exit + teleport to returnPos), y>141 out-of-pocket safety net, proximity HUD prompts, accent glow panes
src/machines/worlds/acidQuest.ts new — Tune the 303: 3 hold-E sweep pedestals, per-entry seeded hidden targets, release-inside-±0.07 locks, fader:move acid_0..2 + workshop:torque gauge
src/machines/worlds/dubQuest.ts new — Feed the Spring: charge carry (wire-carry pattern), 8-segment wobbling kinematic deck overlay (travelling sine, honest velocityAt), pit-fall forgiveness, mid-deck mouth feed
src/machines/worlds/discoQuest.ts new — beat-Simon on the 4×4 tile grid: rounds 4→8, standing-on-tile detection, audio:beat sync + 122 BPM free-run fallback, tile glow plates (lighting is Lane M's, per Lane S's scope)
src/machines/machine.ts + IdInteractions (optional structural per-collider press/hold surface for world machines)
src/machines/index.ts constructs Portals + 3 quests + Stamps inside createMachines; MachineSet.stamps; stamp:all → deck A golden slipmat; re-exports
src/machines/platter.ts + setGoldenSlipmat(on) / isGoldenSlipmat() (gold emissive material swap, idempotent)
src/interact/interaction.ts generalized the workshop's press/hold routing: machines exposing IdInteractions get E with the hit collider id and may run hold-E; with none, behavior is byte-identical to before
src/net/NetClient.ts sends {t:'stamp', w} on local stamp:got (guarded, re-validated); applies remote stamp + hello.state.stamps (quiet) via Stamps.apply
server/relay.mjs one additive message {t:'stamp', w} — see protocol below

No other lane's files were touched. Lane W's geometry constants are mirrored (with file references), not imported — cross-lane imports stay contract-only.

Integrator wiring needed in main.ts: none

Everything rides existing wiring: the world machines are created inside createMachines() (colliders/scene/update/attachPlayer already flow through MachineSet), and the stamp sync lives in NetClient, which already receives machines. Two facts worth knowing, no action required:

  • MachineSet gained a stamps: Stamps field.
  • Portals reach PlayerController.teleport / isFlying structurally via attachPlayer's object. A player without teleport (demo mock) means dives emit events but don't move — harmless in the demo, real game unaffected.

Event vocabulary emitted (for Lanes S / E2)

The machine:interact payloads carry the index BOTH ways E2's handoff asks for: trailing digits of machineId (acid_0..2, disco_tile_0..15) AND a runtime index property (typed-contract-safe via a widened variable — core/events.ts untouched).

event when
world:enter {world} E on a dive plate, before the teleport (S iris, E2 duck+groove)
world:exit {world} exit pad (0.25 s stand) or the y>141 safety net
fader:move {faderId:'acid_0'..'acid_2', value} pedestal sweeps; initial (wrong) values re-emitted on world:enter + once more ~0.35 s later (registration-order proofing)
machine:interact {machineId:'acid_<i>', action:'acid_knob', index} ~6/s while a band sweeps
machine:interact {machineId:'acid_<i>', action:'acid_lock', index} band locked (value snaps to target, so the last fader:move is exactly right)
machine:interact {machineId:'acid', action:'acid_tuned'} all three locked (fires on toy replays too; stamp events do not)
machine:interact {machineId:'dub', action:'dub_pickup' / 'dub_drop' / 'dub_feed'} charge taken / lost (pit fall OR leaving the pocket mid-carry) / fed to the tank
machine:interact {machineId:'disco_tile_<i>', action:'disco_cue', index} sequence playback, one per beat
machine:interact {machineId:'disco_tile_<i>', action:'disco_step', index} player steps a correct tile (E2 echoes a fifth down)
machine:interact {machineId:'disco', action:'disco_miss' / 'disco_lit'} wrong step (gentle reset) / full 8 done
machine:interact {machineId:'portals', action:'portal_dive' / 'portal_exit'} teleports (extra flavor hooks, ignorable)
stamp:got {world, count, total} stamp landed: local win, remote win, and once per stamped world from the join snapshot (idempotent — never re-fires for a world already applied)
stamp:all {} third stamp — exactly on the fresh transition, including when the join snapshot completes the set
workshop:torque {value / null} acid hold gauge (reuses the HUD radial)
workshop:msg {text} prompts/toasts (join-replay stamp toasts suppressed)

Consumed: audio:beat (disco show pacing; free-runs at 122 BPM when silent — the quest can't stall in a dead booth), world:enter/exit (self-wiring).

Join handshake (S §"Join-replay coordination", E2 §"boot handshake"): stamp:got/stamp:all replays fire while NetClient handles hello, i.e. on the ws connect during page boot — measured well inside FxSystem's 4 s celebration-suppression grace. One edge: if the relay is unreachable at boot, the replay arrives on the first successful (re)connect (15 s backoff) and would land OUTSIDE the grace — rare, cosmetic (one extra confetti), noted.

Relay protocol (additive, v3 + stamp)

  • Client → server {t:'stamp', w}: w must be exactly acid|dub|disco (default-closed; anything else dropped). Duplicate = dropped (no rebroadcast). Accept → persist immediately, rebroadcast to other peers.
  • hello.state.stamps: string[] — join snapshot; old clients ignore it, old state files load as no stamps.
  • reset deliberately does NOT clear stamps — stamps are pressed into the records, not the booth (M5). Comment sits in the reset case.

Geometry alignment (to Lane W's landed build)

  • Portals: dive plate hugs each disc's lower rim (probed live: the rim overhangs the stand at eye height — solid y4 to z 35, y5 to z 37), centre [discX+1, 4.0, 34.6], small enough not to wall the crate aisle.
  • Acid: plinth colliders/knobs at world centres [52.5, ·, 44.5/52.5/60.5]; our chrome knob rides ON Lane W's led pip voxel (top face 148), lock lamp above.
  • Dub: deck overlay x 223..226 (centre 224.5), z 40..64, base 147.62 (static deck top 147 — ours never dips below it: 147.07 min, so the ride never pops onto voxels). Mouth = their rca_gold inlay [224,146,52] under our floating deck → feeding is E on the deck within ±3.2 of z 52.5 (the pit's reach geometry makes the bridge the only spot). Charge home [232,146.2,78] in front of their horn (x 232 — their build + ours both keep the horn walk off the exit-pad lane; an earlier centred placement walked players over the pad, found live). Pit fall = feet < 143.6 while carrying (pit walks at 143, canyon at 144 stays safe).
  • Disco: tile centres at Lane W's pitch-8 grid x 384.5/392.5/400.5/408.5, z 52.5/60.5/68.5/76.5 (not the brief's pitch-6 sketch), 5×5 halves. Glow plates hover at y 144.05.
  • Exit pads: trigger centred on the pad's world centre (exitPad+0.5), ±1.3 with a 0.25 s dwell — clipping a pad corner mid-quest can't yank you (the disco far row is 2 voxels from its pad); a 1 s post-teleport cooldown stops re-triggers.

Design decisions worth knowing

  • Acid locks evaluate on hold RELEASE (release inside ±0.07 = lock, value snaps to target). Sweeping through the sweet spot doesn't auto-lock — you tune by ear and let go, radio style. Shift reverses the sweep; ends ping-pong.
  • Quests replay as toys once stamped: same loop, flourish message, no stamp events (Stamps.apply is the single idempotence gate, so E2's "4 bars of glory" only fires on real stamps).
  • Acid targets/values and disco sequences reseed per world:enter; disco sequences avoid immediate repeats (re-entry detection can't see doubles).
  • Dub deck velocityAt reports the true lateral surface velocity of the weave; vertical rides on Lane B's ride-snap (verified 89%+ grounded on the wobble at carry amplitude, worst-case rates ~25× inside the envelope).
  • The safety net skips flying players (dev flight stays usable) and counts as a world:exit of the nearest world so E2's duck always unwinds.

Verification record (real game, port 5194; local relay only)

  • Relay (node scripts, fresh state): stamp round-trip A→B; no sender echo; duplicates dropped; 14-case fuzz (bad types/keys/enums/proto/broken JSON) all dropped with the socket alive; join snapshot carries stamps; persistence + winAt rewind; reset clears repairs, stamps survive, quest replayable. server/booth-state.json deleted before AND after.
  • In-browser (headless-driven via window.TURNCRAFT, real raycast/E/hold routing): all three portals dive with prompts; exit pad + dwell + cooldown; safety net teleports non-flyers and leaves flyers alone; ACID three locks → stamp (exact vocab above, gauge shown/hidden); DUB grounded wobble ride, pickup, canyon-roam safe, pit fall → dub_drop + retry toast, feed → stamp; DISCO wrong-step forgiveness, rounds 4→8 with cue/step/plate lighting → stamp #3 → stamp:allgolden slipmat; reload → join replay (3× stamp:got quiet + golden restored ⇒ stamp:all fired); full reset→re-win cycle: reset broadcast (by name), booth dead, stamps 3/3 + golden intact, dead-booth dive+exit work, re-win with stamps intact. Zero console errors in the final build; demo-machines.html clean.

Known edges

  • Late-reconnect join replay can land outside Lane S's 4 s grace (above).
  • The stamp:got HUD toast for REMOTE stamps shows live (by design — a peer pressing a stamp is multiplayer news); only join-snapshot replays are quiet.
  • If a future phase ever wants stamps resettable, that's a relay change (clear stamps in the reset case) + nothing client-side.