# Local Player Movement

## Metadata

| Field | Value |
|---|---|
| Status | Implemented |
| Target/work package | M02 / M02-GMP-MOVEMENT-001, M02-GMP-SANDBOX-CAPABILITIES-001, M02-GMP-INPUT-PROFILE-335-001 and presentation consumers |
| Owners | Gameplay local movement state; scene composition owns the instance |
| Last verified | M02-GMP-INPUT-PROFILE-335-001 worktree, 2026-07-15 |
| Profiles/capabilities | `RenderSandbox` debug movement; `Blizzlike335` selected bindings and keyboard turn with debug sprint/flight rejected |

## Purpose

Own current local velocity, acceleration and sandbox flight state independently
of the player scene. Convert an immutable `MoveIntent` plus an explicit
Godot-world movement basis into deterministic per-tick displacement and convert
signed turn intent into a yaw delta without querying terrain, cameras, input
devices or visual assets.

## Non-goals

- Apply world transforms or own a `CharacterBody3D`.
- Query terrain height, collision, water or slopes.
- Implement jump, fall, swim, server prediction or reconciliation.
- Select the player basis versus camera-pivot basis; the scene adapter owns that decision.
- Claim build-12340 movement fidelity from sandbox regression alone.

## Context and boundaries

```mermaid
flowchart LR
    Input[MoveIntent] --> Movement[LocalPlayerMovementController]
    Basis[Explicit local movement Basis] --> Movement
    Delta[Physics delta seconds] --> Movement
    Profile[Scene profile ID] --> Capabilities[PlayerMovementCapabilities]
    Capabilities --> Movement
    Movement --> Displacement[Godot-world displacement]
    Movement --> Yaw[Godot yaw delta radians]
    Movement --> Velocity[Read-only velocity state]
    Scene[ThirdPersonWowController] --> Basis
    Displacement --> Scene
    Scene --> Transform[Player world transform]
    Scene --> Terrain[TerrainQuery and ground snap adapter]
    Velocity --> Presentation[Visual facing and CharacterAnimationPresenter adapter]
```

Allowed dependencies:

- `MoveIntent` from the gameplay input boundary.
- Godot value types `Basis` and `Vector3` for renderer world-space direction math.
- Main-thread scene adapter for basis selection and displacement application.

Forbidden dependencies:

- `Node`, `Resource`, `Input`, `Camera3D`, `ADTLoader` or scene lookup.
- World-coordinate conversion or manual ADT/tile formulas.
- Character assets, animation players, materials, packets or server state.

## Public API

| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `PlayerMovementCapabilities.render_sandbox()` | Factory | Creates the only debug sprint/free-flight capability set | Immutable caller value | None |
| `PlayerMovementCapabilities.blizzlike_335()` | Factory | Creates the compatibility-safe set with debug movement disabled | Immutable caller value | Name is intent, not a parity claim |
| `PlayerMovementCapabilities.for_profile_id(...)` | Factory | Maps composition configuration once; unknown IDs fail closed | Immutable caller value | Unknown maps to `Blizzlike335` |
| `LocalPlayerMovementController.new(...)` | Constructor | Sets explicit speeds, acceleration, sprint multiplier and movement capabilities | Created by composition root; retained for player scene lifetime | Null capability defaults to `Blizzlike335` |
| `godot_world_velocity_units_per_second` | Read-only state | Current Godot-world velocity after the last update | Stable until next mutation on owning thread | None |
| `is_flight_enabled` | Read-only state | Current sandbox free-flight mode | Stable until toggle | None |
| `movement_profile_id` | Read-only state | Effective capability profile ID | Controller lifetime | None |
| `toggle_sandbox_flight()` | Command | Toggles flight only when explicitly permitted and clears velocity | Owning physics/input thread | Rejected request returns `false` with state unchanged |
| `advance(move_intent, godot_world_movement_basis, delta_seconds)` | Stateful update | Accelerates toward requested velocity and returns displacement for the tick | Owning physics thread; one call per tick | Negative delta is treated as zero |
| `calculate_yaw_delta_radians(move_intent, delta_seconds)` | Pure query | Converts signed turn input to Godot yaw delta | Owning physics thread | Negative delta is treated as zero |

