# 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.