- 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>
195 lines
11 KiB
Markdown
195 lines
11 KiB
Markdown
# 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.00–20.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.
|