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
14 KiB
Player Input
Metadata
| Field | Value |
|---|---|
| Status | Partial |
| Target/work package | M02 / M02-GMP-INPUT-001, M02-GMP-SANDBOX-CAPABILITIES-001, M02-GMP-INPUT-PROFILE-335-001 |
| Owners | Gameplay input boundary; sindo-main-codex for current package |
| Last verified | M02-GMP-INPUT-PROFILE-335-001 worktree, 2026-07-15 |
| Profiles/capabilities | Distinct RenderSandbox and Blizzlike335 action layouts; debug execution remains RenderSandbox-only |
Purpose
Convert remappable Godot Input Map actions into an immutable, scene-free
MoveIntent that can be consumed by the independently testable local movement
controller. Select either the preserved renderer-sandbox layout or the verified
build-12340 movement bindings at the scene composition boundary.
Non-goals
- Define authoritative server movement, prediction or reconciliation.
- Own character transform, terrain queries, camera state or animation state.
- Implement jump, autorun, left-button select/move or persisted binding settings.
- Remove process-wide debug actions; Blizzlike335 sampling ignores them and the movement capability boundary also rejects directly constructed debug requests.
Context and boundaries
flowchart LR
Devices[Keyboard and mouse] --> InputMap[Godot Input Map]
InputMap --> Source[PlayerInputSource]
Profile[RenderSandbox or Blizzlike335] --> Source
Source --> Intent[MoveIntent]
Intent --> Movement[LocalPlayerMovementController]
Profile[PlayerMovementCapabilities] --> Movement
Movement --> Controller[ThirdPersonWowController adapter]
Controller --> Camera[ThirdPersonCameraRig]
Allowed dependencies:
PlayerInputSourcemay read Godot's process-wideInputadapter.- The scene controller may consume
MoveIntentand camera-specific action names. MoveIntentmay contain scalar axes and request flags only.
Forbidden dependencies:
MoveIntentmust not readInput, inheritNode/Resource, or contain world coordinates.- The input source must not mutate player transforms, camera nodes or animation state.
- Renderer, terrain parsers and network codecs must not read Input Map actions.
Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
MoveIntent |
Immutable value | Carries normalized forward, strafe, vertical and turn axes plus the sandbox sprint request | Created and consumed on the main physics tick; caller retains reference | Constructor clamps axes; no I/O failure |
MoveIntent.has_translation() |
Query | Reports whether any translation axis is non-zero | Any thread while value remains immutable | None |
PlayerInputActions |
Constants | Provides stable action names shared by configuration and adapters | Process lifetime | Missing project actions are detected by verification |
PlayerInputSource.new(profile_id) |
Constructor | Selects sandbox or compatibility action layout once | Scene lifetime | Unknown IDs fail closed to Blizzlike335 |
PlayerInputSource.sample_move_intent() |
Adapter method | Samples the selected action layout for one physics tick | Main thread; returned intent is independent of adapter lifetime | Missing actions resolve to zero strength and are a configuration defect |
PlayerInputSource.compose_move_intent(...) |
Pure factory | Clamps strengths, cancels opposites and normalizes diagonal planar input | Any thread with scalar inputs | None |
PlayerInputSource.compose_blizzlike_335_move_intent(...) |
Pure factory | Composes Q/E strafe, A/D turn and right-mouse A/D strafe semantics | Any thread with scalar inputs | None |
Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Movement action strengths | Godot InputMap/Input |
PlayerInputSource |
Engine-owned snapshot | Sampled on main physics tick |
| Input | Mouse action events | Godot _unhandled_input dispatch |
ThirdPersonCameraRig through player dispatch |
Engine-owned event | Current input dispatch only |
| Output | MoveIntent |
PlayerInputSource |
LocalPlayerMovementController through scene composition |
Immutable caller-held value | One physics tick |
| Output | Debug sprint request/vertical axes | PlayerInputSource |
Capability-gated movement controller | Immutable intent fields | One physics tick |
| Output | Signed turn axis | PlayerInputSource |
LocalPlayerMovementController |
Immutable scalar | One physics tick |
| Output | Camera action names | PlayerInputActions |
ThirdPersonCameraRig |
Static constants | Process lifetime |
Side effects:
PlayerInputSourcehas no side effects beyond reading current engine input.ThirdPersonCameraRigowns mouse mode, orbit state and camera transforms; player remains only the event-dispatch composition boundary.- No filesystem, cache, network, database or renderer-resource mutation occurs.
Data flow
flowchart LR
Project[project.godot action defaults] --> Map[InputMap]
Remap[User or editor remapping] --> Map
Map -->|movement strengths| Source[PlayerInputSource]
Profile[Selected input profile] --> Source
Map -->|camera events| Camera[ThirdPersonCameraRig]
Source -->|clamp cancel normalize| Intent[MoveIntent]
Intent --> Consumer[LocalPlayerMovementController]
Profile[PlayerMovementCapabilities] --> Consumer
Consumer --> Adapter[ThirdPersonWowController]
Lifecycle/state
The adapter is stateless. The scene composition root creates one
PlayerInputSource and LocalPlayerMovementController in _ready; each physics
tick produces a new immutable intent. Flight enabled/disabled state is owned by
the movement controller and is not hidden in the input adapter. RenderSandbox
retains debug requests in its intent; Blizzlike335 does not sample them, and the
immutable movement capability also rejects directly constructed requests.
Main sequence
sequenceDiagram
participant Engine as Godot Input
participant Source as PlayerInputSource
participant Intent as MoveIntent
participant Move as LocalPlayerMovementController
participant Player as ThirdPersonWowController adapter
Engine->>Source: movement action strengths
Source->>Intent: compose normalized axes and requests
Source-->>Player: sample_move_intent()
Player->>Move: calculate yaw delta, then advance(intent, selected basis, delta)
Move-->>Player: displacement and velocity state
Ownership, threading and resources
- The controller owns the input-source reference for its scene lifetime.
- Each
MoveIntentis immutable and retained only by its consumer. - Sampling and controller mutation occur on the main physics thread.
- The module creates no Nodes, Resources, RIDs, workers or background jobs.
- Input Map definitions are project configuration; runtime remapping remains engine-owned and does not mutate the domain value.
Errors, cancellation and recovery
| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Required action missing | Headless contract verifier | Godot returns zero input for that action | PLAYER_INPUT failure naming action |
Restore action or user mapping |
| Conflicting opposite actions | Pure composition | Strengths cancel deterministically | Covered by contract test | Release/remap one action |
| Strength outside range | Pure composition | Value clamps to [0, 1] before axis calculation |
Covered by contract test | Input adapter may be corrected independently |
| Controller disabled/unloaded | Scene lifecycle | Sampling stops with physics processing | Existing scene lifecycle | Re-enable or recreate scene |
Debug request in Blizzlike335 |
Input profile and movement capability checks | Input adapter omits it; directly constructed request has no effect | Profile/capability regressions | Use RenderSandbox only when debug movement is intended |
There is no asynchronous operation to cancel and no persisted state to roll back.
Configuration and capabilities
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|---|---|---|---|---|
| Forward/backward | Physical W/S and Up/Down | Both | Yes, through Input Map | Produces signed forward axis |
| Strafe left/right | Physical A/D | RenderSandbox |
Yes | Preserves renderer baseline |
| Strafe left/right | Physical Q/E | Blizzlike335 |
Yes | Matches selected build-12340 defaults |
| Keyboard turn | Physical A/D and Left/Right | Blizzlike335 |
Yes | Produces signed turn axis |
| Camera-held A/D | Physical A/D while right mouse is held | Blizzlike335 |
Yes | Produces strafe and suppresses keyboard turn |
| Camera rotate | Right mouse button | Current render sandbox | Yes | Captures/releases mouse for orbit |
| Camera zoom | Mouse wheel | Current render sandbox | Yes | Adjusts current camera distance |
| Debug sprint | Shift | Executable only in RenderSandbox |
Yes | Requests multiplier; Blizzlike335 ignores it |
| Debug flight up/down/toggle | E/Q/Space | Executable only in RenderSandbox |
Yes | Requests free flight; Blizzlike335 remains grounded |
Persistence, cache and migration
The action names are additive project configuration. No saved binding schema, cache or migration exists yet. Renaming an action is a contract change and must provide a settings migration once user settings are persisted.
Diagnostics and observability
- Logs: dedicated verifier emits
PLAYER_INPUT PASSor one failure per invariant. - Metrics: none; sampling cost is one small value allocation per physics tick.
- Debug views: none.
- Correlation IDs: not applicable to local synchronous input.
Verification
- Unit/contract tests:
src/tools/verify_player_input.gdvalidates all actions, both profile defaults, axis cancellation/clamping/normalization, turn routing, safe profile fallback, fixture provenance and debug request flags. - Integration/E2E:
src/tests/scenes/player_input_regression.tscnis loaded by the verifier, which drives sandbox forward/camera/flight plus compatibility keyboard turn and right-mouse strafe through real scene controllers. - Profile integration:
verify_player_movement_capabilities.gddrives sprint and flight actions through realRenderSandboxandBlizzlike335scene instances. - Fidelity evidence: sanitized fixture
player_input_profile_335.jsonpins the private build-12340DefaultBindings.wtf/Bindings.xmlhashes and selected movement/camera commands. TrinityCore 3.3.5 revision2853a621d6af91a803787a2b8a509f4ce3e0300dpins the server-contract player turn rate. WoWee revision626243e937fb93965fa583a6507ed5a1aa7dda4bsupports right-mouse A/D strafe routing. Both are compatibility references, not direct original-client timing/conditional-behavior proof. - Performance budgets: no I/O, jobs or scene lookup per sample; one immutable intent allocation per physics tick.
- Manual diagnostics: run the Eastern Kingdoms scene and remap one movement action in Project Settings before checking movement.
Extension points
- A gamepad/accessibility adapter may call
compose_move_intentwithout changing movement consumers. - A later input-context owner may suppress gameplay actions while preserving the same intent contract.
- A future server-aware movement predictor may replace the current local
movement consumer without changing
PlayerInputSource.
Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Remappable current sandbox controls | Implemented | verify_player_input.gd action/default checks |
Add persisted user binding settings later |
PlayerInputSource → MoveIntent seam |
Implemented | Pure composition and controller boundary checks | Local movement consumer is now extracted |
| Selected 3.3.5a movement defaults | Implemented | Pinned private-source hashes, sanitized selected bindings and profile regressions | Jump, autorun and left-button behavior remain planned |
| Compatibility-profile exclusion of sprint/flight | Implemented | Typed capability and two real-scene regressions | Application shell selection remains future work |
| Jump/fall/swim/fly gameplay semantics | Planned | M02 target and gameplay roadmap | Implement after terrain/movement state extraction |
Known gaps and risks
- Only selected movement/camera binding facts are implemented; the fixture explicitly records unimplemented defaults.
- Input Map still exposes debug bindings process-wide; the movement consumer now
rejects their effects outside
RenderSandbox. - Camera state is extracted; active collision remains an explicit planned policy.
- There is no persisted user keybinding format or UI yet.
Source map
| Path | Responsibility |
|---|---|
project.godot |
Additive default Input Map bindings |
src/domain/input/move_intent.gd |
Immutable locomotion request contract |
src/gameplay/input/player_input_actions.gd |
Stable project action names |
src/gameplay/input/player_input_source.gd |
Engine Input Map adapter and pure composition |
src/gameplay/movement/local_player_movement_controller.gd |
Scene-free movement consumer |
src/gameplay/movement/player_movement_capabilities.gd |
Defense-in-depth execution gate for debug requests |
src/scenes/player/third_person_camera_rig.gd |
Camera-action consumer and orbit/zoom state |
src/scenes/player/third_person_wow_controller.gd |
Composition and input-event dispatcher |
src/tests/scenes/player_input_regression.tscn |
Asset-free controller movement/camera regression fixture |
src/tests/fixtures/player_input_profile_335.json |
Sanitized build-12340 binding and server turn-rate provenance |
src/tools/verify_player_input.gd |
Headless contract/integration regression |
src/tools/verify_player_movement_capabilities.gd |
Sandbox versus compatibility-profile integration regression |
Related decisions and references
- ADR: none; this implements the existing architecture and M02 seam.
- Upstream/reference:
targets/02-player-decomposition.md,targets/roadmap/04-gameplay-systems.md,docs/ARCHITECTURE.md, and the pinned sources recorded inplayer_input_profile_335.json.