Lanes A (engine), B (player), C (worldgen), D (machines/quest), E (audio/fx/ui) as landed, each with HANDOFF.md + update docs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
7.6 KiB
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 (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,
maxDistrespected, 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:winfires exactly once, both platters auto-start on win,poweris 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 typecheckandvite buildclean.
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)
createMachinestakes an optional 2nd argquestPos: Partial<QuestPositions>. Pass Lane C'sQUEST_POS; omit it and LAYOUT-derived defaults are used.QuestPositionsis defined inmachines/quest.ts— Lane C'sworldgen/questPositions.tsshould export that shape (I never import Lane C).- 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 atcreateMachinestime. - 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.tsper INTEGRATION.md; add theattachPlayerstep; confirm Lane C'sQUEST_POSmatchesQuestPositions. - 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 theHotbarviaonChange. - Optional polish (not blocking): per-deck tonearm side-swing tuning once C's
plinths exist; a
unlocked45gate 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 tossh://git@100.71.119.27:222/monster/TURNCRAFT.git— say the word and I'llgit 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:5180during verification (now can be stopped).