- Workshop (WORKSHOP_CARTRIDGE): five-stage cartridge assembly at Deck A — seat/square, crimp four tag-wires, torque screws, ride-the-arm counterweight balance, needle-drop diagnostic with per-fault audio + scope. Relay-synced (per-field co-op merge: held screw + carried wire survive remote state). - Glow-Up (G1-G5): 32px atlas with per-voxel variants, selective LED bloom (quality-gated), screen-print decal system + party flyers, mixer/PCB worldgen density pass, record groove-sheen side texture. - 10 confirmed multi-agent review fixes, incl. co-op screw-stomp soft-lock, ride-snap collider-identity (magnet feet / eaten record-fling), workshop SFX exact-match map (rca_seated hijack), beam ride colliders to the head, double-crimp guard, WIRING INCOMPLETE diagnosis mode, skate-abort timer, completedState wiring, per-tick material churn, trackingHeavy platter drag. - Crossfader playtest fix: slew-limited sled (3.4 v/s) + ribbed grip caps with amber index — the sled reads as a heavy handle, not a teleporting wall. - Demo harnesses: machinesDemo hold-key wiring, playerDemo seesaw phase continuity, audioDemo incomplete fault button. - Workshop sync: exact 1.0 screw endpoint gets its own emit signature. Verified: typecheck + vite build clean, live solo quest smoke, two-client co-op relay smoke (simultaneous torque, no rewind, exact convergence). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
215 lines
13 KiB
Markdown
215 lines
13 KiB
Markdown
# TURNCRAFT — Integration Notes (what the integrator changed and why)
|
||
|
||
Integration performed per [INTEGRATION.md](./INTEGRATION.md) after all five
|
||
lanes landed. Every cross-lane glue decision is listed here.
|
||
|
||
## Contract changes (core is integrator-owned)
|
||
|
||
1. **`Y_RECORD_TOP`: 85 → 81** (`src/core/constants.ts`). At 85 the vinyl
|
||
surface was a 5-voxel cliff above the plinth (jump height ≈ 1.3 — the
|
||
record was unreachable), and Lane D's platter stack floated on the plinth
|
||
top, ignoring the 6-deep well Lane C carved. Now the record sits 1 proud of
|
||
the plinth (auto-steppable, like a real 1200) and the platter body fills
|
||
the well. Everything downstream (tonearm heights, audio panner positions,
|
||
FX rim sparkle, D's quest defaults) derives from the constant and moved
|
||
automatically. Added `PLATTER.wellDepth = 6` alongside.
|
||
2. Lane B's requested `velocityAt` out-param overload was NOT taken: Lane D
|
||
already returns a reused per-collider scratch array (no allocation), which
|
||
satisfies the hot-path concern with no contract change.
|
||
|
||
## Cross-lane glue (in `src/main.ts`)
|
||
|
||
- **QuestPositions adapter**: Lane C's nested `QUEST_POS` is mapped to Lane
|
||
D's flat `QuestPositions`; the crossfader slot span is derived from C's
|
||
`MIXER` anchors (centerX ± halfLen, faceTopY−depth+1 … faceTopY−1, z −1…+2)
|
||
so D's slide gate covers exactly the tram volume C carved.
|
||
- **`machines.attachPlayer(player)`** called after the player is built
|
||
(Lane D's added step "6.5") — enables walk-push faders and walk-press plates.
|
||
- **Hotbar**: D's `Hotbar` structurally satisfies E's `HotbarModel`; passed
|
||
directly as `getHotbar`, with `hotbar.onChange(() => hud.refreshHotbar())`.
|
||
- **Input wiring**: LMB hold = mine, RMB = place, E = use, 1–9/wheel = hotbar,
|
||
all gated on pointer lock (matching Lane B's own key gating). Start splash
|
||
click → `audio.init()` + pointer lock; resume click → re-lock only.
|
||
- **Fixed-step loop**: machines → player → interaction → fx at `FIXED_DT`,
|
||
with `MAX_SUBSTEPS` catch-up and an accumulator clamp so a backgrounded tab
|
||
doesn't spiral; `audio.updateListener` + `chunks.update()` + render per frame.
|
||
- Debug handle `window.TURNCRAFT` (world/player/machines/quest/hotbar/audio/
|
||
fx/hud/chunks/interaction) — used by the smoke tests, harmless in play.
|
||
|
||
## Lane-file adjustments (documented per CONTRACTS "integrator resolves")
|
||
|
||
- `src/machines/platter.ts` — platter body now spans the well floor to 1
|
||
below the vinyl (visuals) and its collider spans well floor → `Y_RECORD_TOP`
|
||
so the spinning body also pushes the player out of the well's 2-voxel gap
|
||
ring (which is now a real, intentional fall-in hazard).
|
||
- `src/fx/layout.ts` — VU overlay sprites aligned to Lane C's physical LED
|
||
towers (x = mixer minX+18 / maxX−18, 12 segments, in front of the open
|
||
casing face) per Lane E's friction note #5.
|
||
- `src/interact/interaction.ts` — per-material break times (soft 0.12 s,
|
||
glass 0.25 s, default 0.4 s). The crossfader jam is ~240 dust blocks; at a
|
||
flat 0.4 s it was a 96-second chore.
|
||
- `docs/DESIGN.md` — elevation table updated for the Y_RECORD_TOP change.
|
||
|
||
## Deviations accepted as-is
|
||
|
||
- **45 speed available pre-win** (D's reading; DESIGN's "45 unlocked at win"
|
||
treated as flavor). `Platter.unlocked45` exists if we ever gate it.
|
||
- **`fader_cross` does not drive audio** (E maps pitch + channel digits only);
|
||
crossfader is quest/physical-only in v1.
|
||
- **Both deck pitch faders drive the one global mix pitch** (E has a single
|
||
transport; fine until per-deck stems exist).
|
||
- **Tonearm pedestal (C's voxels) vs D's arm pivot** are ~11 voxels apart —
|
||
cosmetic misalignment, queued for polish, gameplay unaffected.
|
||
|
||
## Verified at integration (live browser, real code paths)
|
||
|
||
- Boot: booth builds + meshes in ~670 ms dev / 46 modules build in 0.5 s;
|
||
fillBox fast path ENABLED; **120 fps**; zero console errors all session.
|
||
- Ride: standing on the record at 33 → clean circle, r drift 20.00→20.02 over
|
||
4 s, y = 81 exactly, spin-up ramp visible; at 45 near the rim → 10.7 v/s
|
||
(= ω×r), jump lands back on the moving disc.
|
||
- Mining: real DDA aim → dust breaks into hotbar; crossfader stays gated
|
||
while dust remains.
|
||
- Quest chain: stems stack 0→5 per `signal:repair`, power switch arms exactly
|
||
after the 4th repair, real button press → `game:win` once, both platters
|
||
auto-start, HUD banner + tracker 5/5, AudioContext running.
|
||
- The platter-well gap ring behaves as a trench (collider pushes, no clipping).
|
||
|
||
## Concept-art pass (post-integration, from John's Gemini mockup)
|
||
|
||
- **Terraced records**: the flat disc became a stepped "groove amphitheater" —
|
||
3 vinyl tiers rising 1 voxel each to the label plateau, then a 4-tier
|
||
stepped spindle tower with an amber lamp (all in `TIERS`, `platter.ts`).
|
||
Each tier is its own stacked kinematic cylinder; you walk/spiral up a
|
||
SPINNING staircase. Tonearm raised (`HEAD_Y` +2.6) and `R_INNER` pulled to
|
||
16 so the needle clears the terraces and never hits the tower.
|
||
- **Player physics upgrades this required** (`PlayerController.ts`):
|
||
cylinder contacts now resolve by minimum penetration (up vs radial), which
|
||
fixes a latent slingshot bug (feet dipping below a disc's top while deep
|
||
inside its footprint used to teleport the player radially to the rim);
|
||
1-voxel collider lips auto-step like voxel ledges; cylinder tops have a
|
||
half-foot radius grace so you can stand on rims.
|
||
- **Well seam**: the platter well's outer ring was a 7-deep inescapable
|
||
trench; floor raised to 1 below the plinth top (worldgen) — now a Technics
|
||
seam you step out of.
|
||
- **Room feel**: plywood ceiling with six recessed amber lamp panels
|
||
(worldgen) + matching warm PointLights (main.ts), warmer/brighter
|
||
hemi/ambient/fog (renderer.ts).
|
||
- **Mixer color pop**: knob caps get a deterministic palette scatter (red/
|
||
white/gold/LED) in `buildMixer`. **Rim crumble**: vinyl chips/dust/screws
|
||
scattered around the well mouths in `buildDeck`.
|
||
- Dev nicety: `window.TURNCRAFT_CINE = { pos, look }` detaches the camera
|
||
for screenshots/trailers; `window.TURNCRAFT` exposes camera/render too.
|
||
- Verified: walk-in from the rim spirals up to the label plateau at 33 rpm
|
||
(fighting the spin is intentional physics — stop the deck for an easy
|
||
climb); tower fully climbable (83→87 by walking); seam escape 1-step;
|
||
121 fps with ceiling + lights; quest/win chain re-verified end-to-end.
|
||
|
||
## Headshell Workshop (Lane D) — wiring & contract additions
|
||
|
||
The `stylus` node is now a five-stage cartridge assembly at the Deck A pedestal
|
||
(`src/machines/workshop/**`). One machine, `machines.workshop` (a `Workshop`),
|
||
owns the visible/collidable assembly arm; the real `Tonearm` sits in **service
|
||
mode** until the node is repaired, then takes back over (cue/track/return).
|
||
|
||
**Events added** (`src/core/events.ts`, additive — sanctioned by the brief):
|
||
- `workshop:test { errors: string[]; clean: boolean }` — needle-drop verdict;
|
||
the HUD names the first error. `errors` are `TestErrors` keys in `ERROR_ORDER`.
|
||
- `workshop:torque { value: number | null }` — radial HUD meter 0..1 while a
|
||
screw is being torqued; `null` hides it.
|
||
- `workshop:msg { text: string }` — transient HUD subtitle (stage prompts).
|
||
|
||
The audio + HUD lanes consume these; Lane D only emits them (never renders the
|
||
meter/subtitle itself).
|
||
|
||
**Multiplayer** — one new relay message both ways: `{ t: 'workshop', s: WorkshopState }`,
|
||
last-write-wins.
|
||
- `WorkshopState` (in `workshop/types.ts`) is JSON-primitive and range-bounded:
|
||
`{ cartSeated, cartRotation:0..3, wires:(0..3|null)[4], screws:[0..1,0..1],
|
||
weightNotch:0..10, tested }`.
|
||
- `NetClient` sends on `workshop.onChange` (guarded by `applyingRemote` so remote
|
||
applies don't echo); remote messages call `workshop.setState(s)` (diff+animate).
|
||
- `server/relay.mjs` `validWorkshop()` validates shape/enums/ranges (clamps
|
||
screws + notch), stores it beside `platters`, persists it, and hands it to late
|
||
joiners in `hello.state.workshop`. `types.ts:coerceState` mirrors this on the
|
||
client. Fuzzed/malformed shapes are dropped on both sides.
|
||
- If `repaired.stylus` is already true on join, the workshop spawns in its
|
||
**completed pose** (`completedState()` — cart seated square, wires on, screws
|
||
tight, notch 8) and the real tonearm is live.
|
||
|
||
**Surgical edits to other lanes' files:**
|
||
- `tonearm.ts` — additive `setService(on)` (parks the arm low + inerts its
|
||
colliders while the workshop owns the assembly arm) and `setStylus(on)`.
|
||
- `PlayerController.ts` (Lane B) — one additive knob, `jumpScale` (default 1).
|
||
`main.ts` sets it to `0.5` while `workshop.isCarrying()` is non-null so a
|
||
carried tag-wire tugs the jump to half height. Purely additive; no reorder.
|
||
- `interact/interaction.ts` — `useDown/useUp/dropCarry` route E (press vs
|
||
hold=crimp/torque) and Q (drop wire) to the workshop; `advanceWorkshopHold`
|
||
cancels a hold if aim leaves the target.
|
||
- `ui/Menu.ts` — STYLUS help text + the E-hold / Q controls updated.
|
||
- `worldgen/buildBooth.ts` dressing was intentionally **not** touched: the
|
||
workshop renders all its own meshes (pedestal, loom, pins, scope, level), so
|
||
no worldgen edit is needed and the glow-up lane keeps that file conflict-free.
|
||
|
||
**Deferred / stretch:** anti-skate dial (brief out-of-scope), Deck B arm stays
|
||
pre-assembled.
|
||
|
||
## Physics — riding tilting / vertically-moving kinematic platforms (Lane B)
|
||
|
||
For the Headshell Workshop's Stage-4 seesaw (the tonearm you stand on while
|
||
balancing). Lane B (`PlayerController.ts`) now rides a platform that **moves
|
||
vertically under the player**, not just horizontally. What the workshop lane
|
||
needs to know:
|
||
|
||
- **Model the arm as kinematic colliders updated each fixed tick, before
|
||
`player.update`.** `tonearm.ts` already does this (an aabb chain tiling the
|
||
pivot→head line + a head aabb). For the seesaw, recompute each segment's
|
||
`min.y/max.y` from the tilt each tick and expose a `velocityAt` that returns
|
||
the segment's surface velocity. The loop order `machines.update(dt)` →
|
||
`player.update(dt)` is what makes the ride correct — don't reorder.
|
||
- **Lane B guarantees the ride; do NOT reposition the player yourself.** Just
|
||
move the colliders. The controller:
|
||
- grounds on the single **highest supporting top** under the footprint;
|
||
- a **rising** arm grounds by penetration (it rises into you) and snaps you up;
|
||
- a **tilting/descending** arm keeps you glued via **ride-snap**: if you were
|
||
riding a platform and it drops out from under you, you snap back onto its top
|
||
across a per-tick vertical gap up to `RIDE_SNAP` (0.75 voxel). Gated to a
|
||
downward/level velocity, so it never fights a jump;
|
||
- horizontal surface motion is carried via `velocityAt` (x/z);
|
||
- min-penetration keeps you centered — **no lateral slingshot**.
|
||
- **Safe envelope (measured in `demo-player.html`):** perfectly glued
|
||
(grounded 100 %, feet on the slab) for tilt rates up to ~2 rad/s / ~24 voxel/s
|
||
surface speed — far beyond a counterweight-driven balance (~0.25 rad/s). Only
|
||
absurd >3 rad/s "mousetrap" tilts separate, and then **gracefully** (you slide
|
||
off, no explosion). Verified at platter-33 and stopped (independent colliders).
|
||
- **Verified against the REAL arm** (not just the demo mock): a headless harness
|
||
drove the actual `Workshop.ts` `ws_ride*` geometry — the same `beamY`/`theta`
|
||
tilt math, `RIDE_SEGMENTS=6`, 0.6-voxel-tall slabs — through `PlayerController`.
|
||
Standing on the beam tail (where you shove the sled) while the weight sweeps the
|
||
full notch range **0↔10**: the realistic balance ritual rides **99.6 % grounded,
|
||
`maxYerr` 0.023, zero lateral drift (`maxZoff` 0.000)**, and every transient
|
||
ungrounded tick is re-caught in **exactly 1 tick**. Because the workshop's
|
||
`theta` eases at `dt*5` toward a one-notch (0.025 rad) target, the per-tick
|
||
vertical travel at the player's feet is ≤ ~0.03 voxel — ~25× inside the
|
||
`RIDE_SNAP` budget. Even a physically-impossible instant full-swing-every-tick
|
||
slam only floats the player upward briefly (still `maxZoff` 0, never falls).
|
||
- **Gotchas:**
|
||
- The arm is a *staircase of flat slabs*; you stand at each segment's midpoint
|
||
height. Finer segments = smoother ride. Keep each segment ≤ ~2 voxels tall so
|
||
min-penetration prefers the top (a very tall arm segment could resolve as a
|
||
wall push instead of a floor).
|
||
- `velocityAt`'s vertical magnitude does **not** affect grounding (ride-snap
|
||
handles vertical); a nonzero horizontal component keeps the player tracking a
|
||
sweeping arm. Returning `[0,0,0]` is fine for a purely vertical tilt.
|
||
- Jumping off the arm works and inherits the arm's horizontal carry (the
|
||
record-edge launch path); vertical is not inherited (no surprise pop).
|
||
- **Eyeball it:** `demo-player.html` has a **Seesaw** toggle (oscillates a
|
||
segmented tilting arm) + **Teleport to arm**; stand on it and watch `groundedOn`
|
||
stay `seesawN` as it tilts.
|
||
|
||
## Not yet done (deploy phase)
|
||
|
||
Run `/ship-check` before exposing anything publicly; deploy per deploy-map
|
||
conventions (static `dist/` — 156 KB gzipped). Cloudflare/host wiring is a
|
||
separate step, to be done with the token holder present.
|