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>
11 KiB
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 |
new — Stamps 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:
MachineSetgained astamps: Stampsfield.- Portals reach
PlayerController.teleport/isFlyingstructurally viaattachPlayer's object. A player withoutteleport(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}:wmust be exactlyacid|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.resetdeliberately does NOT clearstamps— 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
velocityAtreports 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:exitof 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 +
winAtrewind; reset clears repairs, stamps survive, quest replayable.server/booth-state.jsondeleted 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:all→ golden slipmat; reload → join replay (3×stamp:gotquiet + golden restored ⇒stamp:allfired); full reset→re-win cycle: reset broadcast (byname), 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.htmlclean.
Known edges
- Late-reconnect join replay can land outside Lane S's 4 s grace (above).
- The
stamp:gotHUD 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
stampsin the reset case) + nothing client-side.