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

4.7 KiB
Raw Blame History

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

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 KinematicColliders: 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.