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>
138 lines
7.0 KiB
Markdown
138 lines
7.0 KiB
Markdown
# LANE D — Machines, Interaction & Quest
|
||
|
||
**Read first:** `docs/DESIGN.md` (§5–§6), `docs/CONTRACTS.md`, everything in
|
||
`src/core/`.
|
||
**You own:** `src/machines/**`, `src/interact/**`, `src/demo/machinesDemo.ts`,
|
||
`demo-machines.html`.
|
||
**You may not touch anything else.**
|
||
|
||
## Mission
|
||
|
||
Everything that moves or responds: the rotating platters + records, the
|
||
tonearm, faders, buttons — all as `IMachine`s with `KinematicCollider`s —
|
||
plus the interaction layer (raycast, break/place, hotbar model) and the
|
||
"Bring Back the Mix" quest state machine.
|
||
|
||
## Provides
|
||
|
||
- `createMachines(world: IVoxelWorld): MachineSet` where `MachineSet` has
|
||
`machines: IMachine[]`, `getColliders(): KinematicCollider[]` (flattened,
|
||
for Lane B), `update(dt)`, `getObject3Ds(): unknown[]`.
|
||
- `Interaction` — constructor `(world, player: IPlayerView, machines,
|
||
hotbar)`: raycast + break/place + use-key dispatch.
|
||
- `Hotbar` — 9-slot inventory model (counts per BlockId, active slot). Pure
|
||
model + events; Lane E renders it.
|
||
- `Quest` — the signal-chain state machine.
|
||
|
||
## Consumes
|
||
|
||
`IVoxelWorld`, `IPlayerView` (interfaces), `bus`, constants (`LAYOUT`,
|
||
`PLATTER`, `PLAYER.reach`, `SIGNAL_NODES`). At integration you'll also read
|
||
`QUEST_POS` from `src/worldgen/questPositions.ts` — code against a
|
||
`QuestPositions` shape you define locally and have the demo supply mock
|
||
coords; take real positions as a constructor arg, don't import Lane C.
|
||
|
||
## Tasks
|
||
|
||
### D1. Machine framework
|
||
- `IMachine` base helper: owns a THREE.Group, colliders, interact zones.
|
||
Machines are built from simple Three.js primitives (cylinders, boxes) with
|
||
MeshStandardMaterial matching block tints from `blocks.ts` (visual
|
||
consistency with the voxel world; grab tints via `blockDef`).
|
||
|
||
### D2. Platter machine (×2, the star)
|
||
- Visual: layered cylinders at the spindle from `LAYOUT` — platter
|
||
(`PLATTER.radius`, brushed dark alu, strobe-dot ring: small emissive red
|
||
studs around the rim), slipmat, record (`recordRadius`, translucent blue
|
||
on deck A / black on deck B, cream label with a procedurally-drawn canvas
|
||
label texture — have fun: fake catalog number "TC-001 · TURNCRAFT"),
|
||
chrome spindle pin. Fine groove rings via a canvas texture on the record
|
||
top face.
|
||
- Simulation: angular velocity → target RPM (`rpm33`/`rpm45` × pitch
|
||
multiplier), exponential spin-up/brake with `PLATTER.spinUpSeconds` (the
|
||
Technics lurch). Rotate the record+platter group.
|
||
- Collider: one `cylinder` `KinematicCollider` (radius, halfHeight matching
|
||
platter+record stack); `velocityAt` returns tangential velocity ω×r (zero
|
||
y). When stopped, velocity zero.
|
||
- Emits `platter:state` on any play/speed change.
|
||
|
||
### D3. Tonearm machine (×2)
|
||
- Visual: chrome tube from the pedestal (LAYOUT-derived) arcing over the
|
||
record, counterweight, headshell. Deck A's headshell has an EMPTY stylus
|
||
socket until the quest fixes it (then a chrome stylus appears).
|
||
- Behavior: rest position off-disc; when deck playing *and* stylus repaired,
|
||
swings to outer groove then tracks slowly inward (one full traversal ≈ 4
|
||
min, then auto-return). The whole arm is a slow kinematic `aabb` collider
|
||
chain (2–3 segments is enough); the headshell is the "needle plow" — its
|
||
collider pushes the player along.
|
||
- Interact on headshell with stylus block selected in hotbar → consumes it,
|
||
repairs `stylus` node.
|
||
|
||
### D4. Fader & button machines
|
||
- **Pitch fader** (per deck): sled (fader_cap visual) sliding in Lane C's
|
||
canyon slot; player pushes it by walking into it (kinematic aabb that
|
||
yields — clamp to slot, 11 detents); position → pitch multiplier 0.5–1.5,
|
||
emits `fader:move`.
|
||
- **Channel faders ×4 + crossfader**: same sled mechanic on the mixer face.
|
||
Crossfader starts blocked by Lane C's dust plug — the machine checks the
|
||
slot voxels are clear before it can move; when the player mines the dust
|
||
(normal block breaking), first successful full slide repairs `crossfader`.
|
||
- **Buttons**: start/stop plate (E or walk-press: interact toggles deck),
|
||
33/45 rockers, cue lamp, **master power switch** (rear of mixer; inert
|
||
until other 4 nodes repaired — then glows via a swapped emissive material
|
||
and one press triggers the finale). All emit `machine:interact`.
|
||
|
||
### D5. Interaction layer
|
||
- **Raycast**: voxel DDA from eye along lookDir up to `PLAYER.reach` (Amanatides
|
||
& Woo), tested against machine colliders analytically (ray-cylinder,
|
||
ray-aabb); nearest hit wins → `RayHit`.
|
||
- **Break** (LMB hold): breakable check via `blockDef(id).breakable`, break
|
||
time ~0.4 s with a crack overlay hook (emit progress on bus? keep simple:
|
||
Lane E just needs `block:break`), adds block to hotbar, `world.setBlock(AIR)`,
|
||
emit `block:break`.
|
||
- **Place** (RMB): active hotbar block onto hit face, blocked if it overlaps
|
||
the player AABB or a machine collider, emit `block:place`.
|
||
- **Use** (E): machine hit → `onInteract()`; block hit → quest checks (fuse
|
||
socket, RCA plug, patch-bay socket).
|
||
- Selection wireframe box on the aimed block (thin line box, Lane A-style
|
||
visuals not required — a THREE.LineSegments you own).
|
||
|
||
### D6. Quest state machine
|
||
- Tracks `SIGNAL_NODES` repair state; constructor takes quest positions.
|
||
- Node fix logic: `stylus` (D3), `crossfader` (D4), `rca` — the loose gold
|
||
plug is a small machine; interacting shoves it 1 voxel toward the socket
|
||
(3 shoves, clunk-clunk-click) then repairs; `fuse` — interact fuse box with
|
||
fresh-fuse block in hotbar (picked up by breaking the parts-bin fuse) after
|
||
breaking the blown one; `power` — D4's switch.
|
||
- Emits `signal:repair` with running count; when count == total, `game:win`,
|
||
and: both platters auto-start, 45-mode unlocked. Quest state queryable
|
||
(`isRepaired(node)`) for Lane E.
|
||
|
||
## Demo (`demo-machines.html`)
|
||
|
||
A mock flat-world stage (simple `IVoxelWorld` mock + naive box renderer or
|
||
just a ground plane — your call) with: both platters spinning (buttons for
|
||
33/45/stop), tonearm tracking a spinning record, all faders operable via
|
||
click-drag AND a scripted "player AABB" you can drive with arrow keys to
|
||
shove sleds and press plates (no Lane B import — a 20-line mock body),
|
||
quest panel showing node states with buttons to simulate each repair path,
|
||
raycast tester: click to break/place blocks on a small mock wall. Console
|
||
logs every bus event.
|
||
|
||
## Acceptance
|
||
|
||
- [ ] Platters: correct spin-up/brake feel; `velocityAt` returns ω×r
|
||
tangential values (unit-test in demo: log at r=10 & r=41 vs expected)
|
||
- [ ] Tonearm tracks inward over minutes; headshell collider moves with it
|
||
- [ ] All faders/buttons operable; crossfader gated on clear slot voxels
|
||
- [ ] DDA raycast: no misses/tunneling at reach 5; machine hits beat farther blocks
|
||
- [ ] Full quest completable in demo via the real logic (mock positions)
|
||
- [ ] `game:win` fires exactly once; platters auto-start on win
|
||
- [ ] `npm run typecheck` clean; `HANDOFF.md` written
|
||
|
||
## Out of scope
|
||
|
||
Player physics (B — you only consume `IPlayerView` + provide colliders),
|
||
voxel meshing (A), booth geometry (C), all audio/UI rendering (E — you emit
|
||
events; hotbar is a model only).
|