Constructor units:

| Parameter | Unit/meaning |
|---|---|
| `run_speed_units_per_second` | Forward Godot units per second |
| `backward_speed_units_per_second` | Backward Godot units per second |
| `strafe_speed_units_per_second` | Lateral Godot units per second |
| `flight_vertical_speed_units_per_second` | Sandbox vertical Godot units per second |
| `acceleration_units_per_second_squared` | Velocity change per second |
| `sandbox_sprint_multiplier` | Dimensionless debug multiplier |
| `keyboard_turn_speed_radians_per_second` | Positive keyboard yaw speed; defaults to PI rad/s |

## Inputs and outputs

| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | `MoveIntent` | `PlayerInputSource` | Movement controller | Immutable caller-held value | One physics tick |
| Input | `PlayerMovementCapabilities` | Application/player scene composition | Movement controller | Immutable caller-held value | Controller lifetime |
| Input | Godot-world `Basis` | Player or `ThirdPersonCameraRig` adapter | Movement controller | Value copy | One physics tick |
| Input | Delta seconds | Godot physics loop | Movement controller | Scalar value | One physics tick |
| Input | Flight toggle command | Remappable sandbox action via scene adapter | Movement controller | Synchronous command | Input event dispatch |
| Output | Displacement `Vector3` | Movement controller | Player scene adapter | Value copy | Applied in same tick |
| Output | Yaw delta radians | Movement controller | Player scene adapter | Scalar value | Applied before movement basis selection |
| Output | Velocity `Vector3` | Movement controller | Facing and `CharacterAnimationPresenter` scene adapter | Read-only value copy | Until next update |
| Output | Flight state | Movement controller | Basis/terrain scene adapter | Read-only boolean | Until next toggle |

Side effects:

- Mutates only controller-owned velocity and flight state.
- Does not mutate scene tree, transforms, resources, filesystem, network or cache.
- The scene adapter remains responsible for transform mutation and ground snap.

## Data flow

```mermaid
flowchart LR
    Intent[MoveIntent axes] --> Target[Calculate target velocity]
    Intent --> Turn[Calculate signed yaw delta]
    Turn --> SceneYaw[Scene applies character yaw]
    Basis[Player or camera-pivot Basis] --> Directions[Forward/right directions]
    Directions --> Target
    Config[Speeds and sprint multiplier] --> Target
    Profile[RenderSandbox or Blizzlike335] --> Capability[Immutable debug capabilities]
    Capability -->|allow or reject sprint| Target
    Capability -->|allow or reject flight toggle| Current
    Previous[Owned velocity] --> Integrate[move_toward by acceleration × delta]
    Target --> Integrate
    Integrate --> Current[Updated velocity]
    Current --> Step[velocity × delta]
    Step --> Displacement[Scene-applied displacement]
```

## Lifecycle/state

```mermaid
stateDiagram-v2
    [*] --> GroundedSandbox
    GroundedSandbox --> FlyingSandbox: toggle [debug flight allowed] / clear velocity
    GroundedSandbox --> GroundedSandbox: toggle [debug flight rejected]
    FlyingSandbox --> GroundedSandbox: toggle / clear velocity
    GroundedSandbox --> GroundedSandbox: advance
    FlyingSandbox --> FlyingSandbox: advance
```

The names describe the existing sandbox mode only. They are not authoritative
gameplay movement states and do not imply terrain contact or server permission.

## Main sequence

```mermaid
sequenceDiagram
    participant Input as PlayerInputSource
    participant Scene as ThirdPersonWowController
    participant Move as LocalPlayerMovementController
    participant Cap as PlayerMovementCapabilities
    participant Present as CharacterAnimationPresenter adapter
    Input-->>Scene: MoveIntent
    Scene->>Cap: map configured profile once
    Scene->>Move: construct with immutable capabilities
    Scene->>Move: calculate_yaw_delta_radians(intent, delta)
    Move-->>Scene: yaw delta
    Scene->>Scene: apply yaw and synchronize camera orbit yaw
    Scene->>Move: advance(intent, selected basis, delta)
    Move-->>Scene: displacement
    Scene->>Scene: apply global position and ground snap
    Scene->>Move: godot_world_velocity_units_per_second
    Scene->>Present: horizontal motion
```

