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>
This commit is contained in:
jing 2026-07-14 11:24:29 +10:00
parent 7975808c8a
commit b65103ea0e
2 changed files with 289 additions and 0 deletions

111
docs/briefs/GLOWUP_ART.md Normal file
View File

@ -0,0 +1,111 @@
# BRIEF — The Glow-Up (art & atmosphere pass)
*Fresh-eyes verdict: the bones are great, the surfaces are underdressed.
This pass closes the gap to the concept art. Pure art/atmosphere — no
gameplay logic — so it can run in parallel with WORKSHOP_CARTRIDGE.md.*
**For one Opus 4.8 session.** Read `docs/CONTRACTS.md`,
`docs/INTEGRATION_NOTES.md`, `src/engine/atlas.ts`, `src/engine/renderer.ts`,
`src/worldgen/buildBooth.ts`, `src/fx/` first.
**You own:** `src/engine/atlas.ts`, `src/engine/renderer.ts`,
`src/worldgen/**` (dressing additions), `src/fx/**` (decals module),
`src/ui/Menu.ts` (one settings row). **Merge point with the workshop
brief:** `src/main.ts` and `src/worldgen/buildBooth.ts` — keep your edits
small and additive there. Do not touch `src/machines/**`, `src/interact/**`,
`src/audio/**`, `src/net/**`, `server/**`.
## Findings this brief fixes (from the review, with screenshots taken)
1. **Mixer face is an empty black plain** — knob clusters are specks on a
82×95 desert; real DJMs are dense. No screen-print labels anywhere.
2. **PCB Depths read as green wallpaper** — one tile repeated makes
astroturf, not circuitry; caverns lack vertical interest.
3. **Textures are flat at 16px** — brushed_alu reads as concrete speckle
up close; vinyl tiers are matte plastic; no tile variation anywhere.
4. **LEDs don't bloom** — emissive blocks read as flat colored squares;
the concept art's glow is absent.
5. Fader alleys/vents are void-black slots with no depth cues.
## Tasks
### G1. Atlas v2 (engine/atlas.ts)
- Tiles 16→**32 px**, and give patterns **per-block-position variation**:
hash (x,y,z) → one of up to 4 painted variants per pattern (requires
the mesher's UV path to pick a variant — if that's invasive, an
acceptable fallback is 4 variants painted for `pcb` only, chosen by a
world-position hash in the shader patch; document your choice).
- Pattern quality: `brushed` = long directional strokes (not noise);
`grooves` = concentric arcs with a subtle specular line; `pcb` = 34
variants that visually tile into routed traces + via dots + silkscreen
flecks; `plywood` = longer wavy grain, end-grain variant for `ply_edge`.
- Keep `NearestFilter` chunkiness — sharper *pattern*, same voxel soul.
- Material feel: give the two chunk materials a slightly higher
`envMapIntensity`-free polish via roughness tuning only (no env maps).
### G2. Bloom (engine/renderer.ts + main.ts + Menu)
- Selective bloom for emissive: `EffectComposer` + `UnrealBloomPass`
(three/examples), threshold tuned so ONLY LEDs/strobe/lamps bloom
(~strength 0.55, radius 0.4). This is the single biggest step toward
the concept art.
- Gate on quality: add a `bloom` boolean to Settings (default on;
auto-off when quality = 'fast'). `createRenderer` returns a `render()`
that internally uses the composer when enabled — main.ts keeps calling
`render()` unchanged apart from passing the setting through.
- Budget: ≥ 100 fps must survive on the current test machine; measure
before/after and record numbers in your handoff.
### G3. Screen-print decal system (fx/decals.ts + worldgen anchors)
- A tiny module that bakes text/markings onto canvas textures applied as
thin plane meshes (0.05 proud, `polygonOffset` to avoid z-fight).
- Print set (all positions derived from LAYOUT/anchors):
- Mixer face: `CH 14` under each fader alley, tick marks along alleys,
`TURNCRAFT-800` model text at the top edge, crossfader `A ↔ B`.
- Decks: `START·STOP`, `33` `45` beside buttons, pitch `+/` scale,
Technics-style script line on the plinth rear ("Quartz Direct Drive").
- Patch bay: tiny socket numbers. Fuse box: `FUSE 250V`.
- Booth walls: 34 procedural **party flyers** (canvas typography:
random gig names from a word bank, big condensed type, one accent
color each) pasted like posters near the patch bay and front lip.
- Keep total decal draw calls < 25 (merge planes per gear piece where easy).
### G4. Worldgen density pass (buildBooth.ts — additive only)
- **Mixer face**: mushroom knob caps (2×2 cap on 1×1 stem, mixed heights)
replacing the flat single-voxel caps; double the knob rows per channel
strip (EQ + trim + filter row); 1-voxel panel seam grooves (matte_black
→ rubber lines) sectioning the face; LED pips beside each alley.
- **PCB Depths**: IC-chip **pagoda buildings** (matte_black slabs on
solder pin-legs you can walk under, 23 sizes, a few stacked), ribbon
cable walls (alternating cable_black/cable_white columns), more copper
trace inlays connecting features, sparse led_green "status" pips near
ICs (they pulse post-win via existing emissive boost — free).
- **Fader alleys/vents**: a dim led_amber pip at the bottom of each alley
and inside vent mouths so dark slots read as *deep*, not void.
- Keep `buildBooth` deterministic (same seed), build time < 800 ms, and
do NOT move any LAYOUT/QUEST_POS anchor (machines depend on them).
### G5. Record material moment
- The vinyl tier sides get a subtle groove-sheen: a fine repeating
light/dark ring texture on the tier side cylinders (machines/platter.ts
is workshop-owned — instead provide the improved texture via
`blockMaterial`'s existing tint path or hand the workshop session a
`recordSideTexture()` in `src/fx/decals.ts` and note it in your handoff
for them to wire — do not edit platter.ts yourself).
## Acceptance
- [ ] Before/after screenshots of: mixer face, PCB depths, deck top,
full-booth win state (use `window.TURNCRAFT_CINE`)
- [ ] Bloom on LEDs only (plywood must NOT glow), toggleable, fps ≥ 100
measured and recorded
- [ ] All decals legible at play distance, no z-fighting while circling
- [ ] buildBooth deterministic, anchors untouched (grep QUEST_POS usage),
build < 800 ms, chunk meshes < 600
- [ ] `npm run typecheck` clean, zero console errors, live smoke of the
full quest still passes (don't break the game for prettiness)
- [ ] Handoff note listing every file touched + the two merge points
## Out of scope
Mobs/critters, mobile UI, new blocks (atlas is at 31/32 ids — variants
must come from painting, not new ids), any machines/audio/net code.

View File

@ -0,0 +1,178 @@
# 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.