# 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) ```ts // src/machines createMachines(world: IVoxelWorld, questPos?: Partial): 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`. 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.