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

179 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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** (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.