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

11 KiB
Raw Blame History

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 (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-endgame: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 0jumpScale 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.