# Local Player Movement ## Metadata | Field | Value | |---|---| | Status | Implemented | | Target/work package | M02 / M02-GMP-MOVEMENT-001, M02-RND-CAMERA-001 basis producer update | | Owners | Gameplay local movement state; scene composition owns the instance | | Last verified | `8c1cf9b`, 2026-07-14 | | Profiles/capabilities | Current render sandbox; production/debug profile gate pending | ## 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 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 Movement --> Displacement[Godot-world displacement] 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 animation 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 | |---|---|---|---|---| | `LocalPlayerMovementController.new(...)` | Constructor | Sets explicit speeds, acceleration and sandbox sprint multiplier | Created by composition root; retained for player scene lifetime | Caller owns configuration validation | | `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 | | `toggle_sandbox_flight()` | Command | Toggles flight and clears velocity | Owning physics/input thread | Returns new flight state | | `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 | 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 | ## Inputs and outputs | Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime | |---|---|---|---|---|---| | Input | `MoveIntent` | `PlayerInputSource` | Movement controller | Immutable caller-held value | One physics tick | | 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 | Velocity `Vector3` | Movement controller | Facing/animation 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] Basis[Player or camera-pivot Basis] --> Directions[Forward/right directions] Directions --> Target Config[Speeds and sprint multiplier] --> Target 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 / clear velocity 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 Present as Facing/Animation adapter Input-->>Scene: MoveIntent 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. - 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 | | 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 | ## 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 and invalid delta. - 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: default values and formulas are regression-locked against the pre-extraction sandbox. No original-client comparison is claimed. - 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. - Sandbox debug capability gating can reject sprint/flight before commands reach this controller. ## 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 | | Existing camera-relative free flight | Implemented | Pitched-basis and toggle/reset cases | Restrict to sandbox profile | | 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 - Numeric defaults preserve the existing sandbox but 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`. - Sprint and free flight still lack a typed sandbox capability gate. - 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/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/player/third_person_wow_controller.gd` | Composition, basis selection, displacement/terrain/presentation adapters | | `src/tools/verify_local_player_movement.gd` | Pure numeric, state-transition and dependency regression | | `src/tests/scenes/player_input_regression.tscn` | Asset-free integrated player fixture | | `src/tools/verify_player_input.gd` | Input-to-movement scene regression | ## 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`.