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>
95 lines
4.7 KiB
Markdown
95 lines
4.7 KiB
Markdown
# 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 below→up, 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.
|