TURNCRAFT/docs/LANE_D_update.md
jing 9c668d340c Headshell Workshop + Glow-Up phase: assembly minigame, art pass, review fixes
- 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>
2026-07-14 21:32:07 +10:00

169 lines
11 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.

# LANE D — Machines, Interaction & Quest — Progress Update
**Status: complete.** All brief tasks (D1D6) implemented, `npm run typecheck`
clean, every acceptance criterion verified at runtime. Adversarially reviewed:
6 confirmed defects found (incl. one quest deadlock) — **all fixed and
re-verified**. Ready for integration.
_For Fable / the integrator. Companion doc: [`src/machines/HANDOFF.md`](../src/machines/HANDOFF.md) (API surface + contract friction)._
---
## What I built
Everything that moves or responds in the booth, plus the interaction layer and
the quest.
| Area | Files | Delivers |
|---|---|---|
| Framework (D1) | `machines/machine.ts`, `machines/util.ts` | `MachineBase`, block-tint materials, procedural record texture |
| Platters ×2 (D2) | `machines/platter.ts` | rotating ride cylinder, `velocityAt = ω×r`, spin-up/brake, `platter:state` |
| Tonearms ×2 (D3) | `machines/tonearm.ts` | pivoting arm, cue→inward-track→return, needle-plow headshell collider, stylus socket |
| Faders + buttons (D4) | `machines/faders.ts`, `machines/buttons.ts`, `machines/rca.ts` | pitch/channel/crossfader sleds, start-stop/33-45/cue/power, RCA plug |
| Quest (D6) | `machines/quest.ts` | `SIGNAL_NODES` state machine, `signal:repair` / `game:win` |
| Assembly | `machines/index.ts` | `createMachines(world, questPos?) → MachineSet` (wires everything) |
| Interaction (D5) | `interact/raycast.ts`, `interact/interaction.ts`, `interact/hotbar.ts` | DDA+analytic raycast, break/place/use, 9-slot hotbar |
| Demo | `demo-machines.html`, `demo/machinesDemo.ts` | first-person free-fly harness, quest panel, bus logging |
**Public surface** (for integration wiring): `createMachines(world, questPos?)`
returns `{ machines, quest, tonearmA, getColliders(), update(dt), getObject3Ds(),
attachPlayer(player) }`; `new Interaction(world, player, machines, hotbar)`;
`new Hotbar()`. Full details in HANDOFF.md.
## Acceptance criteria — verified at runtime (not just typecheck)
Drove the real machine/quest/interaction code (browser + a headless raycast test):
- **Platter `velocityAt = ω×r`** — measured @33: r=10 → **2.08 v/s**, r=41 → **8.52 v/s**;
@45: r=41 → **11.57 v/s**; `v·r == 0` (perfectly tangential); **0 when stopped**.
Matches the DESIGN targets (label ~2, rim ~8.6/11.6) exactly. Exponential
spin-up/brake confirmed.
- **Tonearm tracks inward** — headshell radius-from-spindle **42 (rest) → 35 (cue)
→ 24.6 (after 80 s tracking)**; headshell collider carries real velocity during
the cue (the plow pushes).
- **Faders operable; crossfader gated** — crossfader refuses to move while its
slot holds dust; after mining the plug, one full slide **repairs `crossfader`**.
- **DDA raycast, no tunneling** — 8/8 headless assertions: correct face/distance,
`maxDist` respected, a shallow diagonal does **not** tunnel a wall, ray↔cylinder
and ray↔aabb hit, and **a nearer machine collider beats a farther block**.
- **Quest end-to-end** — `game:win` fires **exactly once**, both platters
**auto-start on win**, `power` is refused until the other four are done,
re-repairing a node is a no-op (exactly 4 repair events for 4 fixes).
- No console errors across the session; `npm run typecheck` and `vite build` clean.
## Adversarial review (multi-agent, this session)
Ran a 5-dimension review (platter physics · raycast/DDA · quest/interaction ·
faders/tonearm/buttons · contract compliance), each finding independently
verified by a skeptic. **It found 6 confirmed defects — all now fixed and
re-verified through the real code paths**, plus 1 refuted nit. This caught a bug
my own runtime pass missed because I'd cleared the crossfader dust with a direct
`setBlock` instead of mining it.
| # | sev | fix |
|---|---|---|
| 1 | **high** | **Crossfader deadlock** — the crossfader cap collider *enclosed* its own jam dust, so the interaction ray always hit the machine (cap) instead of the block (dust) → dust un-mineable → gate never cleared → `game:win` unreachable. Fixed by excluding Fader caps from the interaction ray (faders are push-operated, never aimed at). Re-verified: dust now mines via the real ray+hold path → crossfader repairs. |
| 2 | **high** | **Tonearm phantom collider** — the arm was one AABB bounding pivot→head, ballooning into a ~700-voxel² box over the rideable record when diagonal. Replaced with a 3-segment AABB chain tiling the tube (~126 voxel²; a far ride point is now outside all arm colliders). |
| 3 | med | Mixer channel-fader / crossfader coords were hardcoded — now derived from `LAYOUT.mixer` and `pos.crossfaderSlot` (keeps gate + geometry coincident). |
| 4 | low | `Platter.velocityAt` returned a fresh array each call (physics hot path) — now writes a reused per-collider scratch (documented "consume immediately"). |
| 5 | low | Tonearm allocated a `THREE.Vector3` per tick — hoisted to a module scratch. |
| 6 | low | `raycast` allocated short-lived arrays per collider — `rayAabb`/`rayCylinder` unrolled. |
| — | refuted | A stopped deck could re-emit a byte-identical `platter:state`; verified harmless (brief says "emit on play/speed change"), but I deduped it anyway. |
Re-verified after the fixes: velocity exact (2.094 / 8.587, tangential=0),
`game:win` once, both platters auto-start, crossfader repairs via real mining,
tonearm still tracks inward, raycast 8/8, no console errors, typecheck clean.
## What the integrator needs to know (3 friction points)
1. **`createMachines` takes an optional 2nd arg** `questPos: Partial<QuestPositions>`.
Pass Lane C's `QUEST_POS`; omit it and LAYOUT-derived defaults are used.
`QuestPositions` is defined in `machines/quest.ts` — Lane C's
`worldgen/questPositions.ts` should export that shape (I never import Lane C).
2. **Call `machines.attachPlayer(player)`** right after building the Lane B player
(a new step ~6.5 in INTEGRATION). Faders (walk-to-push) and walk-press buttons
need the player view, which doesn't exist at `createMachines` time.
3. **Quest item blocks** (`QUEST_ITEMS`): stylus pickup = `chrome` (11), fuse
pickup = `glass` (22). Lane C should place these in the PCB-Depths parts bin.
No contract files were edited; no other lane's concrete code is imported; all
positions derive from `LAYOUT`/`PLATTER`. One small addition to the event surface
was avoided — hotbar changes use `hotbar.onChange(fn)` (the core bus has no
hotbar events by contract); Lane E's HUD subscribes to that.
## Suggested next steps (for Fable to assign)
- **Integration**: wire `src/main.ts` per INTEGRATION.md; add the `attachPlayer`
step; confirm Lane C's `QUEST_POS` matches `QuestPositions`.
- **Cross-lane check with C**: my default quest/gear positions come from `LAYOUT`;
once C's booth lands, verify the crossfader slot span, fuse-box voxel, RCA
plug/socket, and headshell socket line up with C's actual geometry.
- **Cross-lane check with E**: confirm Lane E listens to `platter:state`,
`signal:repair`, `game:win`, `machine:interact`, `fader:move`, and renders the
`Hotbar` via `onChange`.
- **Optional polish** (not blocking): per-deck tonearm side-swing tuning once C's
plinths exist; a `unlocked45` gate if DESIGN's "45 unlocked at win" is taken
literally (I kept 45 always-on for the INTEGRATION rim-launch smoke test).
---
### Notes / repo state
- Local tree is **not a git repo** (`git status` → not a repository). I did not
init or push to `ssh://git@100.71.119.27:222/monster/TURNCRAFT.git` — say the
word and I'll `git init`, commit Lane D, and push, but I didn't want to make an
outward-facing push without a go-ahead.
- Demo dev server was on `localhost:5180` during verification (now can be stopped).
---
## Headshell Workshop (later brief — `docs/briefs/WORKSHOP_CARTRIDGE.md`)
**Status: complete.** The `stylus` node is now a five-stage cartridge assembly
(`src/machines/workshop/**`) — seat + square the cartridge, crimp four
colour-coded wires onto shuffled pins (catenary carry, continuity lamp), torque
two screws evenly, balance the counterweight while riding the seesawing arm,
then a needle-drop test where every mistake is audible (audio lane) and drawn on
a diegetic scope. Multiplayer-shared, last-write-wins. `npm run typecheck`
clean; boot + full flow driven with zero console errors.
**Coordinated split:** the audio + HUD side (`testSignal.ts`,
`AudioEngine.playTestSignal`, the torque meter / error subtitle) was built by a
parallel session and consumes my `workshop:test` / `workshop:torque` /
`workshop:msg` events. I own the machine/state/interaction/net/relay/main side.
**Wiring:** `workshop:*` events (`core/events.ts`), the `{t:'workshop',s}` relay
message (`server/relay.mjs` `validWorkshop` + `net/NetClient.ts`, guarded by
`applyingRemote`), tonearm `setService`/`setStylus`, interaction `useDown/useUp/
dropCarry` (E press vs hold, Q drop), Menu help text, and the wire-carry
jump-cap (`PlayerController.jumpScale`, driven from `main.ts`). Full contract
list in `docs/INTEGRATION_NOTES.md` → "Headshell Workshop".
**Verified at runtime** (headless drive via `window.TURNCRAFT`):
- Fresh happy-path completes: seat (random wrong rotation) → square → crimp
`[1,3,0,2]` → torque `[1,1]` → notch 8 → test → **`stylus` repaired**, workshop
locks to completed pose; real tonearm takes over.
- Error classifier maps all six modes exactly (verified in the prior pass).
- Jump-cap: carrying wire index `0``jumpScale 0.5` (take-off 9.0→4.5); the
`!== null` guard correctly treats the falsy index 0 as "carrying".
- Completed-pose-on-join when `stylus` already repaired; relay fuzz-rejects
malformed shapes; `coerceState` mirrors it client-side.
**Adversarial review — 2 fixes applied this pass:**
1. *(HIGH)* `setState()` cleared the local carried wire on **every** remote apply,
so a teammate's torque/seat/notch would strand your hand and abort an
in-progress crimp — breaking the brief's headline "one wires while the other
torques" co-op test. Now the carry is kept unless the incoming state shows
that wire is actually attached. Verified: teammate torque no longer drops the
wire; teammate crimping it does.
2. *(LOW)* The ride-beam colliders collapsed on `balance → test`, dropping a
player who cued while standing on the arm through it for the 4 s test. The
beam now stays solid through the test.
**Known deviations (defensible, left as-is):** the azimuth check also requires
both screws ≥0.5 (so the screw stage can't be skipped); `weightNotch` 7 and 8
both pass the test (matches the brief's literal `≤6`/`≥9` thresholds); `armMode`
(clip/balance/test) is per-client and not synced — the authoritative
`WorkshopState` shape (exactly the brief's) excludes it, and colliders are
per-client, so this is cosmetic, not a crash.