# 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.