- 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>
169 lines
11 KiB
Markdown
169 lines
11 KiB
Markdown
# LANE D — Machines, Interaction & Quest — Progress Update
|
||
|
||
**Status: complete.** All brief tasks (D1–D6) 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.
|