From 8776a6b3621a9912c623c900fec417f12aa715a8 Mon Sep 17 00:00:00 2001 From: sindoring Date: Wed, 15 Jul 2026 00:17:34 +0400 Subject: [PATCH 1/2] gmp(M02): add 3.3.5 input profile 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 --- .../claims/M02-GMP-INPUT-PROFILE-335-001.md | 84 ++++++++++++++++++ docs/modules/local-player-movement.md | 33 +++++-- docs/modules/player-input.md | 70 +++++++++------ docs/modules/third-person-camera.md | 8 +- project.godot | 24 +++++ src/domain/input/move_intent.gd | 10 ++- src/gameplay/input/player_input_actions.gd | 12 ++- src/gameplay/input/player_input_source.gd | 87 ++++++++++++++++--- .../local_player_movement_controller.gd | 18 +++- src/scenes/player/third_person_camera_rig.gd | 6 ++ .../player/third_person_wow_controller.gd | 15 +++- .../fixtures/player_input_profile_335.json | 57 ++++++++++++ src/tools/verify_local_player_movement.gd | 20 ++++- src/tools/verify_player_input.gd | 83 +++++++++++++++++- 14 files changed, 468 insertions(+), 59 deletions(-) create mode 100644 coordination/claims/M02-GMP-INPUT-PROFILE-335-001.md create mode 100644 src/tests/fixtures/player_input_profile_335.json diff --git a/coordination/claims/M02-GMP-INPUT-PROFILE-335-001.md b/coordination/claims/M02-GMP-INPUT-PROFILE-335-001.md new file mode 100644 index 0000000..0783505 --- /dev/null +++ b/coordination/claims/M02-GMP-INPUT-PROFILE-335-001.md @@ -0,0 +1,84 @@ +# M02-GMP-INPUT-PROFILE-335-001 — WoW 3.3.5a input profile + + + +## Ownership + +- Target: M02 +- Program: GMP/QAR +- Owner/Agent ID: sindo-main-codex +- Branch: `work/sindo-main-codex/m02-input-profile-335` +- Lease expires UTC: 2026-07-17 +- Integrator: M02 milestone integrator + +## Outcome + +Add a profile-aware, remappable `Blizzlike335` input path whose default movement +bindings match the extracted build-12340 binding table while preserving the +existing `RenderSandbox` controls. Add deterministic turn intent and keyboard +turn consumption at the server-contract default turn rate. + +## Non-goals + +- Implement jump/fall/swim, network prediction or server reconciliation. +- Claim complete movement or camera parity from binding and turn-rate evidence. +- Persist user keybindings or build keybinding UI. +- Commit proprietary extracted client files. +- Mark M02 complete or edit its checklist/Evidence. + +## Paths + +- Exclusive: `src/tests/fixtures/player_input_profile_335.json`, this claim +- Shared/hotspots: `project.godot`, `src/domain/input/move_intent.gd`, + `src/gameplay/input/player_input_actions.gd`, + `src/gameplay/input/player_input_source.gd`, + `src/gameplay/movement/local_player_movement_controller.gd`, + `src/scenes/player/third_person_wow_controller.gd`, + `src/scenes/player/third_person_camera_rig.gd`, player/movement verifiers + and regression scene, `docs/modules/player-input.md`, + `docs/modules/local-player-movement.md`, `docs/modules/third-person-camera.md` +- Generated/ignored: local `.godot`, native DLL and proprietary extracted corpus + +## Contracts and data + +- `MoveIntent` gains an immutable, clamped signed turn axis. +- `PlayerInputSource` selects explicit sandbox or compatibility action sets at + construction and routes compatibility A/D to strafe while camera rotation is held. +- `LocalPlayerMovementController` converts turn intent into a deterministic yaw + delta without reading engine input or owning a scene transform. +- A sanitized JSON fixture records selected binding facts, source hashes and the + pinned TrinityCore 3.3.5 server-contract commit. + +## Dependencies + +- Requires: merged sandbox capability package on master `c3094c9`. +- Blocks: final M02 integration audit and milestone closeout. +- External state: local extracted build-12340 binding files are evidence inputs + only; committed tests are reproducible without them. + +## Verification + +- Fixture schema/provenance and selected default-binding regression. +- Pure profile composition, A/D turn, Q/E strafe and camera-held A/D strafe cases. +- Deterministic turn-rate and controller integration regression. +- Existing M02 and repository gates remain required. + +## Documentation deliverables + +- Updated input/movement public API, profile matrix, data flow, sequence, + failure/recovery, fidelity provenance and source maps. + +## Simplicity and naming + +- Important names: `turn_axis`, `BLIZZLIKE_STRAFE_LEFT`, `TURN_LEFT`. +- Simplest approach: one profile selection at composition plus one scalar intent. +- Rejected complexity: input-context framework, command bus and persisted binding store. +- Unavoidable complexity: compatibility A/D behavior depends on camera-rotate state. +- Measured optimization evidence: not applicable. + +## Status + +- State: active +- Done: audited current mappings and captured build-12340/source-core provenance +- Next: implement contracts, profile mapping, regression fixtures and docs +- Blocked by: diff --git a/docs/modules/local-player-movement.md b/docs/modules/local-player-movement.md index 4175ca8..b678b83 100644 --- a/docs/modules/local-player-movement.md +++ b/docs/modules/local-player-movement.md @@ -5,17 +5,18 @@ | Field | Value | |---|---| | Status | Implemented | -| Target/work package | M02 / M02-GMP-MOVEMENT-001, M02-GMP-SANDBOX-CAPABILITIES-001 and presentation consumers | +| 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 | `742c415`, 2026-07-14 | -| Profiles/capabilities | `RenderSandbox` debug movement; `Blizzlike335` rejects debug sprint/flight | +| 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 without -querying terrain, cameras, input devices or visual assets. +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 @@ -35,6 +36,7 @@ flowchart LR 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 @@ -68,6 +70,7 @@ Forbidden dependencies: | `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: @@ -79,6 +82,7 @@ Constructor units: | `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 @@ -90,6 +94,7 @@ Constructor units: | 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 | @@ -104,6 +109,8 @@ Side effects: ```mermaid 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 @@ -144,6 +151,9 @@ sequenceDiagram 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 @@ -190,6 +200,7 @@ controller resets velocity and flight state deterministically. | 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 @@ -208,15 +219,17 @@ separate versioned movement snapshot contract. - 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. + pitched camera basis, state reset, signed keyboard turn and invalid delta. - Capability: `verify_player_movement_capabilities.gd` covers 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.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. +- 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; `MoveIntent` allocation remains owned by the input package. @@ -234,6 +247,7 @@ separate versioned movement snapshot contract. |---|---|---|---| | 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 | @@ -243,7 +257,7 @@ separate versioned movement snapshot contract. ## Known gaps and risks -- Numeric defaults preserve the existing sandbox but are not original-client evidence. +- 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 @@ -269,6 +283,7 @@ separate versioned movement snapshot contract. | `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 diff --git a/docs/modules/player-input.md b/docs/modules/player-input.md index 8b73afd..661dc5e 100644 --- a/docs/modules/player-input.md +++ b/docs/modules/player-input.md @@ -5,24 +5,25 @@ | Field | Value | |---|---| | Status | Partial | -| Target/work package | M02 / input, movement, camera and sandbox-capability consumer updates | +| 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 | `742c415`, 2026-07-14 | -| Profiles/capabilities | Debug requests sampled in all profiles; execution allowed only by `RenderSandbox` movement capabilities | +| 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 current player controller and a later -independently testable local movement controller. +`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. -- Claim that the current W/S/A/D sandbox mapping exactly matches build 12340. -- Suppress debug action sampling by physical device or UI context; execution is - rejected at the typed movement capability boundary. +- 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 @@ -30,6 +31,7 @@ independently testable local movement controller. 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 @@ -53,11 +55,13 @@ Forbidden dependencies: | 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` | 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.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.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 @@ -67,6 +71,7 @@ Forbidden dependencies: | 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: @@ -83,6 +88,7 @@ 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] @@ -95,9 +101,9 @@ flowchart LR 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. Debug requests -remain data in the intent; the immutable movement capability value decides -whether they execute. +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 @@ -111,7 +117,7 @@ sequenceDiagram Engine->>Source: movement action strengths Source->>Intent: compose normalized axes and requests Source-->>Player: sample_move_intent() - Player->>Move: advance(intent, selected basis, delta) + Player->>Move: calculate yaw delta, then advance(intent, selected basis, delta) Move-->>Player: displacement and velocity state ``` @@ -132,7 +138,7 @@ sequenceDiagram | 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` | Movement capability check | Request is sampled but has no movement effect | Capability regression | Use `RenderSandbox` only when debug movement is intended | +| 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. @@ -140,8 +146,11 @@ There is no asynchronous operation to cancel and no persisted state to roll back | 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 | +| 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 | @@ -163,15 +172,20 @@ provide a settings migration once user settings are persisted. ## Verification - Unit/contract tests: `src/tools/verify_player_input.gd` validates all actions, - default bindings, axis cancellation/clamping/normalization and debug request flags. + 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 forward, camera-zoom and immediate sandbox-flight - actions through the real scene controller and also rejects direct - physical-key polling. + 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: current sandbox defaults retain W/S/A/D, E/Q, Shift, Space, - right mouse and wheel bindings. This is observable-regression coverage only. +- 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 @@ -192,13 +206,13 @@ provide a settings migration once user settings are persisted. |---|---|---|---| | 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 | +| 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 -- The defaults preserve the pre-M02 sandbox behavior, not verified original-client semantics. +- 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. @@ -213,10 +227,11 @@ provide a settings migration once user settings are persisted. | `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` | Execution gate for sampled debug requests | +| `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 | @@ -224,4 +239,5 @@ provide a settings migration once user settings are persisted. - 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`. + `targets/roadmap/04-gameplay-systems.md`, `docs/ARCHITECTURE.md`, and the + pinned sources recorded in `player_input_profile_335.json`. diff --git a/docs/modules/third-person-camera.md b/docs/modules/third-person-camera.md index 874436f..4856333 100644 --- a/docs/modules/third-person-camera.md +++ b/docs/modules/third-person-camera.md @@ -5,9 +5,9 @@ | Field | Value | |---|---| | Status | Implemented | -| Target/work package | M02 / M02-RND-CAMERA-001 | +| Target/work package | M02 / M02-RND-CAMERA-001 and M02-GMP-INPUT-PROFILE-335-001 consumer update | | Owners | Camera presentation state and CameraPivot/Camera3D lifecycle | -| Last verified | `8c1cf9b`, 2026-07-14 | +| Last verified | M02-GMP-INPUT-PROFILE-335-001 worktree, 2026-07-15 | | Profiles/capabilities | Current sandbox orbit/zoom; identity collision policy | ## Purpose @@ -60,6 +60,7 @@ Forbidden dependencies: |---|---|---|---|---| | `ThirdPersonCameraRig` | `Node3D` component | Owns orbit/zoom state on CameraPivot | Main thread; scene-owned | Reports missing Camera3D | | `initialize_for_character(character_body)` | Composition method | Attaches yaw target and captures initial character yaw | Main thread after scene ready | Null leaves yaw target absent | +| `synchronize_yaw_from_character()` | Composition method | Refreshes orbit yaw after keyboard movement turns the character | Main physics thread | Missing character leaves yaw unchanged | | `handle_camera_input(event)` | Input adapter | Applies capture/release/zoom/orbit actions | Main input thread | Returns whether camera consumed state | | `set_collision_policy(policy)` | Composition method | Replaces distance resolution policy | Main thread | Null rejected; old policy retained | | `refresh_camera_transform()` | Presentation update | Applies pivot pose and resolved camera distance | Main/physics thread | Missing camera leaves state but no Camera3D mutation | @@ -77,6 +78,7 @@ Read-only rig state: `yaw_radians`, `pitch_radians`, | Input | Camera action `InputEvent` | Godot/player dispatch | Camera rig | Engine-owned event | One dispatch | | Input | Mouse relative pixels | Godot input | Camera rig | Engine-owned event | One dispatch | | Input | Character `Node3D` | Player composition | Camera rig | Scene-owned reference | Rig scene lifetime | +| Input | Keyboard-turn synchronization | Player scene after yaw application | Camera rig | Synchronous notification | Current physics tick | | Input | Collision policy | Composition/test | Camera rig | Rig-held RefCounted | Until replacement | | Output | Character yaw | Camera rig | Player transform | Scene mutation | Orbit event | | Output | Pivot pitch/height | Camera rig | CameraPivot | Scene mutation | Refresh | @@ -194,6 +196,8 @@ tools require no path migration. - State regression: initial yaw/pitch/distance/height/far plane, capture/release, mouse orbit, character yaw, pitch clamps and zoom clamps. +- Input-profile integration verifies keyboard yaw synchronization, preventing a + later right-mouse orbit from restoring stale character yaw. - Policy regression: fixed-distance test policy changes Camera3D Z; identity policy restores requested distance; baseline policy contains no physics query. - Scene integration: Eastern Kingdoms, Kalimdor and asset-free regression scenes diff --git a/project.godot b/project.godot index 6f00c0e..3bfa0ba 100644 --- a/project.godot +++ b/project.godot @@ -24,11 +24,13 @@ enabled=PackedStringArray("res://addons/mpq_extractor/plugin.cfg") openwc_player_move_forward={ "deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194320,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } openwc_player_move_backward={ "deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194322,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } openwc_player_strafe_left={ @@ -41,6 +43,28 @@ openwc_player_strafe_right={ "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null) ] } +openwc_player_blizzlike_strafe_left={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":81,"key_label":0,"unicode":113,"location":0,"echo":false,"script":null) +] +} +openwc_player_blizzlike_strafe_right={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null) +] +} +openwc_player_turn_left={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194319,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +openwc_player_turn_right={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194321,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} openwc_player_debug_fly_up={ "deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null) diff --git a/src/domain/input/move_intent.gd b/src/domain/input/move_intent.gd index ad3660c..25c2573 100644 --- a/src/domain/input/move_intent.gd +++ b/src/domain/input/move_intent.gd @@ -21,10 +21,16 @@ var is_sprint_requested: bool: get: return _is_sprint_requested +## Signed keyboard turn request: left is negative and right is positive. +var turn_axis: float: + get: + return _turn_axis + var _forward_axis: float var _strafe_axis: float var _vertical_axis: float var _is_sprint_requested: bool +var _turn_axis: float ## Creates one frame's movement request without retaining mutable input state. @@ -32,12 +38,14 @@ func _init( forward_axis_value: float = 0.0, strafe_axis_value: float = 0.0, vertical_axis_value: float = 0.0, - sprint_requested: bool = false + sprint_requested: bool = false, + turn_axis_value: float = 0.0 ) -> void: _forward_axis = clampf(forward_axis_value, -1.0, 1.0) _strafe_axis = clampf(strafe_axis_value, -1.0, 1.0) _vertical_axis = clampf(vertical_axis_value, -1.0, 1.0) _is_sprint_requested = sprint_requested + _turn_axis = clampf(turn_axis_value, -1.0, 1.0) ## Returns true when the intent requests planar or vertical translation. diff --git a/src/gameplay/input/player_input_actions.gd b/src/gameplay/input/player_input_actions.gd index 3a9992a..26d72f8 100644 --- a/src/gameplay/input/player_input_actions.gd +++ b/src/gameplay/input/player_input_actions.gd @@ -2,13 +2,17 @@ class_name PlayerInputActions extends RefCounted ## Stable Input Map action names consumed by the runtime player input adapter. -## Defaults preserve the current render-sandbox controls; users may remap every -## action through Godot's Input Map without changing gameplay code. +## Separate translation actions preserve the renderer sandbox while allowing +## Blizzlike335 defaults to match build-12340 movement bindings. const MOVE_FORWARD := &"openwc_player_move_forward" const MOVE_BACKWARD := &"openwc_player_move_backward" const STRAFE_LEFT := &"openwc_player_strafe_left" const STRAFE_RIGHT := &"openwc_player_strafe_right" +const BLIZZLIKE_STRAFE_LEFT := &"openwc_player_blizzlike_strafe_left" +const BLIZZLIKE_STRAFE_RIGHT := &"openwc_player_blizzlike_strafe_right" +const TURN_LEFT := &"openwc_player_turn_left" +const TURN_RIGHT := &"openwc_player_turn_right" const DEBUG_FLY_UP := &"openwc_player_debug_fly_up" const DEBUG_FLY_DOWN := &"openwc_player_debug_fly_down" const DEBUG_SPRINT := &"openwc_player_debug_sprint" @@ -23,6 +27,10 @@ const REQUIRED_ACTIONS: Array[StringName] = [ MOVE_BACKWARD, STRAFE_LEFT, STRAFE_RIGHT, + BLIZZLIKE_STRAFE_LEFT, + BLIZZLIKE_STRAFE_RIGHT, + TURN_LEFT, + TURN_RIGHT, DEBUG_FLY_UP, DEBUG_FLY_DOWN, DEBUG_SPRINT, diff --git a/src/gameplay/input/player_input_source.gd b/src/gameplay/input/player_input_source.gd index 9ceb1c0..e4709f1 100644 --- a/src/gameplay/input/player_input_source.gd +++ b/src/gameplay/input/player_input_source.gd @@ -5,21 +5,86 @@ extends RefCounted ## This adapter owns no movement state and may be replaced independently of the ## local movement controller. +const RENDER_SANDBOX_PROFILE_ID := &"RenderSandbox" +const BLIZZLIKE_335_PROFILE_ID := &"Blizzlike335" -## Samples the current Input singleton state for one physics tick. -func sample_move_intent() -> MoveIntent: - return compose_move_intent( - Input.get_action_strength(PlayerInputActions.MOVE_FORWARD), - Input.get_action_strength(PlayerInputActions.MOVE_BACKWARD), - Input.get_action_strength(PlayerInputActions.STRAFE_LEFT), - Input.get_action_strength(PlayerInputActions.STRAFE_RIGHT), - Input.get_action_strength(PlayerInputActions.DEBUG_FLY_UP), - Input.get_action_strength(PlayerInputActions.DEBUG_FLY_DOWN), - Input.is_action_pressed(PlayerInputActions.DEBUG_SPRINT) +var input_profile_id: StringName: + get: + return _input_profile_id + +var _input_profile_id: StringName + + +## Selects one supported action layout. Unknown profiles fail closed to Blizzlike335. +func _init(requested_profile_id: StringName = BLIZZLIKE_335_PROFILE_ID) -> void: + _input_profile_id = ( + RENDER_SANDBOX_PROFILE_ID + if requested_profile_id == RENDER_SANDBOX_PROFILE_ID + else BLIZZLIKE_335_PROFILE_ID ) -## Builds a normalized intent from action strengths for deterministic tests and +## Samples the current Input singleton state for one physics tick. +func sample_move_intent() -> MoveIntent: + if _input_profile_id == RENDER_SANDBOX_PROFILE_ID: + return compose_move_intent( + Input.get_action_strength(PlayerInputActions.MOVE_FORWARD), + Input.get_action_strength(PlayerInputActions.MOVE_BACKWARD), + Input.get_action_strength(PlayerInputActions.STRAFE_LEFT), + Input.get_action_strength(PlayerInputActions.STRAFE_RIGHT), + Input.get_action_strength(PlayerInputActions.DEBUG_FLY_UP), + Input.get_action_strength(PlayerInputActions.DEBUG_FLY_DOWN), + Input.is_action_pressed(PlayerInputActions.DEBUG_SPRINT) + ) + return compose_blizzlike_335_move_intent( + Input.get_action_strength(PlayerInputActions.MOVE_FORWARD), + Input.get_action_strength(PlayerInputActions.MOVE_BACKWARD), + Input.get_action_strength(PlayerInputActions.BLIZZLIKE_STRAFE_LEFT), + Input.get_action_strength(PlayerInputActions.BLIZZLIKE_STRAFE_RIGHT), + Input.get_action_strength(PlayerInputActions.TURN_LEFT), + Input.get_action_strength(PlayerInputActions.TURN_RIGHT), + Input.is_action_pressed(PlayerInputActions.CAMERA_ROTATE) + ) + + +## Builds the compatibility layout: Q/E always strafe; A/D turn unless +## right-mouse camera rotation is active, when A/D also strafe. Default bindings +## come from build 12340; conditional routing is reference-implementation evidence. +static func compose_blizzlike_335_move_intent( + forward_strength: float, + backward_strength: float, + strafe_left_strength: float, + strafe_right_strength: float, + turn_left_strength: float, + turn_right_strength: float, + camera_rotate_active: bool +) -> MoveIntent: + var effective_strafe_left := strafe_left_strength + var effective_strafe_right := strafe_right_strength + var turn_axis := clampf(turn_right_strength, 0.0, 1.0) - clampf(turn_left_strength, 0.0, 1.0) + if camera_rotate_active: + effective_strafe_left += turn_left_strength + effective_strafe_right += turn_right_strength + turn_axis = 0.0 + var move_intent := compose_move_intent( + forward_strength, + backward_strength, + effective_strafe_left, + effective_strafe_right, + 0.0, + 0.0, + false + ) + return MoveIntent.new( + move_intent.forward_axis, + move_intent.strafe_axis, + 0.0, + false, + turn_axis + ) + + +## Builds a normalized sandbox intent from action strengths for deterministic tests and ## alternative input adapters. Strengths outside `[0, 1]` are clamped. static func compose_move_intent( forward_strength: float, diff --git a/src/gameplay/movement/local_player_movement_controller.gd b/src/gameplay/movement/local_player_movement_controller.gd index 7468394..5f1f375 100644 --- a/src/gameplay/movement/local_player_movement_controller.gd +++ b/src/gameplay/movement/local_player_movement_controller.gd @@ -23,6 +23,7 @@ var _strafe_speed_units_per_second: float var _flight_vertical_speed_units_per_second: float var _acceleration_units_per_second_squared: float var _sandbox_sprint_multiplier: float +var _keyboard_turn_speed_radians_per_second: float var _movement_capabilities: PlayerMovementCapabilities var _godot_world_velocity_units_per_second := Vector3.ZERO var _is_flight_enabled := false @@ -38,7 +39,8 @@ func _init( flight_vertical_speed_units_per_second: float, acceleration_units_per_second_squared: float, sandbox_sprint_multiplier: float, - movement_capabilities: PlayerMovementCapabilities = null + movement_capabilities: PlayerMovementCapabilities = null, + keyboard_turn_speed_radians_per_second: float = PI ) -> void: _run_speed_units_per_second = run_speed_units_per_second _backward_speed_units_per_second = backward_speed_units_per_second @@ -46,6 +48,10 @@ func _init( _flight_vertical_speed_units_per_second = flight_vertical_speed_units_per_second _acceleration_units_per_second_squared = acceleration_units_per_second_squared _sandbox_sprint_multiplier = sandbox_sprint_multiplier + _keyboard_turn_speed_radians_per_second = maxf( + keyboard_turn_speed_radians_per_second, + 0.0 + ) _movement_capabilities = ( movement_capabilities if movement_capabilities != null @@ -53,6 +59,16 @@ func _init( ) +## Returns the Godot yaw delta for one signed keyboard-turn request. +## Positive intent turns right, which is negative rotation around Godot +Y. +func calculate_yaw_delta_radians(move_intent: MoveIntent, delta_seconds: float) -> float: + return ( + -move_intent.turn_axis + * _keyboard_turn_speed_radians_per_second + * maxf(delta_seconds, 0.0) + ) + + ## Toggles sandbox free flight and clears velocity when the profile permits it. ## Returns the resulting state; rejected compatibility-profile requests return false. func toggle_sandbox_flight() -> bool: diff --git a/src/scenes/player/third_person_camera_rig.gd b/src/scenes/player/third_person_camera_rig.gd index b6b3943..56a8e55 100644 --- a/src/scenes/player/third_person_camera_rig.gd +++ b/src/scenes/player/third_person_camera_rig.gd @@ -94,6 +94,12 @@ func initialize_for_character(character_body: Node3D) -> void: refresh_camera_transform() +## Synchronizes orbit yaw after keyboard turning changes the character transform. +func synchronize_yaw_from_character() -> void: + if _character_body != null: + _yaw_radians = _character_body.rotation.y + + ## Replaces camera-distance collision behavior and immediately refreshes the camera. ## Passing null is rejected and preserves the current policy. func set_collision_policy(collision_policy: CameraCollisionPolicy) -> void: diff --git a/src/scenes/player/third_person_wow_controller.gd b/src/scenes/player/third_person_wow_controller.gd index 3f6219e..2c5cb83 100644 --- a/src/scenes/player/third_person_wow_controller.gd +++ b/src/scenes/player/third_person_wow_controller.gd @@ -22,6 +22,8 @@ const ADT_TERRAIN_QUERY_SCRIPT := preload("res://src/gameplay/terrain/adt_terrai @export var sprint_multiplier: float = 6.0 @export var flight_vertical_speed: float = 7.0 @export var acceleration: float = 28.0 +## TrinityCore 3.3.5 player MOVE_TURN_RATE default, in radians per second. +@export var keyboard_turn_speed_radians_per_second: float = 3.141594 ## Local composition profile. RenderSandbox preserves debug sprint/free flight; ## Blizzlike335 rejects them but does not yet claim complete movement parity. @export_enum("RenderSandbox", "Blizzlike335") var movement_profile_id: String = "RenderSandbox" @@ -50,7 +52,7 @@ func set_terrain_query(terrain_query: TerrainQuery) -> void: func _ready() -> void: - _player_input_source = PLAYER_INPUT_SOURCE_SCRIPT.new() + _player_input_source = PLAYER_INPUT_SOURCE_SCRIPT.new(StringName(movement_profile_id)) if _terrain_query == null: _terrain_query = ADT_TERRAIN_QUERY_SCRIPT.new(extracted_dir, map_name) _local_movement_controller = LOCAL_PLAYER_MOVEMENT_CONTROLLER_SCRIPT.new( @@ -60,7 +62,8 @@ func _ready() -> void: flight_vertical_speed, acceleration, sprint_multiplier, - PLAYER_MOVEMENT_CAPABILITIES_SCRIPT.for_profile_id(StringName(movement_profile_id)) + PLAYER_MOVEMENT_CAPABILITIES_SCRIPT.for_profile_id(StringName(movement_profile_id)), + keyboard_turn_speed_radians_per_second ) _camera_rig = get_node_or_null(camera_pivot_path) as ThirdPersonCameraRig _appearance_presenter = get_node_or_null(visual_path) as CharacterAppearancePresenter @@ -96,6 +99,14 @@ func _unhandled_input(event: InputEvent) -> void: func _physics_process(delta: float) -> void: var move_intent := _player_input_source.sample_move_intent() + var yaw_delta_radians := _local_movement_controller.calculate_yaw_delta_radians( + move_intent, + delta + ) + if not is_zero_approx(yaw_delta_radians): + rotation.y += yaw_delta_radians + if _camera_rig != null: + _camera_rig.synchronize_yaw_from_character() var godot_world_movement_basis := global_basis if _local_movement_controller.is_flight_enabled and _camera_rig != null: godot_world_movement_basis = _camera_rig.godot_world_flight_movement_basis() diff --git a/src/tests/fixtures/player_input_profile_335.json b/src/tests/fixtures/player_input_profile_335.json new file mode 100644 index 0000000..c5c0fd9 --- /dev/null +++ b/src/tests/fixtures/player_input_profile_335.json @@ -0,0 +1,57 @@ +{ + "schema_version": 1, + "profile_id": "Blizzlike335", + "sources": { + "original_client_default_bindings": { + "status": "verified_private_metadata", + "build": 12340, + "path": "WTF/DefaultBindings.wtf", + "sha256": "35FFA0E4E2356A13A0633A848F9E57ABE7DF9D8A16C2245E15D33013AD2F56A5", + "distribution": "Selected binding facts only; proprietary file remains outside Git." + }, + "original_client_binding_commands": { + "status": "verified_private_metadata", + "build": 12340, + "path": "Interface/FrameXML/Bindings.xml", + "sha256": "2E01276AFB7462F1417710F13B4BF1A456341F7F28CC8AE0D6D9C7442D7F6793", + "distribution": "Hash and selected command names only; proprietary file remains outside Git." + }, + "trinitycore_player_turn_rate": { + "status": "verified_public_source", + "repository": "TrinityCore/TrinityCore", + "revision": "2853a621d6af91a803787a2b8a509f4ce3e0300d", + "path": "src/server/game/Entities/Unit/Unit.cpp", + "url": "https://raw.githubusercontent.com/TrinityCore/TrinityCore/2853a621d6af91a803787a2b8a509f4ce3e0300d/src/server/game/Entities/Unit/Unit.cpp", + "symbol": "playerBaseMoveSpeed[MOVE_TURN_RATE]", + "radians_per_second": 3.141594 + }, + "wowee_mouse_strafe_reference": { + "status": "verified_public_reference_only", + "repository": "Kelsidavis/WoWee", + "revision": "626243e937fb93965fa583a6507ed5a1aa7dda4b", + "path": "src/rendering/camera_controller.cpp", + "sha256": "54A41EFDA77629B2EAB432734A94D0F6F58223F3E4752338FF4C321E7835001B", + "observation": "A/D turn while right mouse is released and strafe while it is held; Q/E always strafe.", + "fidelity_limit": "Reference implementation behavior, not original-client proof." + } + }, + "selected_default_bindings": [ + {"command": "MOVEFORWARD", "keys": ["W", "UP"], "implemented_action": "openwc_player_move_forward"}, + {"command": "MOVEBACKWARD", "keys": ["S", "DOWN"], "implemented_action": "openwc_player_move_backward"}, + {"command": "TURNLEFT", "keys": ["A", "LEFT"], "implemented_action": "openwc_player_turn_left"}, + {"command": "TURNRIGHT", "keys": ["D", "RIGHT"], "implemented_action": "openwc_player_turn_right"}, + {"command": "STRAFELEFT", "keys": ["Q"], "implemented_action": "openwc_player_blizzlike_strafe_left"}, + {"command": "STRAFERIGHT", "keys": ["E"], "implemented_action": "openwc_player_blizzlike_strafe_right"}, + {"command": "JUMP", "keys": ["SPACE", "NUMPAD0"], "implemented_action": null}, + {"command": "TOGGLEAUTORUN", "keys": ["NUMLOCK", "BUTTON4"], "implemented_action": null}, + {"command": "CAMERAZOOMIN", "keys": ["MOUSEWHEELUP"], "implemented_action": "openwc_player_camera_zoom_in"}, + {"command": "CAMERAZOOMOUT", "keys": ["MOUSEWHEELDOWN"], "implemented_action": "openwc_player_camera_zoom_out"}, + {"command": "TURNORACTION", "keys": ["BUTTON2"], "implemented_action": "openwc_player_camera_rotate"}, + {"command": "CAMERAORSELECTORMOVE", "keys": ["BUTTON1"], "implemented_action": null} + ], + "known_gaps": [ + "Jump, autorun and left-button camera/select behavior are recorded but not implemented by this package.", + "The server-core turn-rate default is compatibility evidence, not direct original-client timing proof.", + "Right-mouse A/D strafe routing is supported by a public reference implementation, not direct original-client capture." + ] +} diff --git a/src/tools/verify_local_player_movement.gd b/src/tools/verify_local_player_movement.gd index 9bf64f2..9977d7c 100644 --- a/src/tools/verify_local_player_movement.gd +++ b/src/tools/verify_local_player_movement.gd @@ -12,6 +12,7 @@ func _initialize() -> void: _verify_acceleration_and_deceleration(failures) _verify_sandbox_sprint(failures) _verify_flight_state_and_basis(failures) + _verify_keyboard_turn(failures) _verify_invalid_delta(failures) _verify_scene_boundary(failures) @@ -21,7 +22,7 @@ func _initialize() -> void: quit(1) return - print("LOCAL_PLAYER_MOVEMENT PASS cases=12 state_transitions=2 scene_boundary=1") + print("LOCAL_PLAYER_MOVEMENT PASS cases=15 state_transitions=2 scene_boundary=1") quit(0) @@ -88,6 +89,13 @@ func _verify_invalid_delta(failures: Array[String]) -> void: _expect_vector_near(controller.godot_world_velocity_units_per_second, Vector3.ZERO, "negative delta state", failures) +func _verify_keyboard_turn(failures: Array[String]) -> void: + var controller := _new_controller() + _expect_near(controller.calculate_yaw_delta_radians(_intent(0.0, 0.0, 0.0, false, 1.0), 0.5), -PI * 0.5, "right turn yaw", failures) + _expect_near(controller.calculate_yaw_delta_radians(_intent(0.0, 0.0, 0.0, false, -1.0), 0.5), PI * 0.5, "left turn yaw", failures) + _expect_near(controller.calculate_yaw_delta_radians(_intent(0.0, 0.0, 0.0, false, 1.0), -1.0), 0.0, "negative turn delta", failures) + + func _verify_scene_boundary(failures: Array[String]) -> void: var movement_source := _read_text(MOVEMENT_CONTROLLER_PATH, failures) for forbidden_text in ["extends Node", "extends Resource", "Input.", "Camera3D", "ADTLoader", "global_position"]: @@ -117,9 +125,10 @@ func _intent( forward_axis: float, strafe_axis: float, vertical_axis: float = 0.0, - sprint_requested: bool = false + sprint_requested: bool = false, + turn_axis: float = 0.0 ) -> MoveIntent: - return MoveIntent.new(forward_axis, strafe_axis, vertical_axis, sprint_requested) + return MoveIntent.new(forward_axis, strafe_axis, vertical_axis, sprint_requested, turn_axis) func _read_text(path: String, failures: Array[String]) -> String: @@ -140,6 +149,11 @@ func _expect_vector_near( failures.append("%s expected %s, got %s" % [label, expected_value, actual_value]) +func _expect_near(actual_value: float, expected_value: float, label: String, failures: Array[String]) -> void: + if absf(actual_value - expected_value) > 0.000001: + failures.append("%s expected %.6f, got %.6f" % [label, expected_value, actual_value]) + + func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void: if not actual_value: failures.append("%s expected true" % label) diff --git a/src/tools/verify_player_input.gd b/src/tools/verify_player_input.gd index 6a09714..612d4ff 100644 --- a/src/tools/verify_player_input.gd +++ b/src/tools/verify_player_input.gd @@ -6,6 +6,7 @@ const MOVE_INTENT_PATH := "res://src/domain/input/move_intent.gd" const PLAYER_INPUT_SOURCE_PATH := "res://src/gameplay/input/player_input_source.gd" const PLAYER_CONTROLLER_PATH := "res://src/scenes/player/third_person_wow_controller.gd" const REGRESSION_SCENE_PATH := "res://src/tests/scenes/player_input_regression.tscn" +const PROFILE_FIXTURE_PATH := "res://src/tests/fixtures/player_input_profile_335.json" func _initialize() -> void: @@ -15,6 +16,7 @@ func _initialize() -> void: func _run_verification() -> void: var failures: Array[String] = [] _verify_default_input_actions(failures) + _verify_profile_fixture(failures) _verify_move_intent_composition(failures) _verify_controller_boundary(failures) _verify_regression_scene(failures) @@ -25,7 +27,7 @@ func _run_verification() -> void: quit(1) return - print("PLAYER_INPUT PASS actions=%d intent_cases=6 controller=1 regression_scene=1" % PlayerInputActions.REQUIRED_ACTIONS.size()) + print("PLAYER_INPUT PASS actions=%d intent_cases=9 profiles=3 fixture=1 controller=1 regression_scene=3" % PlayerInputActions.REQUIRED_ACTIONS.size()) quit(0) @@ -34,9 +36,17 @@ func _verify_default_input_actions(failures: Array[String]) -> void: _expect_true(InputMap.has_action(action_name), "Input Map contains %s" % action_name, failures) _expect_key_binding(PlayerInputActions.MOVE_FORWARD, KEY_W, true, failures) + _expect_key_binding(PlayerInputActions.MOVE_FORWARD, KEY_UP, false, failures) _expect_key_binding(PlayerInputActions.MOVE_BACKWARD, KEY_S, true, failures) + _expect_key_binding(PlayerInputActions.MOVE_BACKWARD, KEY_DOWN, false, failures) _expect_key_binding(PlayerInputActions.STRAFE_LEFT, KEY_A, true, failures) _expect_key_binding(PlayerInputActions.STRAFE_RIGHT, KEY_D, true, failures) + _expect_key_binding(PlayerInputActions.BLIZZLIKE_STRAFE_LEFT, KEY_Q, true, failures) + _expect_key_binding(PlayerInputActions.BLIZZLIKE_STRAFE_RIGHT, KEY_E, true, failures) + _expect_key_binding(PlayerInputActions.TURN_LEFT, KEY_A, true, failures) + _expect_key_binding(PlayerInputActions.TURN_LEFT, KEY_LEFT, false, failures) + _expect_key_binding(PlayerInputActions.TURN_RIGHT, KEY_D, true, failures) + _expect_key_binding(PlayerInputActions.TURN_RIGHT, KEY_RIGHT, false, failures) _expect_key_binding(PlayerInputActions.DEBUG_FLY_UP, KEY_E, true, failures) _expect_key_binding(PlayerInputActions.DEBUG_FLY_DOWN, KEY_Q, true, failures) _expect_key_binding(PlayerInputActions.DEBUG_SPRINT, KEY_SHIFT, false, failures) @@ -47,6 +57,40 @@ func _verify_default_input_actions(failures: Array[String]) -> void: _expect_mouse_binding(PlayerInputActions.CAMERA_ZOOM_OUT, MOUSE_BUTTON_WHEEL_DOWN, failures) +func _verify_profile_fixture(failures: Array[String]) -> void: + var fixture_text := _read_text(PROFILE_FIXTURE_PATH, failures) + var parsed_fixture: Variant = JSON.parse_string(fixture_text) + _expect_true(parsed_fixture is Dictionary, "profile fixture parses", failures) + if not parsed_fixture is Dictionary: + return + var fixture := parsed_fixture as Dictionary + _expect_true(fixture.get("schema_version") == 1, "profile fixture schema", failures) + _expect_true(fixture.get("profile_id") == "Blizzlike335", "profile fixture identity", failures) + var sources := fixture.get("sources", {}) as Dictionary + var bindings_source := sources.get("original_client_default_bindings", {}) as Dictionary + _expect_true(bindings_source.get("build") == 12340, "fixture client build", failures) + _expect_true( + bindings_source.get("sha256") == "35FFA0E4E2356A13A0633A848F9E57ABE7DF9D8A16C2245E15D33013AD2F56A5", + "fixture binding source hash", + failures + ) + var commands_source := sources.get("original_client_binding_commands", {}) as Dictionary + _expect_true( + commands_source.get("sha256") == "2E01276AFB7462F1417710F13B4BF1A456341F7F28CC8AE0D6D9C7442D7F6793", + "fixture command source hash", + failures + ) + var turn_source := sources.get("trinitycore_player_turn_rate", {}) as Dictionary + _expect_near(float(turn_source.get("radians_per_second", 0.0)), 3.141594, "fixture turn rate", failures) + var mouse_strafe_source := sources.get("wowee_mouse_strafe_reference", {}) as Dictionary + _expect_true( + mouse_strafe_source.get("revision") == "626243e937fb93965fa583a6507ed5a1aa7dda4b", + "fixture mouse-strafe reference revision", + failures + ) + _expect_true((fixture.get("selected_default_bindings", []) as Array).size() == 12, "fixture selected binding count", failures) + + func _verify_move_intent_composition(failures: Array[String]) -> void: var neutral := PlayerInputSource.compose_move_intent(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false) _expect_near(neutral.forward_axis, 0.0, "neutral forward", failures) @@ -67,6 +111,18 @@ func _verify_move_intent_composition(failures: Array[String]) -> void: var debug_requests := PlayerInputSource.compose_move_intent(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, true) _expect_true(debug_requests.is_sprint_requested, "sprint request retained", failures) + var blizzlike_strafe := PlayerInputSource.compose_blizzlike_335_move_intent(0.0, 0.0, 1.0, 0.0, 0.0, 0.0, false) + _expect_near(blizzlike_strafe.strafe_axis, -1.0, "blizzlike Q strafe", failures) + var blizzlike_turn := PlayerInputSource.compose_blizzlike_335_move_intent(0.0, 0.0, 0.0, 0.0, 0.0, 1.0, false) + _expect_near(blizzlike_turn.turn_axis, 1.0, "blizzlike D turn", failures) + var mouse_strafe := PlayerInputSource.compose_blizzlike_335_move_intent(0.0, 0.0, 0.0, 0.0, 0.0, 1.0, true) + _expect_near(mouse_strafe.strafe_axis, 1.0, "camera-held D strafe", failures) + _expect_near(mouse_strafe.turn_axis, 0.0, "camera-held D suppresses turn", failures) + + _expect_true(PlayerInputSource.new(&"RenderSandbox").input_profile_id == &"RenderSandbox", "sandbox profile selected", failures) + _expect_true(PlayerInputSource.new(&"Blizzlike335").input_profile_id == &"Blizzlike335", "blizzlike profile selected", failures) + _expect_true(PlayerInputSource.new(&"Unknown").input_profile_id == &"Blizzlike335", "unknown profile fails closed", failures) + func _verify_controller_boundary(failures: Array[String]) -> void: var controller_source := _read_text(PLAYER_CONTROLLER_PATH, failures) @@ -118,6 +174,31 @@ func _verify_regression_scene(failures: Array[String]) -> void: _expect_true(movement_controller.is_flight_enabled, "flight action preserves immediate sandbox toggle", failures) player.free() + var blizzlike_player := packed_scene.instantiate() as CharacterBody3D + blizzlike_player.set("movement_profile_id", "Blizzlike335") + root.add_child(blizzlike_player) + blizzlike_player.set_physics_process(false) + Input.action_press(PlayerInputActions.TURN_RIGHT) + blizzlike_player.call("_physics_process", 0.5) + Input.action_release(PlayerInputActions.TURN_RIGHT) + _expect_near(blizzlike_player.rotation.y, -3.141594 * 0.5, "blizzlike D turns right", failures) + var blizzlike_camera := blizzlike_player.get_node("CameraPivot") as ThirdPersonCameraRig + _expect_near(blizzlike_camera.yaw_radians, blizzlike_player.rotation.y, "keyboard turn synchronizes camera yaw", failures) + blizzlike_player.free() + + var mouse_strafe_player := packed_scene.instantiate() as CharacterBody3D + mouse_strafe_player.set("movement_profile_id", "Blizzlike335") + root.add_child(mouse_strafe_player) + mouse_strafe_player.set_physics_process(false) + Input.action_press(PlayerInputActions.CAMERA_ROTATE) + Input.action_press(PlayerInputActions.TURN_RIGHT) + mouse_strafe_player.call("_physics_process", 0.5) + Input.action_release(PlayerInputActions.TURN_RIGHT) + Input.action_release(PlayerInputActions.CAMERA_ROTATE) + _expect_near(mouse_strafe_player.rotation.y, 0.0, "camera-held D does not turn", failures) + _expect_near(mouse_strafe_player.position.x, 2.25, "camera-held D strafes right", failures) + mouse_strafe_player.free() + func _expect_key_binding( action_name: StringName, From e819eeb35b1651502b1691c1169ce1fd26ca77c7 Mon Sep 17 00:00:00 2001 From: sindoring Date: Wed, 15 Jul 2026 00:18:03 +0400 Subject: [PATCH 2/2] docs(M02): hand off 3.3.5 input profile Work-Package: M02-GMP-INPUT-PROFILE-335-001 Agent: sindo-main-codex Tests: records complete package and renderer verification Fidelity: records private-source hashes, public reference revisions and remaining gaps --- .../claims/M02-GMP-INPUT-PROFILE-335-001.md | 57 ++++++++++++++++++- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/coordination/claims/M02-GMP-INPUT-PROFILE-335-001.md b/coordination/claims/M02-GMP-INPUT-PROFILE-335-001.md index 0783505..c764c7d 100644 --- a/coordination/claims/M02-GMP-INPUT-PROFILE-335-001.md +++ b/coordination/claims/M02-GMP-INPUT-PROFILE-335-001.md @@ -78,7 +78,58 @@ turn consumption at the server-contract default turn rate. ## Status -- State: active -- Done: audited current mappings and captured build-12340/source-core provenance -- Next: implement contracts, profile mapping, regression fixtures and docs +- State: ready-for-review +- Done: added distinct remappable profiles, signed turn intent/controller path, + camera-yaw synchronization, sanitized provenance fixture, regression coverage + and updated module specifications +- Next: milestone integrator reviews and merges `8776a6b` - Blocked by: + + + +## Handoff + +- Branch/commit: `work/sindo-main-codex/m02-input-profile-335` at `8776a6b`. +- Outcome: `RenderSandbox` retains A/D strafe and debug E/Q/Space/Shift behavior; + `Blizzlike335` now samples W/S and Up/Down forward/back, Q/E strafe, A/D and + Left/Right turn, with A/D routed to strafe while right-mouse rotation is active. +- Public contracts: `MoveIntent.turn_axis`; profile-selecting + `PlayerInputSource.new(profile_id)`; pure compatibility composition; + `LocalPlayerMovementController.calculate_yaw_delta_radians(...)`; camera yaw + synchronization after keyboard turn. Unknown input profiles fail closed to + `Blizzlike335`. +- Project configuration: four additive compatibility actions; existing action + names and sandbox mappings remain valid. W/S also gain the original Up/Down + secondary defaults. +- Verification: player input `actions=16 intent_cases=9 profiles=3 fixture=1 + controller=1 regression_scene=3`; movement `cases=15 state_transitions=2`; + capabilities `profiles=3 sprint=2 flight=2 scenes=2`; terrain + `contract=4 interpolation=1 cache=1 failures=2`; camera + `state_cases=12 policy_cases=3`; presentation `appearance_cases=9 + animation_cases=10`; coordinate boundaries `files=101`; StreamingFocus + `runtime_scenes=2 capture_tools=3`. +- Renderer: unified dry-run passed project/material/dedupe/cache/manifest/ + calibration/coordinate/server-spawn gates and planned all seven checkpoints; + coordinate gate reported `files=103`. Missing proprietary GLB/DBC/ADT inputs + produced the expected diagnostics only. +- Repository gates: documentation `module_specs=8`; coordination + `fallback_claims=31`, with the same five unrelated expired M00 warnings. +- Fidelity: local build-12340 `DefaultBindings.wtf` SHA-256 + `35FFA0E4E2356A13A0633A848F9E57ABE7DF9D8A16C2245E15D33013AD2F56A5` + and `Bindings.xml` SHA-256 + `2E01276AFB7462F1417710F13B4BF1A456341F7F28CC8AE0D6D9C7442D7F6793` + were rechecked and selected facts sanitized into the committed fixture. + TrinityCore revision `2853a621d6af91a803787a2b8a509f4ce3e0300d` supplies the + 3.141594 rad/s player server-contract turn rate. WoWee revision + `626243e937fb93965fa583a6507ed5a1aa7dda4b` supports right-mouse A/D routing. +- Fidelity limits: server turn rate and public reference routing are not direct + original-client timing/behavior proof. Jump, autorun and left-button + camera/select defaults are recorded but not implemented. This package does + not claim complete input, movement or camera parity. +- Migration/cache: no persisted settings or cache schema. Future saved bindings + must migrate stable action names; current project users receive additive actions. +- Documentation: updated input, movement and camera APIs, inputs/outputs, data + flow, sequence, ownership, failure/recovery, configuration, fidelity and source maps. +- Recommended merge order: merge after sandbox capabilities (already on master), + then run the final M02 integration/evidence audit without marking the target + done solely from this package.