# Player Input ## Metadata | Field | Value | |---|---| | Status | Partial | | Target/work package | M02 / M02-GMP-INPUT-001, M02-GMP-MOVEMENT-001 and M02-RND-CAMERA-001 consumer updates | | Owners | Gameplay input boundary; `sindo-main-codex` for current package | | Last verified | `working tree`, 2026-07-14 | | Profiles/capabilities | Current render-sandbox defaults; `Blizzlike335` binding semantics not yet verified | ## Purpose Convert remappable Godot Input Map actions into an immutable, scene-free `MoveIntent` that can be consumed by the current player controller and a later independently testable local movement controller. ## Non-goals - Define authoritative server movement, prediction or reconciliation. - Own character transform, terrain queries, camera state or animation state. - Claim that the current W/S/A/D sandbox mapping exactly matches build 12340. - Make sprint or free flight available to the production compatibility profile; their profile gate remains a later M02 package. ## Context and boundaries ```mermaid flowchart LR Devices[Keyboard and mouse] --> InputMap[Godot Input Map] InputMap --> Source[PlayerInputSource] Source --> Intent[MoveIntent] Intent --> Movement[LocalPlayerMovementController] 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 and vertical 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.sample_move_intent()` | Adapter method | Samples configured actions 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 | ## 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 | 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] Map -->|camera events| Camera[ThirdPersonCameraRig] Source -->|clamp cancel normalize| Intent[MoveIntent] Intent --> Consumer[LocalPlayerMovementController] 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. ## 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: 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 | 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 | Current render sandbox | Yes, through Input Map | Produces signed forward axis | | Strafe left/right | Physical A/D | Current render sandbox | Yes | Produces signed right axis | | 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 | Sandbox debug only by intent; profile gate pending | Yes | Requests existing speed multiplier | | Debug flight up/down/toggle | E/Q/Space | Sandbox debug only by intent; profile gate pending | Yes | Requests existing free-flight controls | ## 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, default bindings, axis cancellation/clamping/normalization and debug request flags. - Integration/E2E: `src/tests/scenes/player_input_regression.tscn` is loaded by the verifier, which drives forward, camera-zoom and immediate sandbox-flight actions through the real scene controller and also rejects direct physical-key polling. - Fidelity evidence: current sandbox defaults retain W/S/A/D, E/Q, Shift, Space, right mouse and wheel bindings. This is observable-regression coverage only. - 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 | | Exact 3.3.5a default semantics | Unknown | No original-client binding fixture in this package | Capture build-12340 defaults and mouse-turn/strafe policy | | Production profile exclusion of sprint/flight | Partial | Debug actions are named explicitly | Add typed profile/capability gate in M02 | | Jump/fall/swim/fly gameplay semantics | Planned | M02 target and gameplay roadmap | Implement after terrain/movement state extraction | ## Known gaps and risks - The defaults preserve the pre-M02 sandbox behavior, not verified original-client semantics. - Sprint and free flight are still executable by the sandbox controller until a typed build-profile gate is introduced. - 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/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/tools/verify_player_input.gd` | Headless contract/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`.