TURNCRAFT/docs/briefs/WORKSHOP_CARTRIDGE.md
jing b65103ea0e Briefs: Headshell Workshop (cartridge assembly) + Glow-Up art pass
Fresh-eyes review outcomes as two parallel-safe Opus briefs:
- WORKSHOP_CARTRIDGE.md: the stylus node becomes a five-stage cartridge
  assembly - seat/rotate the cart, crimp four color-coded wires onto
  stubborn pins (catenary carry, continuity lamp), torque screws evenly
  (tilt = azimuth error), balance the counterweight while RIDING the
  seesawing tonearm, then a needle-drop test where every mistake is
  audible + drawn on a diegetic scope. Multiplayer-shared state.
- GLOWUP_ART.md: atlas v2 (32px, tile variants), selective LED bloom,
  screen-print decals + party flyers, mixer-face density, PCB pagodas.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 11:24:29 +10:00

9.2 KiB
Raw Blame History

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):

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 (13).
  • 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). 910 = 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.