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>
15 KiB
TURNCRAFT — Integration Notes (what the integrator changed and why)
Integration performed per INTEGRATION.md after all five lanes landed. Every cross-lane glue decision is listed here.
Contract changes (core is integrator-owned)
Y_RECORD_TOP: 85 → 81 (src/core/constants.ts). At 85 the vinyl surface was a 5-voxel cliff above the plinth (jump height ≈ 1.3 — the record was unreachable), and Lane D's platter stack floated on the plinth top, ignoring the 6-deep well Lane C carved. Now the record sits 1 proud of the plinth (auto-steppable, like a real 1200) and the platter body fills the well. Everything downstream (tonearm heights, audio panner positions, FX rim sparkle, D's quest defaults) derives from the constant and moved automatically. AddedPLATTER.wellDepth = 6alongside.- Lane B's requested
velocityAtout-param overload was NOT taken: Lane D already returns a reused per-collider scratch array (no allocation), which satisfies the hot-path concern with no contract change.
Cross-lane glue (in src/main.ts)
- QuestPositions adapter: Lane C's nested
QUEST_POSis mapped to Lane D's flatQuestPositions; the crossfader slot span is derived from C'sMIXERanchors (centerX ± halfLen, faceTopY−depth+1 … faceTopY−1, z −1…+2) so D's slide gate covers exactly the tram volume C carved. machines.attachPlayer(player)called after the player is built (Lane D's added step "6.5") — enables walk-push faders and walk-press plates.- Hotbar: D's
Hotbarstructurally satisfies E'sHotbarModel; passed directly asgetHotbar, withhotbar.onChange(() => hud.refreshHotbar()). - Input wiring: LMB hold = mine, RMB = place, E = use, 1–9/wheel = hotbar,
all gated on pointer lock (matching Lane B's own key gating). Start splash
click →
audio.init()+ pointer lock; resume click → re-lock only. - Fixed-step loop: machines → player → interaction → fx at
FIXED_DT, withMAX_SUBSTEPScatch-up and an accumulator clamp so a backgrounded tab doesn't spiral;audio.updateListener+chunks.update()+ render per frame. - Debug handle
window.TURNCRAFT(world/player/machines/quest/hotbar/audio/ fx/hud/chunks/interaction) — used by the smoke tests, harmless in play.
Lane-file adjustments (documented per CONTRACTS "integrator resolves")
src/machines/platter.ts— platter body now spans the well floor to 1 below the vinyl (visuals) and its collider spans well floor →Y_RECORD_TOPso the spinning body also pushes the player out of the well's 2-voxel gap ring (which is now a real, intentional fall-in hazard).src/fx/layout.ts— VU overlay sprites aligned to Lane C's physical LED towers (x = mixer minX+18 / maxX−18, 12 segments, in front of the open casing face) per Lane E's friction note #5.src/interact/interaction.ts— per-material break times (soft 0.12 s, glass 0.25 s, default 0.4 s). The crossfader jam is ~240 dust blocks; at a flat 0.4 s it was a 96-second chore.docs/DESIGN.md— elevation table updated for the Y_RECORD_TOP change.
Deviations accepted as-is
- 45 speed available pre-win (D's reading; DESIGN's "45 unlocked at win"
treated as flavor).
Platter.unlocked45exists if we ever gate it. fader_crossdoes not drive audio (E maps pitch + channel digits only); crossfader is quest/physical-only in v1.- Both deck pitch faders drive the one global mix pitch (E has a single transport; fine until per-deck stems exist).
- Tonearm pedestal (C's voxels) vs D's arm pivot are ~11 voxels apart — cosmetic misalignment, queued for polish, gameplay unaffected.
Verified at integration (live browser, real code paths)
- Boot: booth builds + meshes in ~670 ms dev / 46 modules build in 0.5 s; fillBox fast path ENABLED; 120 fps; zero console errors all session.
- Ride: standing on the record at 33 → clean circle, r drift 20.00→20.02 over 4 s, y = 81 exactly, spin-up ramp visible; at 45 near the rim → 10.7 v/s (= ω×r), jump lands back on the moving disc.
- Mining: real DDA aim → dust breaks into hotbar; crossfader stays gated while dust remains.
- Quest chain: stems stack 0→5 per
signal:repair, power switch arms exactly after the 4th repair, real button press →game:winonce, both platters auto-start, HUD banner + tracker 5/5, AudioContext running. - The platter-well gap ring behaves as a trench (collider pushes, no clipping).
Concept-art pass (post-integration, from John's Gemini mockup)
- Terraced records: the flat disc became a stepped "groove amphitheater" —
3 vinyl tiers rising 1 voxel each to the label plateau, then a 4-tier
stepped spindle tower with an amber lamp (all in
TIERS,platter.ts). Each tier is its own stacked kinematic cylinder; you walk/spiral up a SPINNING staircase. Tonearm raised (HEAD_Y+2.6) andR_INNERpulled to 16 so the needle clears the terraces and never hits the tower. - Player physics upgrades this required (
PlayerController.ts): cylinder contacts now resolve by minimum penetration (up vs radial), which fixes a latent slingshot bug (feet dipping below a disc's top while deep inside its footprint used to teleport the player radially to the rim); 1-voxel collider lips auto-step like voxel ledges; cylinder tops have a half-foot radius grace so you can stand on rims. - Well seam: the platter well's outer ring was a 7-deep inescapable trench; floor raised to 1 below the plinth top (worldgen) — now a Technics seam you step out of.
- Room feel: plywood ceiling with six recessed amber lamp panels (worldgen) + matching warm PointLights (main.ts), warmer/brighter hemi/ambient/fog (renderer.ts).
- Mixer color pop: knob caps get a deterministic palette scatter (red/
white/gold/LED) in
buildMixer. Rim crumble: vinyl chips/dust/screws scattered around the well mouths inbuildDeck. - Dev nicety:
window.TURNCRAFT_CINE = { pos, look }detaches the camera for screenshots/trailers;window.TURNCRAFTexposes camera/render too. - Verified: walk-in from the rim spirals up to the label plateau at 33 rpm (fighting the spin is intentional physics — stop the deck for an easy climb); tower fully climbable (83→87 by walking); seam escape 1-step; 121 fps with ceiling + lights; quest/win chain re-verified end-to-end.
Headshell Workshop (Lane D) — wiring & contract additions
The stylus node is now a five-stage cartridge assembly at the Deck A pedestal
(src/machines/workshop/**). One machine, machines.workshop (a Workshop),
owns the visible/collidable assembly arm; the real Tonearm sits in service
mode until the node is repaired, then takes back over (cue/track/return).
Events added (src/core/events.ts, additive — sanctioned by the brief):
workshop:test { errors: string[]; clean: boolean }— needle-drop verdict; the HUD names the first error.errorsareTestErrorskeys inERROR_ORDER.workshop:torque { value: number | null }— radial HUD meter 0..1 while a screw is being torqued;nullhides it.workshop:msg { text: string }— transient HUD subtitle (stage prompts).
The audio + HUD lanes consume these; Lane D only emits them (never renders the meter/subtitle itself).
Multiplayer — one new relay message both ways: { t: 'workshop', s: WorkshopState },
last-write-wins.
WorkshopState(inworkshop/types.ts) is JSON-primitive and range-bounded:{ cartSeated, cartRotation:0..3, wires:(0..3|null)[4], screws:[0..1,0..1], weightNotch:0..10, tested }.NetClientsends onworkshop.onChange(guarded byapplyingRemoteso remote applies don't echo); remote messages callworkshop.setState(s)(diff+animate).server/relay.mjsvalidWorkshop()validates shape/enums/ranges (clamps screws + notch), stores it besideplatters, persists it, and hands it to late joiners inhello.state.workshop.types.ts:coerceStatemirrors this on the client. Fuzzed/malformed shapes are dropped on both sides.- If
repaired.stylusis already true on join, the workshop spawns in its completed pose (completedState()— cart seated square, wires on, screws tight, notch 8) and the real tonearm is live.
Surgical edits to other lanes' files:
tonearm.ts— additivesetService(on)(parks the arm low + inerts its colliders while the workshop owns the assembly arm) andsetStylus(on).PlayerController.ts(Lane B) — one additive knob,jumpScale(default 1).main.tssets it to0.5whileworkshop.isCarrying()is non-null so a carried tag-wire tugs the jump to half height. Purely additive; no reorder.interact/interaction.ts—useDown/useUp/dropCarryroute E (press vs hold=crimp/torque) and Q (drop wire) to the workshop;advanceWorkshopHoldcancels a hold if aim leaves the target.ui/Menu.ts— STYLUS help text + the E-hold / Q controls updated.worldgen/buildBooth.tsdressing was intentionally not touched: the workshop renders all its own meshes (pedestal, loom, pins, scope, level), so 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
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 toclipso everything stays revisitable. - Seesaw ride: standing on
ws_ride3while 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),
jumpScale0.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
workshopshapes (bad enums/lengths/types/NaN/∞/proto) all dropped, stored state uncorrupted, no crash; a valid message is still accepted and served inhello.state.workshop. npm run typecheckclean; 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)
For the Headshell Workshop's Stage-4 seesaw (the tonearm you stand on while
balancing). Lane B (PlayerController.ts) now rides a platform that moves
vertically under the player, not just horizontally. What the workshop lane
needs to know:
- Model the arm as kinematic colliders updated each fixed tick, before
player.update.tonearm.tsalready does this (an aabb chain tiling the pivot→head line + a head aabb). For the seesaw, recompute each segment'smin.y/max.yfrom the tilt each tick and expose avelocityAtthat returns the segment's surface velocity. The loop ordermachines.update(dt)→player.update(dt)is what makes the ride correct — don't reorder. - Lane B guarantees the ride; do NOT reposition the player yourself. Just
move the colliders. The controller:
- grounds on the single highest supporting top under the footprint;
- a rising arm grounds by penetration (it rises into you) and snaps you up;
- a tilting/descending arm keeps you glued via ride-snap: if you were
riding a platform and it drops out from under you, you snap back onto its top
across a per-tick vertical gap up to
RIDE_SNAP(0.75 voxel). Gated to a downward/level velocity, so it never fights a jump; - horizontal surface motion is carried via
velocityAt(x/z); - min-penetration keeps you centered — no lateral slingshot.
- Safe envelope (measured in
demo-player.html): perfectly glued (grounded 100 %, feet on the slab) for tilt rates up to ~2 rad/s / ~24 voxel/s surface speed — far beyond a counterweight-driven balance (~0.25 rad/s). Only absurd >3 rad/s "mousetrap" tilts separate, and then gracefully (you slide off, no explosion). Verified at platter-33 and stopped (independent colliders). - Verified against the REAL arm (not just the demo mock): a headless harness
drove the actual
Workshop.tsws_ride*geometry — the samebeamY/thetatilt math,RIDE_SEGMENTS=6, 0.6-voxel-tall slabs — throughPlayerController. Standing on the beam tail (where you shove the sled) while the weight sweeps the full notch range 0↔10: the realistic balance ritual rides 99.6 % grounded,maxYerr0.023, zero lateral drift (maxZoff0.000), and every transient ungrounded tick is re-caught in exactly 1 tick. Because the workshop'sthetaeases atdt*5toward a one-notch (0.025 rad) target, the per-tick vertical travel at the player's feet is ≤ ~0.03 voxel — ~25× inside theRIDE_SNAPbudget. Even a physically-impossible instant full-swing-every-tick slam only floats the player upward briefly (stillmaxZoff0, never falls). - Gotchas:
- The arm is a staircase of flat slabs; you stand at each segment's midpoint height. Finer segments = smoother ride. Keep each segment ≤ ~2 voxels tall so min-penetration prefers the top (a very tall arm segment could resolve as a wall push instead of a floor).
velocityAt's vertical magnitude does not affect grounding (ride-snap handles vertical); a nonzero horizontal component keeps the player tracking a sweeping arm. Returning[0,0,0]is fine for a purely vertical tilt.- Jumping off the arm works and inherits the arm's horizontal carry (the record-edge launch path); vertical is not inherited (no surprise pop).
- Eyeball it:
demo-player.htmlhas a Seesaw toggle (oscillates a segmented tilting arm) + Teleport to arm; stand on it and watchgroundedOnstayseesawNas it tilts.
Not yet done (deploy phase)
Run /ship-check before exposing anything publicly; deploy per deploy-map
conventions (static dist/ — 156 KB gzipped). Cloudflare/host wiring is a
separate step, to be done with the token holder present.