TURNCRAFT/src/machines/HANDOFF.md
jing 5a39e3a947 TURNCRAFT: contracts, docs, and all five lane deliverables (pre-integration)
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>
2026-07-13 20:50:56 +10:00

5.3 KiB
Raw Blame History

LANE D — Machines, Interaction & Quest — HANDOFF

Owns src/machines/**, src/interact/**, src/demo/machinesDemo.ts, demo-machines.html. npm run typecheck is clean. Demo runs with zero code from other lanes.

Public API (the 10-line surface)

// src/machines
createMachines(world: IVoxelWorld, questPos?: Partial<QuestPositions>): MachineSet
//   MachineSet = { machines, quest, tonearmA, getColliders(), update(dt),
//                  getObject3Ds(), attachPlayer(player) }
Quest        // .isRepaired(node) .canPowerOn() .repair(node) .count() .isWon() .pos
QUEST_ITEMS  // { stylus: 11 (chrome), fuse: 22 (glass) } — blocks Lane C parks in the parts bin
QuestPositions, NODES, Platter, Tonearm, Fader, Button

// src/interact
new Interaction(world, player: IPlayerView, machines: MachineSet, hotbar: Hotbar)
//   .startMining()/.stopMining() .place() .use() .update(dt) .getObject3D() (selection box)
new Hotbar() // .slots .active .setActive(i) .activeBlock() .add(id,n) .consumeActive(n) .onChange(fn)
raycastVoxel(...) / raycastColliders(...)  // DDA + analytic, exported for reuse

Done (all acceptance criteria verified — see LANE_D_update.md)

  • Platters ×2: rotating cylinder collider, velocityAt = ω×r (tangential, zero-y, zero when stopped). Exponential spin-up/brake. Runtime-measured 33→8.52 v/s @ rim / 2.08 v/s @ r=10, 45→11.57 v/s. Blue/black records with a procedural groove+label canvas texture ("TC-001 · TURNCRAFT"), strobe-dot rim, chrome spindle. Emits platter:state on play/speed/pitch change.
  • Tonearms ×2: rigid arm pivoting about a rear pedestal; cue → inward track (~4 min) → auto-return. Headshell is a moving kinematic aabb ("needle plow") with real finite-difference velocity. Deck A starts with an empty stylus socket; setStylus(true) reveals the needle. Deck B starts fitted.
  • Faders: pitch (retunes its deck), 4 channel faders, crossfader. Pushed by the player walking into the sled (needs attachPlayer), 11 detents, or setValue() for a UI. Crossfader is gated on its slot voxels being clear and repairs crossfader on the first full slide once clear.
  • Buttons: start/stop (E or walk-press), 33/45 rockers, cue lamp (glow + event), master power (inert/dark until canPowerOn(), then glows; press = finale).
  • Interaction: eye-ray DDA voxels (Amanatides & Woo, verified no-tunnel) + analytic ray↔cylinder/aabb, nearest wins. Break (0.4 s hold → hotbar + block:break), place (blocked by player/collider/occupied + block:place), use (routes stylus & fuse via hotbar+quest, else machine.onInteract()). Selection wireframe.
  • Quest: pure state machine over SIGNAL_NODES; signal:repair per fix, game:win exactly once on the fifth, both platters auto-start on win.

Contract friction / deviations (for the integrator)

  1. createMachines takes a 2nd arg questPos?: Partial<QuestPositions>. The brief's Provides line shows createMachines(world); D6 + INTEGRATION both require quest positions, so I made it optional — createMachines(world) works (LAYOUT-derived defaults via defaultQuestPositions()), and integration passes real QUEST_POS. QuestPositions is defined in src/machines/quest.ts — Lane C's src/worldgen/questPositions.ts should export a QUEST_POS of that shape (I do not import Lane C).
  2. MachineSet.attachPlayer(player) is an addition beyond the brief. Faders (walk-to-push) and walk-press buttons need the player view, which doesn't exist at createMachines time. Integrator: call machines.attachPlayer(player) right after constructing the Lane B player (step 6.5). Without it, faders still work via setValue and buttons via E; only walk-interaction is inert.
  3. Quest item blocks (QUEST_ITEMS): the stylus pickup is chrome (11) and the fuse pickup is glass (22). Lane C should place these as the parts-bin pickups. Stylus fit = use on Deck A's headshell holding chrome; fuse = break the blown fuse at pos.fuseBox, then use nearby holding glass.
  4. Hotbar change events use hotbar.onChange(fn) (not the core bus — the bus has no hotbar events by contract). Lane E's HUD subscribes to that.
  5. Emissive pulse hook: I don't touch Lane A materials. The master power / cue lamps swap their own emissive material; Lane E's global emissive boost (setEmissiveBoost) is orthogonal.
  6. 45-mode: kept available at all times (INTEGRATION smoke-test 2 rim-launches at 45 pre-win). DESIGN §6's "45 unlocked at win" is treated as flavor. Flag Platter.unlocked45 exists if you want to gate it.

Stubbed / out of scope (by contract)

  • No player physics, no voxel meshing, no booth geometry, no audio/UI rendering. I emit events; Lane E renders hotbar/lights/sound from them.
  • The demo's MockWorld, naive block viz, and MockPlayer are // DEMO MOCK and never used in integration.
  • Demo exposes window.TURNCRAFT_D (debug handle: machines/quest/step(n)); harmless, demo-only.

Verify

npm run dev → open /demo-machines.html. Header comment in src/demo/machinesDemo.ts lists exactly what to press for each criterion. Note: in a hidden/background tab browsers pause requestAnimationFrame, so the sim appears frozen — use TURNCRAFT_D.step(n) from the console to advance, or just focus the tab.