## Ownership, threading and resources

- The player scene creates and exclusively owns one movement controller.
- The composition root creates one immutable capability value; the controller
  retains it and exposes only its effective profile ID.
- Velocity and flight state have the same lifetime as that controller.
- All mutations currently occur on the main input/physics thread.
- Read-only value returns are copies; consumers cannot mutate owned state.
- No Nodes, Resources, RIDs, files, jobs or worker threads are created.
- The supplied `Basis` is a Godot-world direction basis, not a typed world-position contract or coordinate conversion.

## Errors, cancellation and recovery

| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Negative physics delta | `advance` guard | Returns zero displacement; velocity unchanged | Unit regression | Resume with valid non-negative delta |
| Missing scene basis owner | Scene composition defect | Controller cannot be called correctly | Scene/parser error or boundary test | Restore explicit scene dependency |
| Flight toggle during motion | Command path | Velocity clears before next step | State-transition regression | Continue from zero velocity |
| Flight/sprint request in `Blizzlike335` | Capability check | Request has no effect | Profile regression | Select explicit `RenderSandbox` only for tooling/debug scenes |
| Unknown/null profile capability | Factory/constructor safe default | Uses `Blizzlike335`; debug movement disabled | Capability regression | Correct composition profile ID |
| Missing terrain data | `TerrainGroundSample.is_available == false` | Scene skips existing ground snap | Stable terrain failure code | Replace/recover query backend |

There is no asynchronous cancellation, retry or rollback. Recreating the
controller resets velocity and flight state deterministically.

## Configuration and capabilities

| Setting/capability | Default | Profile | Runtime mutable | Effect |
|---|---|---|---|---|
| Forward speed | `7.0` units/s | Current sandbox | Fixed for controller lifetime | Target forward velocity |
| Backward speed | `4.5` units/s | Current sandbox | Fixed for controller lifetime | Target backward velocity |
| Strafe speed | `4.5` units/s | Current sandbox | Fixed for controller lifetime | Target lateral velocity |
| Acceleration | `28.0` units/s² | Current sandbox | Fixed for controller lifetime | Acceleration and deceleration |
| Flight vertical speed | `7.0` units/s | Sandbox debug | Fixed for controller lifetime | Vertical free-flight target |
| Sprint multiplier | `6.0` | Sandbox debug | Fixed for controller lifetime | Multiplies planar and vertical targets |
| Camera-relative basis | Selected only while flight is enabled | Sandbox debug | Per tick | Preserves existing pitched flight direction |
| Debug sprint permission | Enabled | `RenderSandbox` only | Immutable | Allows sprint request multiplier |
| Debug free-flight permission | Enabled | `RenderSandbox` only | Immutable | Allows toggle and vertical movement |
| Compatibility-safe debug permissions | Disabled | `Blizzlike335` | Immutable | Ignores sprint/toggle requests |
| Keyboard turn rate | `3.141594` rad/s | `Blizzlike335` composition | Fixed for controller lifetime | Server-contract default applied to signed turn intent |

## Persistence, cache and migration

Movement state is transient and not serialized. No schema, cache, migration or
saved settings change is introduced. Future reconnect/replay state requires a
separate versioned movement snapshot contract.

## Diagnostics and observability

- Logs: verifier emits `LOCAL_PLAYER_MOVEMENT PASS` or named invariant failures.
- Metrics: none yet; update is constant-time vector math.
- Debug views: current character facing and animation remain observable scene outputs.
- Correlation IDs: not applicable to synchronous local sandbox state.

## Verification

- Unit/contract: `src/tools/verify_local_player_movement.gd` covers forward,
  backward, strafe, rotated basis, acceleration, deceleration, sprint, flight,
  pitched camera basis, state reset, signed keyboard turn and invalid delta.
- Capability: `verify_player_movement_capabilities.gd` covers three profile-ID
  cases, safe defaults, sprint/flight allow/reject behavior and two real scene compositions.
- Boundary: verifier rejects Node/Input/Camera/ADT dependencies and movement
  integration/state remaining in the player scene.
