TURNCRAFT/docs/UPDATE_B.md
jing 5a39e3a947 TURNCRAFT: contracts, docs, and all five lane deliverables (pre-integration)
Lanes A (engine), B (player), C (worldgen), D (machines/quest),
E (audio/fx/ui) as landed, each with HANDOFF.md + update docs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 20:50:56 +10:00

121 lines
6.4 KiB
Markdown
Raw 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
**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.