# BRIEF — The Headshell Workshop *Expands the `stylus` quest node from a 30-second fetch into the game's centerpiece: a full cartridge assembly — four stubborn wires, alignment, screws, and balancing a tonearm you're standing on.* **For one Opus 4.8 session.** Read `docs/CONTRACTS.md`, `docs/DESIGN.md`, `docs/INTEGRATION_NOTES.md` and skim `src/machines/tonearm.ts`, `src/machines/faders.ts` (the push-sled mechanic you'll reuse), `src/interact/interaction.ts`, `src/audio/AudioEngine.ts`, `src/ui/Hud.ts`, `src/net/NetClient.ts`, `server/relay.mjs` before writing code. **You own:** new `src/machines/workshop/**`, plus surgical edits to `src/machines/tonearm.ts`, `src/interact/interaction.ts`, `src/audio/AudioEngine.ts` (additive methods only), `src/ui/Hud.ts` (additive), `src/worldgen/buildBooth.ts` (workbench dressing only), `src/net/NetClient.ts` + `server/relay.mjs` (one new message type), `src/core/events.ts` (adding `workshop:*` events is sanctioned — keep them additive), `src/main.ts` (wiring). Do not restructure existing systems. ## Why this design Real headshell assembly is: 4 colored tag-wires onto tiny cartridge pins, cartridge aligned square in the shell, two screws tightened evenly, then counterweight balancing for tracking force. It's fiddly for full-size humans. At 7 mm tall it's *civil engineering* — and every real-world failure mode has an **audible** consequence, which is this game's language: sound is the win state. Design laws: 1. **Nothing hard-fails.** Every mistake is visible/audible and reversible. 2. **Diegetic feedback.** A continuity lamp, a bubble level, and a scope block tell you what's wrong — no tutorial text walls. 3. **Every stage is multiplayer-shared** — two players can split the work. ## The five stages State machine `WorkshopState` (authoritative shape, also what syncs): ```ts interface WorkshopState { cartSeated: boolean; cartRotation: 0 | 1 | 2 | 3; // quarter turns; 0 = correct (stylus forward) wires: (0 | 1 | 2 | 3 | null)[]; // wire i (R,W,G,B) -> pin index or unattached screws: [number, number]; // 0..1 torque each weightNotch: number; // 0..10 along the arm; 7 = zero-balance, 8 = 2g target tested: boolean; // last needle-drop was clean } ``` ### Stage 1 — Seat the cartridge - The existing parts-bin chrome pickup **becomes the cartridge** (rename in help text; same block item 11, same carry flow). - At the tonearm pedestal (Deck A), the arm rests in a **service clip**: add a `rest_clip` visual on the pedestal and a new Tonearm state `'service'` — arm parked low over the pedestal, headshell at player-reachable height (~`Y_PLINTH_TOP + 3`), colliders inert. - `use` with the cartridge on the headshell (existing route in `interaction.ts`) now **seats** instead of instantly repairing: consume the item, spawn the cartridge body visual (a chunky box ~4×2.5×3 with a cantilever + needle tip) in a **random wrong rotation** (1–3). - E on the seated cartridge cycles rotation 90°. Correct orientation: needle tip glints (small emissive flash) + a soft click. Wrong is allowed — it just can't pass the needle-drop test. ### Stage 2 — The four wires (the iconic bit) - A **wire loom** stands on the pedestal (new worldgen dressing: a small spool rack). Four tag-wires hang from it: red, white, green, blue. - The cartridge back is a wall with **four pin posts**, color-capped: R+ red, L+ white, R− green, L− blue — but shuffled physically (pin *positions* are fixed per world; color caps tell you the truth). - Mechanic: E on a wire end → you're **carrying the wire**; a rendered catenary line (THREE.Line, ~12 segments, sag by distance) trails from the loom to your hand. Walk to a pin, **hold E 1.2 s** to crimp it on — the hold has a wiggle animation (the pin is stubborn). E on an attached wire pulls it off again. Any wire can go on any pin. - **Continuity lamp**: a 4-LED strip block on the loom rack; LED *i* lights green when wire *i* is on its correct pin. (Solvable by sight.) - Carrying a wire caps jump velocity at half (the wire tugs) — release to climb. Dropping (walking > 30 voxels away or pressing Q) returns the wire to the loom. ### Stage 3 — Screws - Two screw posts sit beside the shell slots. **Hold E** on a screw to torque it: a radial HUD meter (new Hud element, bottom-center) fills 0→1 over ~2.5 s. **Shift+hold E** backs it off. - Balance rule: if `|screws[0] − screws[1]| > 0.4` while either is > 0.3, the cartridge **visibly tilts** toward the tighter side + a creak SFX. Finishing tilted (both ≥ 0.9 while tilted) = azimuth error (test consequence below). Even torquing squares it up with a satisfying CLUNK. ### Stage 4 — Balance the arm (the showstopper) - E on the cue lever releases the arm from the service clip → it becomes a **live seesaw**: tilt angle driven by `weightNotch` vs the (now cartridge-laden) head mass. Tail-heavy → arm tips back; nose-heavy → needle dives. - The **counterweight is a push-sled** (reuse the Fader mechanic on a rail along the arm tube, 11 notches): walk into it to shove it. The arm is a kinematic collider chain that **tilts in real time under your feet** — the player typically stands ON the arm while doing this. Physics: update the arm segment colliders' transforms per tick from tilt; Lane B's min-penetration cylinder/aabb resolution handles the ride (see `INTEGRATION_NOTES.md` physics section — verify no slingshot on tilt). - **Bubble level**: a block on the pedestal with an emissive dot that drifts left/right of center with tilt; centered = zero balance (`weightNotch === 7`). One more notch (8) = correct 2 g tracking force (detent click + the dot settles into a marked ring). 9–10 = too heavy. ≤6 = too light. ### Stage 5 — Drop the needle (the test) - E on the cue lever again: arm swings over the (must-be-spinning) record and drops. A **test signal** plays — implement `AudioEngine.playTestSignal(errors: TestErrors)`: a bare kick+bass loop routed through an error chain: - `swapLR` (R/W wires crossed with G/B partners): hard pan flip — ping-pong the kick L/R so it's obvious. - `phaseCancel` (a +/− pair swapped): invert one channel + collapse to mono momentarily — thin, hollow, "wrong" sound. - `azimuthTilt` (screws uneven): one channel −12 dB + light distortion (waveshaper). - `trackingLight` (notch ≤ 6): stutter — gate the signal 80 ms on/off randomly + arm visibly bounces. - `trackingHeavy` (notch ≥ 9): lowpass 1.2 kHz + platter rpm sag −15% (temporarily scale the platter's target omega). - Rotation wrong: needle skates — arm slides straight to the spindle with a zip SFX, test aborts. - The **scope block** near the pedestal (canvas-textured face, ~6×4) draws the verdict: clean sine when perfect; recognizable glyphs per error (mirrored trace = swap, thin double line = phase, sagging line = heavy…). Also emit `workshop:test` with the error list so the HUD subtitle names the problem ("LEFT AND RIGHT ARE SWAPPED"). - All clean → `quest.repair('stylus')` (existing event does the rest: stem unlock, LED trace), plus a confetti burst of tiny screws (reuse `Burst`), and the workshop machines lock into their final pose. The arm then behaves exactly as the pre-existing tonearm (cue/track/return). ## Multiplayer - One new message both ways: `{ t: 'workshop', s: WorkshopState }` — last-write-wins, relayed and stored on the server beside `platters` (validate: shape, enums, numbers in range). Late joiners get it in `hello.state`. - `NetClient`: send on any local state change (reuse the `applyingRemote` guard pattern); apply remote by calling `workshop.setState(s)` which diffs and animates. - If `repaired.stylus` is already true on join, the workshop spawns in its completed pose (cart seated, wires on, weight at 8). ## Quest gating changes - The `stylus` node's help text changes (Menu HELP_HTML): "…find the cartridge in the parts bin, then assemble it at the Deck A workbench — wires, screws, balance. The lamp, the bubble and the scope will tell you what's wrong." - Power-switch gating (`canPowerOn`) is unchanged — `stylus` just takes honest work now. ## Acceptance (verify each in a live browser, and two-tab the sync) - [ ] Full assembly happy-path completes and repairs `stylus`; ~5 min first time, all stages revisitable - [ ] Each of the 6 error modes is audible AND readable on the scope, and fixable without restarting - [ ] Standing on the arm while pushing the counterweight: the seesaw tilts under you with no physics explosions (test at 33 AND stopped) - [ ] Wire carry: catenary renders, jump-cap applies, Q/distance returns it - [ ] Two tabs: one attaches wires while the other torques screws; states converge; joiner sees mid-assembly state correctly - [ ] Completed-workshop pose on join when stylus already repaired - [ ] `npm run typecheck` clean; zero console errors in a full session; relay validates the new message (fuzz: bad shapes dropped) - [ ] Update `docs/INTEGRATION_NOTES.md` with a short section + any contract additions listed ## Out of scope Deck B's arm (stays pre-assembled), anti-skate dial (stretch note only), mobile input, any rework of existing quest nodes.