- Scene integration: `verify_player_input.gd` drives the asset-free player
  regression scene through forward motion and immediate flight toggle.
- Fidelity evidence: translation defaults remain regression-locked against the
  pre-extraction sandbox. The compatibility keyboard turn default is pinned to
  TrinityCore 3.3.5 `playerBaseMoveSpeed[MOVE_TURN_RATE]`; that server contract
  does not alone prove original-client timing parity.
- Performance budget: constant-time vector math, no per-tick I/O or scene lookup;
  `MoveIntent` allocation remains owned by the input package.

## Extension points

- Terrain/collision policy can consume displacement without changing input or velocity ownership.
- A future server-aware predictor may replace this controller behind the scene composition boundary.
- A typed movement snapshot may expose deterministic replay state when M08/M09 require it.
- A future application profile can map to this narrow capability value without
  coupling the movement controller to the application shell.

## Capability status

| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Scene-free local velocity state | Implemented | Pure controller and source boundary regression | Integrate target checklist after review |
| Existing planar acceleration/speeds | Implemented | Exact numeric unit cases and real scene regression | Compare with build 12340 before fidelity claim |
| Compatibility keyboard turn | Implemented | Pinned server-core value, signed pure cases and real-scene profile regression | Direct original-client timing comparison remains required |
| Existing camera-relative free flight | Implemented in `RenderSandbox` | Pitched-basis, profile and toggle/reset cases | Original-client flight semantics remain out of scope |
| Typed sprint/free-flight exclusion | Implemented | Pure and real-scene profile regressions | Application shell must select profile explicitly later |
| Terrain height query | Implemented | Typed `TerrainQuery` and injected player regression | Ground-snap policy remains scene-owned |
| Terrain collision movement policy | Planned | Height-only query does not model collision | Add slopes/holes/collision later |
| Jump/fall/swim | Planned | M02/M09 roadmap | Requires terrain/liquid and server contracts |
| Prediction/reconciliation | Planned | M08/M09 roadmap | Requires movement snapshot/network contract |

## Known gaps and risks

- Translation/acceleration defaults preserve the existing sandbox and are not original-client evidence.
- Configuration is captured at scene `_ready`; runtime mutation of exported speed
  properties does not reconfigure an existing controller.
- Ground snap remains in the player scene, but its ADT data access is now behind
  a replaceable `TerrainQuery`.
- Runtime renderer scenes intentionally default to `RenderSandbox`; a future
  game-client composition must explicitly select `Blizzlike335`.
- `Blizzlike335` currently means only debug movement exclusion and must not be
  interpreted as evidence that remaining movement semantics are 1:1.
- The module uses Godot math value types; a future engine-independent gameplay
  predictor may require scalar/domain movement contracts.

## Source map

| Path | Responsibility |
|---|---|
| `src/gameplay/movement/local_player_movement_controller.gd` | Owned velocity/flight state and deterministic integration |
| `src/gameplay/movement/player_movement_capabilities.gd` | Immutable profile-to-debug-capability contract and safe defaults |
| `src/gameplay/terrain/terrain_query.gd` | Replaceable ground-height input boundary |
| `src/scenes/player/third_person_camera_rig.gd` | Camera-relative sandbox flight basis producer |
| `src/scenes/character/character_animation_presenter.gd` | Locomotion presentation consumer behind the scene adapter |
| `src/scenes/player/third_person_wow_controller.gd` | Composition, basis selection, displacement/terrain and presentation-state adapters |
| `src/tools/verify_local_player_movement.gd` | Pure numeric, state-transition and dependency regression |
| `src/tools/verify_player_movement_capabilities.gd` | Profile factory, allow/reject and real-scene regression |
| `src/tests/scenes/player_input_regression.tscn` | Asset-free integrated player fixture |
| `src/tools/verify_player_input.gd` | Input-to-movement scene regression |
| `src/tests/fixtures/player_input_profile_335.json` | Turn-rate and selected binding provenance |

## Related decisions and references

- ADR: none; this follows the existing M02 decomposition boundary.
- Upstream/reference: `targets/02-player-decomposition.md`,
  `targets/roadmap/04-gameplay-systems.md`, `docs/ARCHITECTURE.md`.
