# 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

```mermaid
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:

- `PlayerInputSource` may read Godot's process-wide `Input` adapter.
- The scene controller may consume `MoveIntent` and camera-specific action names.
- `MoveIntent` may contain scalar axes and request flags only.

Forbidden dependencies:

- `MoveIntent` must not read `Input`, inherit `Node`/`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:

- `PlayerInputSource` has no side effects beyond reading current engine input.
- `ThirdPersonCameraRig` owns 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

```mermaid
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

```mermaid
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 `MoveIntent` is 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 PASS` or 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.gd` validates 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.tscn` is 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.gd` drives sprint
  and flight actions through real `RenderSandbox` and `Blizzlike335` scene instances.
- Fidelity evidence: sanitized fixture `player_input_profile_335.json` pins the
  private build-12340 `DefaultBindings.wtf`/`Bindings.xml` hashes and selected
  movement/camera commands. TrinityCore 3.3.5 revision
  `2853a621d6af91a803787a2b8a509f4ce3e0300d` pins the server-contract player
  turn rate. WoWee revision `626243e937fb93965fa583a6507ed5a1aa7dda4b`
  supports 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_intent` without 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 in `player_input_profile_335.json`.
