Work-Package: M02-GMP-INPUT-PROFILE-335-001 Agent: sindo-main-codex Tests: player input/movement/capability/terrain/camera/presentation, coordinate/streaming, docs/coordination and renderer dry-run passed Fidelity: build-12340 binding hashes and selected defaults pinned; TrinityCore turn rate and WoWee mouse routing are compatibility references with explicit limits
17 KiB
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
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:
MoveIntentfrom the gameplay input boundary.- Godot value types
BasisandVector3for renderer world-space direction math. - Main-thread scene adapter for basis selection and displacement application.
Forbidden dependencies:
Node,Resource,Input,Camera3D,ADTLoaderor 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
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
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
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
Basisis 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 PASSor 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.gdcovers 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.gdcovers 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.gddrives 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;
MoveIntentallocation 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 selectBlizzlike335. Blizzlike335currently 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.