# SIDE B — Lane M: portals, world quests, stamps, relay Read `docs/briefs/SIDEB.md` + `src/core/worlds.ts` first — **plus `docs/briefs/SOCIAL_RESET.md` and the SOCIAL_RESET handoffs**: the reset ritual, emotes, and `interaction.setBreakGuard` already exist and are server-authoritative — you build AROUND them, never on them. You own **`src/machines/**`, `src/interact/**`, `src/net/NetClient.ts`, `server/relay.mjs`**. Reuse the proven patterns: interaction tags + press/hold (see `machines/workshop/`), kinematic colliders, `MachineBase`. Keep the Workshop untouched except where noted. ## M1. Portals (src/machines/worlds/portal.ts or similar) At each `WORLD_DEFS[k].portalStand`: an interaction collider ("DIVE INTO (E)" via `workshop:msg`-style HUD text — use `workshop:msg`). On E: emit `world:enter {world}`, teleport the player to `entry` (PlayerController has `teleport`). At `exitPad`: walking onto it (or E) → `world:exit {world}` + teleport to `returnPos`. Falling out of a pocket must be impossible (Lane W seals shells), but add a safety: if player y > 141 and NOT inside any pocket bounds, teleport to `returnPos` of the nearest world (belt-and-braces). While inside a pocket, suppress the booth machines' interactions? NOT needed — distance already prevents it. ## M2. One mini-quest per world (own module per world) All retryable, sound-first, no hard fail. On completion: emit stamp via relay (see M4), `stamp:got {world, count, total}` locally, and a `workshop:msg` line. Already-stamped worlds stay visitable (quest replays as a toy, no double-stamp). - **ACID — Tune the 303.** Three knob pedestals (Lane W builds plinths at x=52, z=44/52/60, pocket-relative — colliders yours). Audio (Lane E2) loops a 2-bar acid line whose filter cutoff is WRONG on entry. Hold-E a pedestal = sweep its band (reuse hold/torque pattern; emit `workshop:torque` for the HUD gauge). Each pedestal has a hidden target 0..1 (seeded per entry); within ±0.07 of target = locked (its pip glows — emit `machine:interact {action:'acid_lock'}`). All three locked → the line snaps into tune → stamp. Emit `machine:interact {action:'acid_knob'}` with each meaningful change; Lane E2 maps value→cutoff via `fader:move {faderId:'acid_0'..'acid_2'}` (reuse that event, values 0..1). - **DUB — Feed the spring.** An echo charge (glowing carryable, like the wire carry — reuse the carry pattern with `isCarrying`-style state) sits at the horn. Carry it across the spring-tank bridge: you overlay a 3-wide moving kinematic deck on Lane W's static chrome deck (segments like the workshop beam, gentle sine wobble ±0.35 rad/s max — the ride-snap handles it). Reach the tank mouth (chasm centre), E to drop → `machine:interact {action:'dub_feed'}` → stamp. Falling into the chasm floor: charge returns to the horn, subtitle "THE SPRING WANTS IT — TRY AGAIN". - **DISCO — Light the floor.** The 4×4 tile grid (Lane W lays it; your colliders detect standing-on-tile). On `audio:beat`, flash a sequence of 4 growing to 8 tiles (`machine:interact {action:'disco_cue', index}` so Lane S can light them); the player repeats it by stepping tiles between beats. Wrong tile = gentle reset ("THE FLOOR FORGIVES — WATCH AGAIN"). Full 8 → stamp. ## M3. Stamps + golden unlock Track stamps in a small `Stamps` store (Set) synced via relay (M4), persisted server-side like repairs. On count===3 (any client): emit `stamp:all`, and swap deck A's slipmat material to a golden variant (you own platter.ts — add `setGoldenSlipmat(on)`) + Lane E2's bonus stem fires. On join, relay state re-applies stamps (idempotent). ## M4. Relay protocol (server/relay.mjs + NetClient) ONE additive message, validated default-closed like `workshop` (emotes and unrepair/reset already exist — touch neither): - `{t:'stamp', w}` — w in acid|dub|disco. Adds to a persisted `stamps` set; rebroadcast; included in the join snapshot. Reject anything else. Keep frame caps. Client mirror: NetClient sends stamps, applies incoming + join snapshot (`stamp` → Stamps store + `stamp:got`, idempotent). ## M5. Coexist with the reset ritual (do not rebuild it) The SOCIAL_RESET reset (5 s fuse hold → server-authoritative reset, 10-min cooldown) already resets the quest + workshop. Your additions must survive a full reset→re-win cycle: **stamps persist through resets** (they're pressed into the records, not the booth — assert this), portals keep working while the booth is dead, and world quests remain playable. Verify one full cycle. ## Verify + handoff Typecheck clean; drive everything headless via `window.TURNCRAFT` in the real game (dev port: launch config `turncraft-laneD-verify`, 5194) with zero console errors: enter/exit each world, complete each quest, stamp sync across a relay round-trip (run `node server/relay.mjs` locally; NOTE: delete `server/booth-state.json` before AND after — stale state makes clients join completed), plus one full reset→re-win cycle with stamps surviving. Fuzz the new relay message (bad kinds/shapes rejected). Handoff: `docs/SIDEB_M_handoff.md` incl. exact event vocab emitted for Lanes S/E2.