TURNCRAFT/docs/UPDATE_B.md
jing 9c668d340c Headshell Workshop + Glow-Up phase: assembly minigame, art pass, review fixes
- Workshop (WORKSHOP_CARTRIDGE): five-stage cartridge assembly at Deck A —
  seat/square, crimp four tag-wires, torque screws, ride-the-arm counterweight
  balance, needle-drop diagnostic with per-fault audio + scope. Relay-synced
  (per-field co-op merge: held screw + carried wire survive remote state).
- Glow-Up (G1-G5): 32px atlas with per-voxel variants, selective LED bloom
  (quality-gated), screen-print decal system + party flyers, mixer/PCB worldgen
  density pass, record groove-sheen side texture.
- 10 confirmed multi-agent review fixes, incl. co-op screw-stomp soft-lock,
  ride-snap collider-identity (magnet feet / eaten record-fling), workshop SFX
  exact-match map (rca_seated hijack), beam ride colliders to the head,
  double-crimp guard, WIRING INCOMPLETE diagnosis mode, skate-abort timer,
  completedState wiring, per-tick material churn, trackingHeavy platter drag.
- Crossfader playtest fix: slew-limited sled (3.4 v/s) + ribbed grip caps with
  amber index — the sled reads as a heavy handle, not a teleporting wall.
- Demo harnesses: machinesDemo hold-key wiring, playerDemo seesaw phase
  continuity, audioDemo incomplete fault button.
- Workshop sync: exact 1.0 screw endpoint gets its own emit signature.

Verified: typecheck + vite build clean, live solo quest smoke, two-client
co-op relay smoke (simultaneous torque, no rewind, exact convergence).

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

195 lines
11 KiB
Markdown
Raw Permalink 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.

