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>
91 lines
5.1 KiB
Markdown
91 lines
5.1 KiB
Markdown
# 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.
|