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

91 lines
5.1 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 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
<NAME> (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<WorldKey>) 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.