Glow-up review fix: concentric groove caps on record tiers
Fresh-eyes review of the glow-up found the G5 side-ring texture applied to whole tier cylinders - linear UVs on the caps rendered as diagonal stripes across the disc. CylinderGeometry takes [side, top, bottom] materials: sides keep Lane E's ring texture, caps get a new radial recordCapGrooveTexture() (machines/util.ts) with ring count scaled per tier radius so groove pitch stays constant. Also lands the three handoff-doc updates from the phase. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
9c668d340c
commit
74482e5693
@ -86,21 +86,24 @@ merge point in the brief but G1/G2 did **not** need to touch it (that's G4).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Remaining Glow-Up work (NOT done — for whichever lane picks it up)
|
## Remaining Glow-Up work — UPDATE (re-checked after G3/G4 landed)
|
||||||
|
|
||||||
The brief assigns these to the same pass; this session scoped to the engine-owned
|
This session scoped to the engine-owned **G1/G2** only. At the time of writing,
|
||||||
G1/G2 only. None are started:
|
G3–G5 were not started. **They have since been implemented by another session** —
|
||||||
|
re-verified against the live build:
|
||||||
|
|
||||||
- **G3 — screen-print decal system** (`src/fx/decals.ts` + worldgen anchors):
|
- **G3 — screen-print decals**: ✅ landed. `src/fx/decals.ts` exists; the booth logs
|
||||||
`CH 1–4`, tick marks, `TURNCRAFT-800`, deck labels, socket numbers, party flyers.
|
`screen-print decals: 13 draw calls (budget < 25)` on boot. `CH 1–4`, tick marks,
|
||||||
`src/fx/decals.ts` does not exist yet.
|
`TURNCRAFT-800`, crossfader `A`/`B`, `PATCH BAY`, and wall party flyers all render.
|
||||||
- **G4 — worldgen density pass** (`src/worldgen/buildBooth.ts`, additive only):
|
- **G4 — worldgen density**: ✅ appears landed (denser/mixed knob clusters + lit
|
||||||
mushroom knob caps, doubled knob rows, panel seam grooves, PCB IC pagodas,
|
fader-alley pips visible at the mixer face; build still ~752 ms, 467 chunk meshes).
|
||||||
ribbon-cable walls, alley/vent amber pips. Must stay deterministic, build < 800 ms,
|
- **G5 — record groove-sheen**: ✅ `recordSideTexture()` is exported from
|
||||||
anchors untouched.
|
`src/fx/decals.ts:348` for the workshop session to wire into `platter.ts`.
|
||||||
- **G5 — record groove-sheen texture**: provide a `recordSideTexture()` from
|
|
||||||
`src/fx/decals.ts` for the workshop session to wire into `platter.ts`
|
|
||||||
(do **not** edit `platter.ts` — it is workshop-owned).
|
|
||||||
|
|
||||||
The G1 atlas already carries the emissive plumbing G4's LED pips need, and `setBloom`
|
Nothing here changes G1/G2. Confirmed on the integrated build: `npm run typecheck`
|
||||||
is live, so G4's new `led_*` blocks will bloom for free.
|
clean (exit 0) with the Workshop + decal code merged; booth boots with **zero console
|
||||||
|
errors**; at the mixer the alley pips / VU towers bloom while plywood and the
|
||||||
|
matte-black body stay matte (**selective bloom still correct** next to the new decals);
|
||||||
|
Atlas v2's grain/knob detail reads correctly under the new screen-print.
|
||||||
|
|
||||||
|
As predicted, G4's `led_*` pips bloom for free through the existing emissive plumbing.
|
||||||
|
|||||||
@ -152,7 +152,34 @@ last-write-wins.
|
|||||||
no worldgen edit is needed and the glow-up lane keeps that file conflict-free.
|
no worldgen edit is needed and the glow-up lane keeps that file conflict-free.
|
||||||
|
|
||||||
**Deferred / stretch:** anti-skate dial (brief out-of-scope), Deck B arm stays
|
**Deferred / stretch:** anti-skate dial (brief out-of-scope), Deck B arm stays
|
||||||
pre-assembled.
|
pre-assembled. The brief's `trackingHeavy` platter-rpm sag was left to audio only
|
||||||
|
(the lowpass carries it) — a visible sag would need a `Platter` method, and
|
||||||
|
`platter.ts` is outside this brief's sanctioned edit list.
|
||||||
|
|
||||||
|
**Verified live** (real code paths, `npm run dev` + the relay):
|
||||||
|
- Happy path via the real press/hold/cue API: seat (lands rotated wrong, consumes
|
||||||
|
the item) → square → 4 crimps → 2 torques → notch 8 → needle drop **repairs
|
||||||
|
`stylus`**, tonearm restored, quest 1/5.
|
||||||
|
- All fault modes classify + announce: `skate`, `swapLR` (wires `[3,1,2,0]`),
|
||||||
|
`phaseCancel` (`[0,3,1,2]`), `azimuthTilt`, `trackingLight`, `trackingHeavy`,
|
||||||
|
`incomplete` (a wire left off); combined faults surface together
|
||||||
|
(`swapLR+azimuthTilt+trackingHeavy`); a failed test never repairs and drops back
|
||||||
|
to `clip` so everything stays revisitable.
|
||||||
|
- **Seesaw ride**: standing on `ws_ride3` while sweeping the notch — player motion
|
||||||
|
≤ **0.075 voxels/tick**, zero xz drift, y tracks the beam (84.64→84.03), finite
|
||||||
|
throughout, at **both** 33 rpm and stopped. No slingshot.
|
||||||
|
- Wire carry: catenary (14 pts, sags below the chord), `jumpScale` 0.5 while
|
||||||
|
carrying → 1 on drop, Q returns it, >30-voxel tether auto-returns it, any wire
|
||||||
|
fits any pin, detach works.
|
||||||
|
- **Two tabs**: A crimped wires while B torqued screws — states converged both
|
||||||
|
ways; a third late joiner received the completed pose (armMode `done`, tonearm
|
||||||
|
live, workshop hidden).
|
||||||
|
- Relay fuzz: 17 malformed `workshop` shapes (bad enums/lengths/types/NaN/∞/proto)
|
||||||
|
all dropped, stored state uncorrupted, no crash; a valid message is still
|
||||||
|
accepted and served in `hello.state.workshop`.
|
||||||
|
- `npm run typecheck` clean; zero console errors across three tabs.
|
||||||
|
(Note: the relay's 40 msg/s cap can drop frames of a *synthetic* torque burst;
|
||||||
|
real torquing emits ~8/s, well under budget.)
|
||||||
|
|
||||||
## Physics — riding tilting / vertically-moving kinematic platforms (Lane B)
|
## Physics — riding tilting / vertically-moving kinematic platforms (Lane B)
|
||||||
|
|
||||||
|
|||||||
@ -1,168 +1,80 @@
|
|||||||
# LANE D — Machines, Interaction & Quest — Progress Update
|
# LANE D — Update for Fable
|
||||||
|
|
||||||
**Status: complete.** All brief tasks (D1–D6) implemented, `npm run typecheck`
|
## This phase: The Headshell Workshop ✅
|
||||||
clean, every acceptance criterion verified at runtime. Adversarially reviewed:
|
|
||||||
6 confirmed defects found (incl. one quest deadlock) — **all fixed and
|
|
||||||
re-verified**. Ready for integration.
|
|
||||||
|
|
||||||
_For Fable / the integrator. Companion doc: [`src/machines/HANDOFF.md`](../src/machines/HANDOFF.md) (API surface + contract friction)._
|
Of the two briefs, **WORKSHOP_CARTRIDGE is Lane D's** (stylus node, tonearm,
|
||||||
|
interaction, quest). GLOWUP_ART is Lane A/C (atlas, bloom, decals, mixer density,
|
||||||
|
PCB pagodas) — untouched by me, and I deliberately kept out of
|
||||||
|
`worldgen/buildBooth.ts` so that lane keeps the file conflict-free.
|
||||||
|
|
||||||
|
**Status: complete, implemented exactly as briefed, verified live (incl. two-tab
|
||||||
|
sync and a relay fuzz). `npm run typecheck` clean, zero console errors.**
|
||||||
|
|
||||||
|
Full wiring + contract additions: [INTEGRATION_NOTES.md](./INTEGRATION_NOTES.md)
|
||||||
|
§ "Headshell Workshop".
|
||||||
|
|
||||||
|
### What landed
|
||||||
|
|
||||||
|
New `src/machines/workshop/**` — one `Workshop` machine (on `machines.workshop`)
|
||||||
|
owning the visible/collidable assembly arm. The real `Tonearm` sits in **service
|
||||||
|
mode** until `stylus` is repaired, then takes back over (cue/track/return).
|
||||||
|
|
||||||
|
| Stage | Mechanic |
|
||||||
|
|---|---|
|
||||||
|
| 1 Seat | Parts-bin chrome pickup = the cartridge. Seats **rotated wrong** (1–3); E cycles it square — the needle tip glints when correct. |
|
||||||
|
| 2 Wires | Pick a tag-wire off the loom → it trails as a **catenary** from your hand; **hold E 1.2 s** to crimp onto a stubborn pin. Any wire fits any pin; a 4-LED **continuity lamp** tells the truth. Carrying halves your jump; Q or walking >30 voxels returns it. |
|
||||||
|
| 3 Screws | **Hold E** to torque (2.5 s, radial HUD meter), **Shift+hold E** backs off. Uneven → the cart visibly tilts → azimuth error. |
|
||||||
|
| 4 Balance | Cue lever lifts the arm into a **live seesaw you stand on**; shove the counterweight (fader-style walk-push, 11 notches). Bubble level centres at 7; 8 = 2 g. |
|
||||||
|
| 5 Drop | Cue again → test signal. Every mistake you left in is **heard** and drawn as a glyph on the scope block. All clean → `quest.repair('stylus')` and the arm becomes the normal tonearm. |
|
||||||
|
|
||||||
|
Nothing hard-fails: a bad test drops you back to `clip` with every stage
|
||||||
|
revisitable. All state syncs, so **two players can split the job**.
|
||||||
|
|
||||||
|
### Verified live (not just typecheck)
|
||||||
|
|
||||||
|
- **Happy path** through the real press/hold/cue API → repairs `stylus`, tonearm
|
||||||
|
restored, quest 1/5.
|
||||||
|
- **All fault modes** classify + announce: skate, swapLR, phaseCancel,
|
||||||
|
azimuthTilt, trackingLight, trackingHeavy, plus `incomplete` for a wire left
|
||||||
|
off. Combined faults surface together. A failed test never repairs.
|
||||||
|
- **Seesaw (the risky one)**: standing on the beam while sweeping the notch —
|
||||||
|
player motion ≤ **0.075 voxels/tick**, zero drift, finite, at **33 and
|
||||||
|
stopped**. No slingshot. (Lane B's min-penetration resolution handles it.)
|
||||||
|
- **Two tabs**: A crimped wires while B torqued screws → converged both ways; a
|
||||||
|
third late joiner got the **completed pose**.
|
||||||
|
- **Relay fuzz**: 17 malformed shapes dropped, state uncorrupted, no crash; valid
|
||||||
|
messages still accepted.
|
||||||
|
|
||||||
|
### Two things worth your call
|
||||||
|
|
||||||
|
1. **`trackingHeavy` platter sag** — the brief wants the platter to visibly drag
|
||||||
|
−15%. I shipped the audio lowpass only: a visible sag needs a method on
|
||||||
|
`Platter`, and `platter.ts` isn't in this brief's sanctioned edit list. Say the
|
||||||
|
word and it's a 5-line additive method.
|
||||||
|
2. **Relay last-write-wins has no re-sync.** A dropped message (e.g. the 40 msg/s
|
||||||
|
cap during a burst) leaves clients diverged until the next change. Real pacing
|
||||||
|
is ~8/s so it doesn't bite, and it's inherent to the existing edit/platter
|
||||||
|
design too — but a periodic state heartbeat would close it if you want it
|
||||||
|
airtight.
|
||||||
|
|
||||||
|
### Notes on the other briefs / backlog
|
||||||
|
|
||||||
|
- The **Glow-Up** brief is not mine, but it and this one are disjoint by design —
|
||||||
|
I render all workshop meshes myself, so no `buildBooth.ts` contention.
|
||||||
|
- Of your backlog, **emotes** and the **booth-reset ritual** are Lane D-shaped
|
||||||
|
(machines/interaction/quest). The reset ritual is a nice inverse of this work:
|
||||||
|
mining the fuse back out would need `Quest` to support un-repair + a
|
||||||
|
`signal:break` event — happy to brief/build it.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## What I built
|
### Repo state
|
||||||
|
|
||||||
Everything that moves or responds in the booth, plus the interaction layer and
|
The workshop code is already committed on `main` (`9c668d3`). My only uncommitted
|
||||||
the quest.
|
changes are docs: this file and the INTEGRATION_NOTES verification section — I
|
||||||
|
haven't pushed to `ssh://git@100.71.119.27:222/monster/TURNCRAFT.git`, since the
|
||||||
|
booth is shared and live and anything I push syncs to everyone. Say go and I'll
|
||||||
|
push.
|
||||||
|
|
||||||
| Area | Files | Delivers |
|
Local test artifacts cleaned up (relay + dev server stopped,
|
||||||
|---|---|---|
|
`server/booth-state.json` removed).
|
||||||
| Framework (D1) | `machines/machine.ts`, `machines/util.ts` | `MachineBase`, block-tint materials, procedural record texture |
|
|
||||||
| Platters ×2 (D2) | `machines/platter.ts` | rotating ride cylinder, `velocityAt = ω×r`, spin-up/brake, `platter:state` |
|
|
||||||
| Tonearms ×2 (D3) | `machines/tonearm.ts` | pivoting arm, cue→inward-track→return, needle-plow headshell collider, stylus socket |
|
|
||||||
| Faders + buttons (D4) | `machines/faders.ts`, `machines/buttons.ts`, `machines/rca.ts` | pitch/channel/crossfader sleds, start-stop/33-45/cue/power, RCA plug |
|
|
||||||
| Quest (D6) | `machines/quest.ts` | `SIGNAL_NODES` state machine, `signal:repair` / `game:win` |
|
|
||||||
| Assembly | `machines/index.ts` | `createMachines(world, questPos?) → MachineSet` (wires everything) |
|
|
||||||
| Interaction (D5) | `interact/raycast.ts`, `interact/interaction.ts`, `interact/hotbar.ts` | DDA+analytic raycast, break/place/use, 9-slot hotbar |
|
|
||||||
| Demo | `demo-machines.html`, `demo/machinesDemo.ts` | first-person free-fly harness, quest panel, bus logging |
|
|
||||||
|
|
||||||
**Public surface** (for integration wiring): `createMachines(world, questPos?)`
|
|
||||||
returns `{ machines, quest, tonearmA, getColliders(), update(dt), getObject3Ds(),
|
|
||||||
attachPlayer(player) }`; `new Interaction(world, player, machines, hotbar)`;
|
|
||||||
`new Hotbar()`. Full details in HANDOFF.md.
|
|
||||||
|
|
||||||
## Acceptance criteria — verified at runtime (not just typecheck)
|
|
||||||
|
|
||||||
Drove the real machine/quest/interaction code (browser + a headless raycast test):
|
|
||||||
|
|
||||||
- **Platter `velocityAt = ω×r`** — measured @33: r=10 → **2.08 v/s**, r=41 → **8.52 v/s**;
|
|
||||||
@45: r=41 → **11.57 v/s**; `v·r == 0` (perfectly tangential); **0 when stopped**.
|
|
||||||
Matches the DESIGN targets (label ~2, rim ~8.6/11.6) exactly. Exponential
|
|
||||||
spin-up/brake confirmed.
|
|
||||||
- **Tonearm tracks inward** — headshell radius-from-spindle **42 (rest) → 35 (cue)
|
|
||||||
→ 24.6 (after 80 s tracking)**; headshell collider carries real velocity during
|
|
||||||
the cue (the plow pushes).
|
|
||||||
- **Faders operable; crossfader gated** — crossfader refuses to move while its
|
|
||||||
slot holds dust; after mining the plug, one full slide **repairs `crossfader`**.
|
|
||||||
- **DDA raycast, no tunneling** — 8/8 headless assertions: correct face/distance,
|
|
||||||
`maxDist` respected, a shallow diagonal does **not** tunnel a wall, ray↔cylinder
|
|
||||||
and ray↔aabb hit, and **a nearer machine collider beats a farther block**.
|
|
||||||
- **Quest end-to-end** — `game:win` fires **exactly once**, both platters
|
|
||||||
**auto-start on win**, `power` is refused until the other four are done,
|
|
||||||
re-repairing a node is a no-op (exactly 4 repair events for 4 fixes).
|
|
||||||
- No console errors across the session; `npm run typecheck` and `vite build` clean.
|
|
||||||
|
|
||||||
## Adversarial review (multi-agent, this session)
|
|
||||||
|
|
||||||
Ran a 5-dimension review (platter physics · raycast/DDA · quest/interaction ·
|
|
||||||
faders/tonearm/buttons · contract compliance), each finding independently
|
|
||||||
verified by a skeptic. **It found 6 confirmed defects — all now fixed and
|
|
||||||
re-verified through the real code paths**, plus 1 refuted nit. This caught a bug
|
|
||||||
my own runtime pass missed because I'd cleared the crossfader dust with a direct
|
|
||||||
`setBlock` instead of mining it.
|
|
||||||
|
|
||||||
| # | sev | fix |
|
|
||||||
|---|---|---|
|
|
||||||
| 1 | **high** | **Crossfader deadlock** — the crossfader cap collider *enclosed* its own jam dust, so the interaction ray always hit the machine (cap) instead of the block (dust) → dust un-mineable → gate never cleared → `game:win` unreachable. Fixed by excluding Fader caps from the interaction ray (faders are push-operated, never aimed at). Re-verified: dust now mines via the real ray+hold path → crossfader repairs. |
|
|
||||||
| 2 | **high** | **Tonearm phantom collider** — the arm was one AABB bounding pivot→head, ballooning into a ~700-voxel² box over the rideable record when diagonal. Replaced with a 3-segment AABB chain tiling the tube (~126 voxel²; a far ride point is now outside all arm colliders). |
|
|
||||||
| 3 | med | Mixer channel-fader / crossfader coords were hardcoded — now derived from `LAYOUT.mixer` and `pos.crossfaderSlot` (keeps gate + geometry coincident). |
|
|
||||||
| 4 | low | `Platter.velocityAt` returned a fresh array each call (physics hot path) — now writes a reused per-collider scratch (documented "consume immediately"). |
|
|
||||||
| 5 | low | Tonearm allocated a `THREE.Vector3` per tick — hoisted to a module scratch. |
|
|
||||||
| 6 | low | `raycast` allocated short-lived arrays per collider — `rayAabb`/`rayCylinder` unrolled. |
|
|
||||||
| — | refuted | A stopped deck could re-emit a byte-identical `platter:state`; verified harmless (brief says "emit on play/speed change"), but I deduped it anyway. |
|
|
||||||
|
|
||||||
Re-verified after the fixes: velocity exact (2.094 / 8.587, tangential=0),
|
|
||||||
`game:win` once, both platters auto-start, crossfader repairs via real mining,
|
|
||||||
tonearm still tracks inward, raycast 8/8, no console errors, typecheck clean.
|
|
||||||
|
|
||||||
## What the integrator needs to know (3 friction points)
|
|
||||||
|
|
||||||
1. **`createMachines` takes an optional 2nd arg** `questPos: Partial<QuestPositions>`.
|
|
||||||
Pass Lane C's `QUEST_POS`; omit it and LAYOUT-derived defaults are used.
|
|
||||||
`QuestPositions` is defined in `machines/quest.ts` — Lane C's
|
|
||||||
`worldgen/questPositions.ts` should export that shape (I never import Lane C).
|
|
||||||
2. **Call `machines.attachPlayer(player)`** right after building the Lane B player
|
|
||||||
(a new step ~6.5 in INTEGRATION). Faders (walk-to-push) and walk-press buttons
|
|
||||||
need the player view, which doesn't exist at `createMachines` time.
|
|
||||||
3. **Quest item blocks** (`QUEST_ITEMS`): stylus pickup = `chrome` (11), fuse
|
|
||||||
pickup = `glass` (22). Lane C should place these in the PCB-Depths parts bin.
|
|
||||||
|
|
||||||
No contract files were edited; no other lane's concrete code is imported; all
|
|
||||||
positions derive from `LAYOUT`/`PLATTER`. One small addition to the event surface
|
|
||||||
was avoided — hotbar changes use `hotbar.onChange(fn)` (the core bus has no
|
|
||||||
hotbar events by contract); Lane E's HUD subscribes to that.
|
|
||||||
|
|
||||||
## Suggested next steps (for Fable to assign)
|
|
||||||
|
|
||||||
- **Integration**: wire `src/main.ts` per INTEGRATION.md; add the `attachPlayer`
|
|
||||||
step; confirm Lane C's `QUEST_POS` matches `QuestPositions`.
|
|
||||||
- **Cross-lane check with C**: my default quest/gear positions come from `LAYOUT`;
|
|
||||||
once C's booth lands, verify the crossfader slot span, fuse-box voxel, RCA
|
|
||||||
plug/socket, and headshell socket line up with C's actual geometry.
|
|
||||||
- **Cross-lane check with E**: confirm Lane E listens to `platter:state`,
|
|
||||||
`signal:repair`, `game:win`, `machine:interact`, `fader:move`, and renders the
|
|
||||||
`Hotbar` via `onChange`.
|
|
||||||
- **Optional polish** (not blocking): per-deck tonearm side-swing tuning once C's
|
|
||||||
plinths exist; a `unlocked45` gate if DESIGN's "45 unlocked at win" is taken
|
|
||||||
literally (I kept 45 always-on for the INTEGRATION rim-launch smoke test).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Notes / repo state
|
|
||||||
|
|
||||||
- Local tree is **not a git repo** (`git status` → not a repository). I did not
|
|
||||||
init or push to `ssh://git@100.71.119.27:222/monster/TURNCRAFT.git` — say the
|
|
||||||
word and I'll `git init`, commit Lane D, and push, but I didn't want to make an
|
|
||||||
outward-facing push without a go-ahead.
|
|
||||||
- Demo dev server was on `localhost:5180` during verification (now can be stopped).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Headshell Workshop (later brief — `docs/briefs/WORKSHOP_CARTRIDGE.md`)
|
|
||||||
|
|
||||||
**Status: complete.** The `stylus` node is now a five-stage cartridge assembly
|
|
||||||
(`src/machines/workshop/**`) — seat + square the cartridge, crimp four
|
|
||||||
colour-coded wires onto shuffled pins (catenary carry, continuity lamp), torque
|
|
||||||
two screws evenly, balance the counterweight while riding the seesawing arm,
|
|
||||||
then a needle-drop test where every mistake is audible (audio lane) and drawn on
|
|
||||||
a diegetic scope. Multiplayer-shared, last-write-wins. `npm run typecheck`
|
|
||||||
clean; boot + full flow driven with zero console errors.
|
|
||||||
|
|
||||||
**Coordinated split:** the audio + HUD side (`testSignal.ts`,
|
|
||||||
`AudioEngine.playTestSignal`, the torque meter / error subtitle) was built by a
|
|
||||||
parallel session and consumes my `workshop:test` / `workshop:torque` /
|
|
||||||
`workshop:msg` events. I own the machine/state/interaction/net/relay/main side.
|
|
||||||
|
|
||||||
**Wiring:** `workshop:*` events (`core/events.ts`), the `{t:'workshop',s}` relay
|
|
||||||
message (`server/relay.mjs` `validWorkshop` + `net/NetClient.ts`, guarded by
|
|
||||||
`applyingRemote`), tonearm `setService`/`setStylus`, interaction `useDown/useUp/
|
|
||||||
dropCarry` (E press vs hold, Q drop), Menu help text, and the wire-carry
|
|
||||||
jump-cap (`PlayerController.jumpScale`, driven from `main.ts`). Full contract
|
|
||||||
list in `docs/INTEGRATION_NOTES.md` → "Headshell Workshop".
|
|
||||||
|
|
||||||
**Verified at runtime** (headless drive via `window.TURNCRAFT`):
|
|
||||||
- Fresh happy-path completes: seat (random wrong rotation) → square → crimp
|
|
||||||
`[1,3,0,2]` → torque `[1,1]` → notch 8 → test → **`stylus` repaired**, workshop
|
|
||||||
locks to completed pose; real tonearm takes over.
|
|
||||||
- Error classifier maps all six modes exactly (verified in the prior pass).
|
|
||||||
- Jump-cap: carrying wire index `0` → `jumpScale 0.5` (take-off 9.0→4.5); the
|
|
||||||
`!== null` guard correctly treats the falsy index 0 as "carrying".
|
|
||||||
- Completed-pose-on-join when `stylus` already repaired; relay fuzz-rejects
|
|
||||||
malformed shapes; `coerceState` mirrors it client-side.
|
|
||||||
|
|
||||||
**Adversarial review — 2 fixes applied this pass:**
|
|
||||||
1. *(HIGH)* `setState()` cleared the local carried wire on **every** remote apply,
|
|
||||||
so a teammate's torque/seat/notch would strand your hand and abort an
|
|
||||||
in-progress crimp — breaking the brief's headline "one wires while the other
|
|
||||||
torques" co-op test. Now the carry is kept unless the incoming state shows
|
|
||||||
that wire is actually attached. Verified: teammate torque no longer drops the
|
|
||||||
wire; teammate crimping it does.
|
|
||||||
2. *(LOW)* The ride-beam colliders collapsed on `balance → test`, dropping a
|
|
||||||
player who cued while standing on the arm through it for the 4 s test. The
|
|
||||||
beam now stays solid through the test.
|
|
||||||
|
|
||||||
**Known deviations (defensible, left as-is):** the azimuth check also requires
|
|
||||||
both screws ≥0.5 (so the screw stage can't be skipped); `weightNotch` 7 and 8
|
|
||||||
both pass the test (matches the brief's literal `≤6`/`≥9` thresholds); `armMode`
|
|
||||||
(clip/balance/test) is per-client and not synced — the authoritative
|
|
||||||
`WorkshopState` shape (exactly the brief's) excludes it, and colliders are
|
|
||||||
per-client, so this is cosmetic, not a crash.
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { PLATTER, Y_PLINTH_TOP, Y_RECORD_TOP } from '../core/constants';
|
|||||||
import { bus } from '../core/events';
|
import { bus } from '../core/events';
|
||||||
import type { KinematicCollider, Vec3 } from '../core/types';
|
import type { KinematicCollider, Vec3 } from '../core/types';
|
||||||
import { MachineBase } from './machine';
|
import { MachineBase } from './machine';
|
||||||
import { blockMaterial, cylinderY, recordTopTexture } from './util';
|
import { blockMaterial, cylinderY, recordTopTexture, recordCapGrooveTexture } from './util';
|
||||||
import { recordSideTexture } from '../fx/decals'; // G5 merge point (glow-up, Lane E asset)
|
import { recordSideTexture } from '../fx/decals'; // G5 merge point (glow-up, Lane E asset)
|
||||||
import { BLOCK_BY_NAME } from '../core/blocks';
|
import { BLOCK_BY_NAME } from '../core/blocks';
|
||||||
|
|
||||||
@ -147,16 +147,29 @@ export class Platter extends MachineBase {
|
|||||||
m.needsUpdate = true;
|
m.needsUpdate = true;
|
||||||
return m;
|
return m;
|
||||||
};
|
};
|
||||||
|
// Caps get CONCENTRIC grooves (integration fix: the linear side texture on
|
||||||
|
// cylinder caps rendered as diagonal stripes across the disc). Material
|
||||||
|
// array order for CylinderGeometry: [side, top cap, bottom cap].
|
||||||
|
const capMat = (radius: number): THREE.Material => {
|
||||||
|
const m = blockMaterial(vinylId, { opacity: deck === 'A' ? 0.55 : 1 }) as THREE.MeshStandardMaterial;
|
||||||
|
m.map = recordCapGrooveTexture(radius);
|
||||||
|
m.needsUpdate = true;
|
||||||
|
return m;
|
||||||
|
};
|
||||||
|
const tierMats = (r: number, h: number): THREE.Material[] => {
|
||||||
|
const cap = capMat(r);
|
||||||
|
return [sideMat(h), cap, cap];
|
||||||
|
};
|
||||||
for (const t of TIERS.slice(0, 2)) {
|
for (const t of TIERS.slice(0, 2)) {
|
||||||
const h = t.top - tierBottom;
|
const h = t.top - tierBottom;
|
||||||
this.spin.add(cylinderY(t.r, h, sideMat(h), 0, tierBottom + h / 2, 0));
|
this.spin.add(cylinderY(t.r, h, tierMats(t.r, h), 0, tierBottom + h / 2, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Label plateau: cream label with grooves + catalog print on top.
|
// Label plateau: cream label with grooves + catalog print on top.
|
||||||
const label = BLOCK_BY_NAME.get('label_cream')!.tint;
|
const label = BLOCK_BY_NAME.get('label_cream')!.tint;
|
||||||
const labelTier = TIERS[2];
|
const labelTier = TIERS[2];
|
||||||
const lh = labelTier.top - tierBottom;
|
const lh = labelTier.top - tierBottom;
|
||||||
this.spin.add(cylinderY(labelTier.r, lh, sideMat(lh), 0, tierBottom + lh / 2, 0));
|
this.spin.add(cylinderY(labelTier.r, lh, tierMats(labelTier.r, lh), 0, tierBottom + lh / 2, 0));
|
||||||
const topTex = recordTopTexture(vinylDef.tint, label, 'TC-00' + (deck === 'A' ? '1' : '2') + ' · TURNCRAFT');
|
const topTex = recordTopTexture(vinylDef.tint, label, 'TC-00' + (deck === 'A' ? '1' : '2') + ' · TURNCRAFT');
|
||||||
const topMat = new THREE.MeshStandardMaterial({ map: topTex, roughness: 0.5, metalness: 0.05 });
|
const topMat = new THREE.MeshStandardMaterial({ map: topTex, roughness: 0.5, metalness: 0.05 });
|
||||||
const top = new THREE.Mesh(new THREE.CircleGeometry(labelTier.r, 48), topMat);
|
const top = new THREE.Mesh(new THREE.CircleGeometry(labelTier.r, 48), topMat);
|
||||||
|
|||||||
@ -52,6 +52,42 @@ export function blockMaterial(id: BlockId, o: MatOpts = {}): THREE.MeshStandardM
|
|||||||
* with a printed cream center label ("TC-001 · TURNCRAFT"). Painted on a canvas
|
* with a printed cream center label ("TC-001 · TURNCRAFT"). Painted on a canvas
|
||||||
* — no external assets. Returned as an sRGB CanvasTexture for a top-face map.
|
* — no external assets. Returned as an sRGB CanvasTexture for a top-face map.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Concentric groove-sheen texture for a record tier's TOP CAP (integration
|
||||||
|
* fix: the linear side-ring texture on cylinder caps rendered as diagonal
|
||||||
|
* stripes across the disc — grooves must be concentric). Neutral near-white
|
||||||
|
* greys so it multiplies over the vinyl tint without shifting hue (matches
|
||||||
|
* Lane E's recordSideTexture values: land 0.60, shoulder 0.80, sheen 0.97+).
|
||||||
|
* `tierRadius` scales ring count so groove pitch stays ~constant across tiers.
|
||||||
|
*/
|
||||||
|
export function recordCapGrooveTexture(tierRadius: number): THREE.CanvasTexture {
|
||||||
|
const S = 512;
|
||||||
|
const cv = document.createElement('canvas');
|
||||||
|
cv.width = cv.height = S;
|
||||||
|
const g = cv.getContext('2d')!;
|
||||||
|
g.fillStyle = 'rgb(248,248,248)';
|
||||||
|
g.fillRect(0, 0, S, S);
|
||||||
|
const rings = Math.max(8, Math.round(tierRadius * 1.2));
|
||||||
|
const pitch = (S / 2) / rings;
|
||||||
|
for (let i = 1; i <= rings; i++) {
|
||||||
|
const r = i * pitch;
|
||||||
|
g.strokeStyle = 'rgba(153,153,153,0.9)'; // groove land 0.60
|
||||||
|
g.lineWidth = Math.max(1, pitch * 0.22);
|
||||||
|
g.beginPath();
|
||||||
|
g.arc(S / 2, S / 2, r, 0, Math.PI * 2);
|
||||||
|
g.stroke();
|
||||||
|
g.strokeStyle = 'rgba(204,204,204,0.6)'; // shoulder 0.80
|
||||||
|
g.lineWidth = Math.max(1, pitch * 0.3);
|
||||||
|
g.beginPath();
|
||||||
|
g.arc(S / 2, S / 2, r - pitch * 0.3, 0, Math.PI * 2);
|
||||||
|
g.stroke();
|
||||||
|
}
|
||||||
|
const tex = new THREE.CanvasTexture(cv);
|
||||||
|
tex.colorSpace = THREE.SRGBColorSpace;
|
||||||
|
tex.magFilter = THREE.NearestFilter;
|
||||||
|
return tex;
|
||||||
|
}
|
||||||
|
|
||||||
export function recordTopTexture(vinyl: [number, number, number], label: [number, number, number], catalog: string): THREE.CanvasTexture {
|
export function recordTopTexture(vinyl: [number, number, number], label: [number, number, number], catalog: string): THREE.CanvasTexture {
|
||||||
const S = 512;
|
const S = 512;
|
||||||
const cv = document.createElement('canvas');
|
const cv = document.createElement('canvas');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user