TURNCRAFT/docs/briefs/SIDEB_MACHINES.md
type-two 758e9dc4d3 SIDE B briefs + contracts: crate worlds, per-world quests, stamps
Contract: src/core/worlds.ts (three pocket worlds above the booth walls,
portal/entry/exit anchors, stamp count) + additive world/stamp bus events.
Briefs: SIDEB overview + WORLDS / MACHINES / SOCIAL(ambience) / AUDIO lanes,
rescoped around the already-shipped SOCIAL_RESET (emotes, reset ritual,
avatars) and first-five-minutes (heartbeat, beacons, flythrough) phases.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 14:55:12 +10:00

5.1 KiB
Raw Permalink Blame History

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.