# Lane B — Player & Physics — Update for Review
> **Round 2.1 (2026-07-14, resumed session) — closed the real-geometry loop.**
> Round 2 verified ride-snap against a faithful *mock* seesaw (the account ran
> out of tokens before the real `Workshop.ts` arm existed). That arm now exists,
> and it's wired into `machines.getColliders()` → the player, so I verified
> ride-snap against the **actual** geometry: a headless harness drives the real
> `ws_ride*` segments (`beamY`/`theta` math, `RIDE_SEGMENTS=6`, 0.6-tall slabs)
> through `PlayerController`. Result — the realistic balance ritual (weight swept
> 0↔10) rides **99.6 % grounded, `maxZoff` 0.000 (no slingshot), `maxYerr` 0.023**,
> every ungrounded tick re-caught in 1 tick; even an impossible instant-slam only
> floats you upward, never falls or flings. **Regressions re-confirmed** (disc
> circle 20.0020.11, horizontal platform 99.7 %, flat-floor false-stick fix
> holds → `groundedOn=null`). Whole-project `tsc --noEmit` clean. No code changes
> were needed — the Round-2 ride-snap already handles the real arm with ~25×
> margin; this round is the missing cross-lane verification + doc update
> (INTEGRATION_NOTES physics section now cites the real-arm numbers).
>
> ---
>
> **Round 2 (2026-07-14) — Physics groundwork for the Headshell Workshop.**
> New briefs landed (`WORKSHOP_CARTRIDGE.md`, `GLOWUP_ART.md`). Glow-Up is a Lane
> A/E art pass — not mine. The **Workshop leans on Lane B for one thing**: Stage 4
> has you *standing on the tonearm as it seesaws under your feet* — the brief says
> *"Lane B's … resolution handles the ride — verify no slingshot on tilt."* That's
> riding a **vertically-moving** platform, exactly the limitation I flagged last
> round. I did that slice.
>
> **What I found (tested, didn't assume):** I built a faithful seesaw mock (a
> tilting aabb-segment chain like `tonearm.ts` will use) and drove it headlessly.
> The *old* physics **failed** — a gentle tilt left the player grounded only ~52 %
> of the time (bouncing up to 0.7 above the arm); a fast tilt **flung them off**
> onto the floor. Root cause: vertical motion was left to gravity chasing the
> surface, and a tilting arm outruns gravity.
>
> **What I fixed:** added **ride-snap** in `PlayerController.resolveColliders` — a
> rising arm grounds by penetration; a descending/tilting arm snaps the player
> back onto its top across a ≤`RIDE_SNAP` (0.75 v) per-tick gap, gated so it never
> fights a jump. (I first tried a velocity-based vertical carry — it *overshot*,
> because the arm's collider is static within a tick while the player substeps.
> Ride-snap avoids that entirely.)
>
> **Verified live:** seesaw ride now **glued at the realistic tonearm pace
> (grounded 100 %, feet on the slab)** and holds glued to ~2 rad/s / ~24 v/s
> surface speed — ~8× past anything a counterweight can drive; beyond that it
> slides off *gracefully* (no explosion). All prior mechanics re-verified intact
> (disc circle 20.01, fling 10.2, platform, auto-step, no wall tunneling). No
> lateral slingshot at any rate.
>
> **Self-review caught 2 more:** an adversarial pass on the ride-snap diff found
> a false-stick (a platform flush with the voxel floor kept carrying you after
> you stepped off onto solid ground) and a fly-mode lifecycle gap — both fixed
> (gate ride-snap on *not already on voxel ground* + clear ride state on fly) and
> re-verified: flush-floor now reads `groundedOn=null`, riding over a real gap
> still reads `platform`.
>
> **Files this round:** `src/player/PlayerController.ts` (ride-snap; +`rideCollider`
> state), `src/demo/playerDemo.ts` (seesaw mock + Seesaw/Teleport-to-arm controls),
> `docs/INTEGRATION_NOTES.md` (new **"Physics — riding tilting … platforms"**
> section — the one the Workshop brief tells its dev to read), `src/player/HANDOFF.md`,
> `.claude/launch.json` (added a `turncraft-laneB` port so my demo server doesn't
> collide with the parallel session's). **Stayed in my lane** — did NOT touch
> `tonearm.ts`/`interaction.ts`/audio (those are the Workshop session's edits).
>
> **Heads-up for Fable:** `src/audio/testSignal.ts` (the Workshop session's WIP
> Stage-5 file, untracked) currently fails `tsc` — not mine, flagging it so a repo
> typecheck isn't mistaken for a Lane B regression. My files are clean.
>
> **Next:** ride-snap is done and documented; the Workshop dev just moves the arm
> colliders each tick and Lane B carries the player. `RIDE_SNAP` is a one-liner if
> a wilder tilt ever needs more envelope. Round 1 (the base controller) follows.
---
# Lane B — Player & Physics — Update for Review
**Author:** Lane B · **Status:** ✅ complete, verified live, self-reviewed
**Scope touched:** `src/player/**`, `src/demo/playerDemo.ts`, `demo-player.html`,
`.claude/launch.json` (dev-server config for the demo). No writes to `src/core/`,
`src/main.ts`, or any other lane's directory.
---
## TL;DR
First-person controller with Minecraft feel **and** the signature record-riding
mechanic is done. `npm run typecheck` is clean, the standalone demo runs with no
console errors, and all 10 brief acceptance criteria are verified by driving the
physics in a browser (not just compiling). I then ran a 20-agent adversarial
review, which confirmed 6 issues (from 15 raised); I fixed 3 in code and
documented 2 as core-contract friction for the integrator. Re-verified after the
fixes — no regressions.
## Deliverables
| File | Role |
|---|---|
| [src/player/PlayerController.ts](../src/player/PlayerController.ts) | The controller. Implements `IPlayerView`. |
| [src/player/collision.ts](../src/player/collision.ts) | Swept per-axis AABB-vs-voxel resolution + auto-step. |
| [src/player/input.ts](../src/player/input.ts) | Scriptable `InputState` + pointer-lock `InputController`. |
| [src/player/index.ts](../src/player/index.ts) | Public surface (what integration imports). |
| [src/player/HANDOFF.md](../src/player/HANDOFF.md) | API + tuning + friction detail. |
| [src/demo/playerDemo.ts](../src/demo/playerDemo.ts) + [demo-player.html](../demo-player.html) | Standalone demo (all mocks marked `// DEMO MOCK`). |
**Public API** (per the brief, exactly):
```ts
const player = new PlayerController(world /* IVoxelWorld */, {
getColliders: () => machines.flatMap(m => m.getColliders()),
camera, domElement, spawn: [x, y, z],
});
player.update(FIXED_DT); // call each fixed tick AFTER machines.update(dt)
player.teleport(v); player.setFlying(b);
// IPlayerView for Lanes D/E: position (feet), eye, lookDir, onGround, groundedOn
```
## Acceptance — verified live
Driven headlessly through a debug handle in the demo (deterministic `update()`
stepping), so these are measured, not eyeballed:
| Criterion | Measured result |
|---|---|
| Walk / sprint / jump / fly | ✅ all correct |
| Auto-step 1-voxel ledge | ✅ climbs staircase to y=6 |
| 2-voxel wall NOT stepped | ✅ blocked at x=29.7, never climbed |
| No tunneling (400× sprint-jump into wall) | ✅ never crosses x=30 |
| Disc ride = clean circle, no rim drift | ✅ radius holds at 20.00 over 2s |
| Near-center calm / rpm45 rim > sprint | ✅ carry 0.42 vs 11.31 (sprint=5.6) |
| Fling on jump off rim | ✅ ~10.2 v/s, airborne |
| Oscillating AABB platform, no fall-through | ✅ groundedOn=platform, y=1.0 |
| `player:step` / `player:landed` fire | ✅ 6 steps over 10.5 voxels / impact 26.5 |
| `npm run typecheck` clean, no console errors | ✅ |
## Adversarial review outcome (self-run, 20 agents, 6/15 confirmed)
**Fixed in code (re-verified, no regressions):**
1. **Overlapping colliders double-applied carry** *(medium)*`resolveColliders`
applied surface carry per-collider, so two overlapping rideable colliders
(e.g. a future fader sled crossing a platter rim) would translate the player
by the *sum* and report only the last one via `groundedOn`/`carryVelocity`.
Fix: ground on exactly one collider (highest supporting top), apply carry once.
2. **Keyboard input wasn't gated by pointer-lock** *(medium)* — WASD/F drove the
character while unlocked (only mouse-look was gated). Fix: gate key handlers on
lock state, mirroring mouse-look. **Integrator note:** in-game, movement now
requires clicking to lock first (standard FPS behavior).
3. **Footsteps sampled a single center column** *(low)* — at a bridge/ledge edge
the center column can be air while a neighbor supports the foot, silently
dropping the event. Fix: sample the full footprint (same cells grounding uses).
Verified: walking a bridge edge now fires footsteps where it fired none before.
**Documented as core-contract friction (needs an integrator/core decision — I did
not edit `src/core/`):**
4. **`KinematicCollider.velocityAt(x,y,z): Vec3` allocates per sample** *(low)*
while riding I sample it up to 8×/frame (~480 arrays/sec), brushing CONTRACTS
§8. Clean fix is a core out-param overload `velocityAt(x, y, z, out?: Vec3)`.
5. **Vertical platform carry is intentionally dropped** — fine for v1 gear (+Y
platters, horizontal faders/tonearm). Only matters if Lane D ships a
vertically-moving rideable collider; then `applyCarry` needs a Y sweep.
*(9 findings were refuted on verification — e.g. "getters leak live arrays" and
"held-jump re-emits landing"; details in the review, both judged non-issues.)*
## Integration notes for `main.ts` (per docs/INTEGRATION.md step 6)
- Construct **after** Lane A's `world` and Lane D's machines exist; pass
`getColliders: () => machines.flatMap(m => m.getColliders())`.
- Fixed loop order: `machines.update(dt)` **then** `player.update(dt)` — the
player reads the *current* collider positions/velocities that tick.
- The controller owns the camera's transform + FOV each tick; don't also move it.
- Local feel constants (accel/friction/step/bob) live at the top of
`PlayerController.ts`, not in core, because `PLAYER` has no such fields. If you
want them centralized, promote them to a **new** non-core config file.
## Suggested next steps (for Fable to weigh)
- **Contract tweak decision:** approve the `velocityAt` out-param overload above?
It's a one-line core change that removes the only hot-path allocation. If yes,
I'll wire Lane B to the buffered form once core changes.
- **Cross-lane:** Lane D should confirm cylinder colliders are +Y-axis only and
expose platter/fader/tonearm colliders with `velocityAt` in voxels/sec — Lane B
already rides anything matching `KinematicCollider`.
- Optional polish I left out of scope (not in brief): crouch, coyote-time jump,
ladder/vent climbing for the speaker-mesh (DESIGN §4.2) — flag if wanted.
## Repo / git
This working copy is **not yet a git repo** (`git status` fails). I did not run
`git init`, branch, or push to `ssh://git@100.71.119.27:222/monster/TURNCRAFT.git`
— initializing the shared repo and landing five lanes is an integration decision,
not Lane B's to make unilaterally. Everything above is on disk under
`/Users/jing/TURNCRAFT`, ready to commit on the Lane B branch when you say go.