TURNCRAFT/docs/INTEGRATION_NOTES.md
jing 74482e5693 Glow-up review fix: concentric groove caps on record tiers
Fresh-eyes review of the glow-up found the G5 side-ring texture applied to
whole tier cylinders - linear UVs on the caps rendered as diagonal stripes
across the disc. CylinderGeometry takes [side, top, bottom] materials: sides
keep Lane E's ring texture, caps get a new radial recordCapGrooveTexture()
(machines/util.ts) with ring count scaled per tier radius so groove pitch
stays constant. Also lands the three handoff-doc updates from the phase.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 22:48:32 +10:00

242 lines
15 KiB
Markdown
Raw 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.

# 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, faceTopYdepth+1 … faceTopY1, 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, 19/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 / maxX18, 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. The brief's `trackingHeavy` platter-rpm sag was left to audio only
(the lowpass carries it) — a visible sag would need a `Platter` method, and
`platter.ts` is outside this brief's sanctioned edit list.
**Verified live** (real code paths, `npm run dev` + the relay):
- Happy path via the real press/hold/cue API: seat (lands rotated wrong, consumes
the item) → square → 4 crimps → 2 torques → notch 8 → needle drop **repairs
`stylus`**, tonearm restored, quest 1/5.
- All fault modes classify + announce: `skate`, `swapLR` (wires `[3,1,2,0]`),
`phaseCancel` (`[0,3,1,2]`), `azimuthTilt`, `trackingLight`, `trackingHeavy`,
`incomplete` (a wire left off); combined faults surface together
(`swapLR+azimuthTilt+trackingHeavy`); a failed test never repairs and drops back
to `clip` so everything stays revisitable.
- **Seesaw ride**: standing on `ws_ride3` while sweeping the notch — player motion
**0.075 voxels/tick**, zero xz drift, y tracks the beam (84.64→84.03), finite
throughout, at **both** 33 rpm and stopped. No slingshot.
- Wire carry: catenary (14 pts, sags below the chord), `jumpScale` 0.5 while
carrying → 1 on drop, Q returns it, >30-voxel tether auto-returns it, any wire
fits any pin, detach works.
- **Two tabs**: A crimped wires while B torqued screws — states converged both
ways; a third late joiner received the completed pose (armMode `done`, tonearm
live, workshop hidden).
- Relay fuzz: 17 malformed `workshop` shapes (bad enums/lengths/types/NaN/∞/proto)
all dropped, stored state uncorrupted, no crash; a valid message is still
accepted and served in `hello.state.workshop`.
- `npm run typecheck` clean; zero console errors across three tabs.
(Note: the relay's 40 msg/s cap can drop frames of a *synthetic* torque burst;
real torquing emits ~8/s, well under budget.)
## 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.