TURNCRAFT/docs/briefs/LANE_B_PLAYER.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

95 lines
4.7 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 Controller & Physics
**Read first:** `docs/DESIGN.md`, `docs/CONTRACTS.md`, everything in `src/core/`.
**You own:** `src/player/**`, `src/demo/playerDemo.ts`, `demo-player.html`.
**You may not touch anything else.**
## Mission
First-person character controller with Minecraft feel: AABB-vs-voxel collision,
gravity/jump, sprint, pointer-lock mouse look, fly mode — plus the project's
signature requirement: **riding kinematic platforms** (spinning platters,
sliding fader sleds, the swinging tonearm) supplied as `KinematicCollider`s.
## Provides
- `PlayerController` — implements `IPlayerView` (`src/core/types.ts`).
Constructor: `(world: IVoxelWorld, opts: { getColliders(): KinematicCollider[],
camera: PerspectiveCamera, domElement: HTMLElement, spawn: Vec3 })`.
Methods: `update(dt)` (fixed step), `teleport(v: Vec3)`, `setFlying(b)`.
## Consumes
`IVoxelWorld` (interface only), `KinematicCollider` list via the injected
getter (Lane D implements the real ones; you mock in your demo), constants
from `src/core/constants.ts` (`PLAYER`, `GRAVITY`, `FIXED_DT`).
## Tasks
### B1. Input
- Pointer lock on click; WASD + Space (jump) + Shift (sprint) + F (toggle
fly; in fly, Space/Ctrl = up/down, no gravity). Mouse look with pitch clamp.
- Input state object separated from physics so the demo can script inputs.
### B2. Voxel collision
- Player is an AABB (`PLAYER.width/height`), moved with swept per-axis
resolution (move X, resolve; Y; Z — Minecraft order) sampling
`world.isSolid` over the overlapped voxel range. No tunneling at sprint +
platform speeds combined (clamp per-step displacement to < 0.5 voxel per
axis by substepping inside `update` when needed).
- Step-assist: auto-step up 1-voxel ledges when grounded and moving into them
(makes knob forests and cable bridges traversable without jump spam).
- Track `onGround`, emit `player:landed { impactSpeed }` on landing and
`player:step { block }` every ~1.8 voxels of grounded travel (Lane E plays
footsteps off this; read the block under the feet via `world.getBlock`).
### B3. Kinematic platforms (the signature feature)
- Each tick, after voxel resolution, test the player AABB against every
`KinematicCollider`:
- `aabb` shape: standard AABB resolution, pushing the player out along the
minimum axis; if it pushes from belowup, treat as ground.
- `cylinder` shape (axis +Y): resolve top-surface standing (player bottom
within a small epsilon above `center.y + halfHeight` and horizontal
distance < radius grounded on it) and side push-out (horizontal radial).
- When grounded on a kinematic collider: set `groundedOn` to its id and add
`collider.velocityAt(playerPos)` to the player's displacement this tick
(position-level carry, not velocity accumulation jumping off should
inherit the carry velocity at the moment of leaving, so the record-edge
launch works: cache last carry velocity and add it to the jump).
- Standing on a spinning cylinder must be rock-solid: no drift toward the rim
at 2 game-RPM near the center, believable slide/fling at 45-speed rim
(it's fine if this emerges naturally from carry velocity + inertia; verify
it feels right rather than simulating friction properly).
### B4. Feel tuning
- Air control ~30% of ground accel; ground friction snappy (Minecraft-like).
Use the constants; if a constant feels wrong, tune locally with a
clearly-marked multiplier and flag it in HANDOFF (don't edit core).
- Camera: subtle view-bob when walking (toggleable), FOV +5° while sprinting.
## Demo (`demo-player.html`)
All mocks in your demo file, marked `// DEMO MOCK`:
- `MockWorld implements IVoxelWorld`: 96×96 floor, a staircase of 1-voxel
steps, a 2-voxel wall (must NOT auto-step), a narrow 2-wide bridge, a pit.
- Two mock `KinematicCollider`s: a spinning cylinder (r=41, using
`PLATTER.rpm33` / a button to switch to `rpm45`) rendered as a visible disc
mesh you rotate in the demo loop, and a slow horizontal oscillating AABB
platform. Buttons: toggle spin speed, teleport-to-disc, toggle fly.
- HUD text: position, onGround, groundedOn, current carry velocity.
## Acceptance
- [ ] Walk/sprint/jump/fly all correct; 1-voxel auto-step works, 2-voxel blocked
- [ ] Standing anywhere on the spinning disc carries you in a clean circle;
near-center is calm; rim at rpm45 outruns sprint and flings on jump
- [ ] Riding the oscillating AABB platform: zero jitter, no falling through
- [ ] No tunneling: sprint-jump at the wall/bridge/pit 20 times, never clip
- [ ] `player:step` / `player:landed` fire correctly (log to console in demo)
- [ ] `npm run typecheck` clean; `HANDOFF.md` written
## Out of scope
Raycasting/block interaction (Lane D), rendering the real world (Lane A),
sounds (Lane E), mobile/touch input.