Compare commits

..

5 Commits

Author SHA1 Message Date
sindoring e819eeb35b 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
2026-07-15 00:18:03 +04:00
sindoring 8776a6b362 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
2026-07-15 00:17:34 +04:00
sindoring c3094c9413 Merge pull request 'gmp(M02): gate debug movement capabilities' (#26) from work/sindo-main-codex/m02-sandbox-capabilities into master
Reviewed-on: #26
2026-07-14 22:58:54 +03:00
sindoring 7d5d13f702 docs(M02): publish movement capability handoff
Work-Package: M02-GMP-SANDBOX-CAPABILITIES-001

Agent: sindo-main-codex
2026-07-14 23:56:00 +04:00
sindoring 742c415885 gmp(M02): gate debug movement capabilities
Work-Package: M02-GMP-SANDBOX-CAPABILITIES-001

Agent: sindo-main-codex

Tests: capability/input/movement/terrain/camera/presentation; coordinate/streaming/docs/coordination; renderer dry-run 7/7

Fidelity: proves debug sprint/free-flight exclusion in Blizzlike335; no positive movement parity claim
2026-07-14 23:55:27 +04:00
18 changed files with 930 additions and 71 deletions
@@ -0,0 +1,135 @@
# M02-GMP-INPUT-PROFILE-335-001 — WoW 3.3.5a input profile
<!-- OPENWC_CLAIM:M02-GMP-INPUT-PROFILE-335-001:sindo-main-codex:2026-07-17 -->
## 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: 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:
<!-- OPENWC_HANDOFF:READY:M02-GMP-INPUT-PROFILE-335-001:8776a6b -->
## 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.
@@ -0,0 +1,126 @@
# M02-GMP-SANDBOX-CAPABILITIES-001 — Debug movement capability gate
<!-- OPENWC_CLAIM:M02-GMP-SANDBOX-CAPABILITIES-001:sindo-main-codex:2026-07-16 -->
## Ownership
- Target: M02
- Program: GMP/FND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m02-sandbox-capabilities`
- Lease expires UTC: 2026-07-16
- Integrator: M02 milestone integrator
## Outcome
Introduce an immutable local-player movement capability contract and require it
at the movement boundary so debug sprint and free flight execute only in the
explicit `RenderSandbox` profile and are rejected by `Blizzlike335`.
## Non-goals
- Claim that current movement values or `Blizzlike335` locomotion are 1:1.
- Build a global application/profile framework before the application shell exists.
- Remove debug Input Map actions or change their current sandbox bindings.
- Implement jump, fall, swim, collision, server authority or reconciliation.
- Mark M02 complete or edit its checklist/Evidence.
## Paths
- Exclusive: `src/gameplay/movement/player_movement_capabilities.gd`,
`src/tools/verify_player_movement_capabilities.gd`, this claim
- Shared/hotspots: `src/gameplay/movement/local_player_movement_controller.gd`,
`src/scenes/player/third_person_wow_controller.gd`, movement/input verifiers,
player regression scene, `docs/modules/local-player-movement.md`,
`docs/modules/player-input.md`
- Generated/ignored: local `.godot`, native DLL, generated resources and renderer corpus
## Contracts and data
- `PlayerMovementCapabilities` owns immutable profile ID plus explicit debug
sprint/flight booleans and safe named factories.
- `LocalPlayerMovementController` requires the capability value; missing input
falls back to `Blizzlike335`, never sandbox.
- Player scene maps its exported profile selection once at the composition boundary.
- Input actions and `MoveIntent` remain unchanged; rejected debug requests have no effect.
## Dependencies
- Requires: merged character presentation package on master `8c71e46`.
- Blocks: final M02 integration audit and milestone closeout.
- External state: none; verification is asset-free.
## Verification
- Pure profile factory/default and immutability regression.
- Sprint accepted in RenderSandbox and ignored in Blizzlike335.
- Flight toggle/vertical basis accepted in RenderSandbox and rejected in Blizzlike335.
- Real regression scene verifies both profile compositions.
- Existing input/movement/terrain/camera/presentation and renderer gates remain required.
- Fidelity evidence: negative proof that debug capabilities cannot affect the
compatibility profile; no positive movement parity claim.
## Documentation deliverables
- Inline public API and profile semantics on the capability/controller boundary.
- Updated movement and input specs: API, inputs/outputs, data flow, lifecycle,
failure/recovery, capability matrix and source map.
## Simplicity and naming
- Important name: `PlayerMovementCapabilities`.
- Simplest approach: one immutable value injected into the existing controller.
- Rejected complexity: autoload singleton, generic feature-flag service and global profile registry.
- Unavoidable complexity: one profile-to-capability mapping at scene composition.
- Measured optimization evidence: not applicable.
## Status
- State: ready-for-review
- Done: added immutable capability contract, gated sprint/free-flight, covered
pure and real-scene profiles and updated movement/input specifications
- Next: milestone integrator reviews and merges `742c415`
- Blocked by:
<!-- OPENWC_HANDOFF:READY:M02-GMP-SANDBOX-CAPABILITIES-001:742c415 -->
## Handoff
- Branch/commit: `work/sindo-main-codex/m02-sandbox-capabilities` at `742c415`.
- Outcome: debug sprint and free flight now require explicit
`PlayerMovementCapabilities.render_sandbox()`; `Blizzlike335`, unknown and
null capability inputs reject both effects.
- Public contract: immutable profile ID plus `allows_debug_sprint` and
`allows_debug_free_flight`; named factories map configuration once at the
composition boundary. The movement controller exposes its effective profile ID.
- Runtime composition: `ThirdPersonWowController.movement_profile_id` defaults
to `RenderSandbox`, preserving renderer-scene behavior. Selecting
`Blizzlike335` keeps ordinary movement while ignoring Shift/Space/E/Q effects.
- Verification: capability `profiles=3 sprint=2 flight=2 scenes=2 boundary=1`,
movement `cases=12 state_transitions=2`, input `actions=12 intent_cases=6`,
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 consumers=5`, and StreamingFocus `runtime_scenes=2 capture_tools=3` passed.
- Renderer: dry-run passed project/material/dedupe/cache/manifest/calibration/
coordinate/server-spawn gates and planned all seven checkpoints; the combined
run reported coordinate `files=103`. Missing proprietary GLB/DBC/ADT inputs
produced the expected diagnostics only.
- Repository gates: documentation `module_specs=8`, coordination
`fallback_claims=30`; five unrelated expired M00 claim warnings remain.
- Fidelity: this package provides negative evidence that sandbox-only debug
movement cannot alter the `Blizzlike335` profile. It does not prove ordinary
movement, bindings, flight or timing match build 12340.
- Migration: direct `LocalPlayerMovementController.new(...)` callers that need
debug behavior must now pass `render_sandbox()`; omitted/null capability is
intentionally compatibility-safe. Existing repository scenes require no edit.
- Cache/persistence: no schema, cache, binding or stored-profile migration.
- Local ignored inputs: native extension DLL, UID cache and generated ADT
resource scripts supported renderer verification and are not committed.
- Risks: runtime renderer scenes still default to `RenderSandbox`; the future
application shell must explicitly choose its profile. `Blizzlike335` currently
guarantees only debug exclusion, not complete movement fidelity.
- Recommended merge order: merge after presentation (already on master), then
perform the final M02 integration/evidence audit before any target closeout.
- Documentation: updated movement/input API, inputs/outputs, data flow, state,
sequence, ownership, failure/recovery, capabilities, verification and source maps.
+60 -15
View File
@@ -5,17 +5,18 @@
| Field | Value | | Field | Value |
|---|---| |---|---|
| Status | Implemented | | Status | Implemented |
| Target/work package | M02 / M02-GMP-MOVEMENT-001, camera basis and character-presentation consumer updates | | 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 | | Owners | Gameplay local movement state; scene composition owns the instance |
| Last verified | `97bb53f`, 2026-07-14 | | Last verified | M02-GMP-INPUT-PROFILE-335-001 worktree, 2026-07-15 |
| Profiles/capabilities | Current render sandbox; production/debug profile gate pending | | Profiles/capabilities | `RenderSandbox` debug movement; `Blizzlike335` selected bindings and keyboard turn with debug sprint/flight rejected |
## Purpose ## Purpose
Own current local velocity, acceleration and sandbox flight state independently Own current local velocity, acceleration and sandbox flight state independently
of the player scene. Convert an immutable `MoveIntent` plus an explicit of the player scene. Convert an immutable `MoveIntent` plus an explicit
Godot-world movement basis into deterministic per-tick displacement without Godot-world movement basis into deterministic per-tick displacement and convert
querying terrain, cameras, input devices or visual assets. signed turn intent into a yaw delta without querying terrain, cameras, input
devices or visual assets.
## Non-goals ## Non-goals
@@ -32,7 +33,10 @@ flowchart LR
Input[MoveIntent] --> Movement[LocalPlayerMovementController] Input[MoveIntent] --> Movement[LocalPlayerMovementController]
Basis[Explicit local movement Basis] --> Movement Basis[Explicit local movement Basis] --> Movement
Delta[Physics delta seconds] --> Movement Delta[Physics delta seconds] --> Movement
Profile[Scene profile ID] --> Capabilities[PlayerMovementCapabilities]
Capabilities --> Movement
Movement --> Displacement[Godot-world displacement] Movement --> Displacement[Godot-world displacement]
Movement --> Yaw[Godot yaw delta radians]
Movement --> Velocity[Read-only velocity state] Movement --> Velocity[Read-only velocity state]
Scene[ThirdPersonWowController] --> Basis Scene[ThirdPersonWowController] --> Basis
Displacement --> Scene Displacement --> Scene
@@ -57,11 +61,16 @@ Forbidden dependencies:
| Symbol | Kind | Purpose | Thread/lifetime | Errors | | Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---| |---|---|---|---|---|
| `LocalPlayerMovementController.new(...)` | Constructor | Sets explicit speeds, acceleration and sandbox sprint multiplier | Created by composition root; retained for player scene lifetime | Caller owns configuration validation | | `PlayerMovementCapabilities.render_sandbox()` | Factory | Creates the only debug sprint/free-flight capability set | Immutable caller value | None |
| `PlayerMovementCapabilities.blizzlike_335()` | Factory | Creates the compatibility-safe set with debug movement disabled | Immutable caller value | Name is intent, not a parity claim |
| `PlayerMovementCapabilities.for_profile_id(...)` | Factory | Maps composition configuration once; unknown IDs fail closed | Immutable caller value | Unknown maps to `Blizzlike335` |
| `LocalPlayerMovementController.new(...)` | Constructor | Sets explicit speeds, acceleration, sprint multiplier and movement capabilities | Created by composition root; retained for player scene lifetime | Null capability defaults to `Blizzlike335` |
| `godot_world_velocity_units_per_second` | Read-only state | Current Godot-world velocity after the last update | Stable until next mutation on owning thread | None | | `godot_world_velocity_units_per_second` | Read-only state | Current Godot-world velocity after the last update | Stable until next mutation on owning thread | None |
| `is_flight_enabled` | Read-only state | Current sandbox free-flight mode | Stable until toggle | None | | `is_flight_enabled` | Read-only state | Current sandbox free-flight mode | Stable until toggle | None |
| `toggle_sandbox_flight()` | Command | Toggles flight and clears velocity | Owning physics/input thread | Returns new flight state | | `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 | | `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: Constructor units:
@@ -73,16 +82,19 @@ Constructor units:
| `flight_vertical_speed_units_per_second` | Sandbox vertical Godot units per second | | `flight_vertical_speed_units_per_second` | Sandbox vertical Godot units per second |
| `acceleration_units_per_second_squared` | Velocity change per second | | `acceleration_units_per_second_squared` | Velocity change per second |
| `sandbox_sprint_multiplier` | Dimensionless debug multiplier | | `sandbox_sprint_multiplier` | Dimensionless debug multiplier |
| `keyboard_turn_speed_radians_per_second` | Positive keyboard yaw speed; defaults to PI rad/s |
## Inputs and outputs ## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime | | Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| Input | `MoveIntent` | `PlayerInputSource` | Movement controller | Immutable caller-held value | One physics tick | | Input | `MoveIntent` | `PlayerInputSource` | Movement controller | Immutable caller-held value | One physics tick |
| Input | `PlayerMovementCapabilities` | Application/player scene composition | Movement controller | Immutable caller-held value | Controller lifetime |
| Input | Godot-world `Basis` | Player or `ThirdPersonCameraRig` adapter | Movement controller | Value copy | One physics tick | | Input | Godot-world `Basis` | Player or `ThirdPersonCameraRig` adapter | Movement controller | Value copy | One physics tick |
| Input | Delta seconds | Godot physics loop | Movement controller | Scalar value | One physics tick | | Input | 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 | | 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 | 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 | 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 | | Output | Flight state | Movement controller | Basis/terrain scene adapter | Read-only boolean | Until next toggle |
@@ -97,9 +109,14 @@ Side effects:
```mermaid ```mermaid
flowchart LR flowchart LR
Intent[MoveIntent axes] --> Target[Calculate target velocity] 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] Basis[Player or camera-pivot Basis] --> Directions[Forward/right directions]
Directions --> Target Directions --> Target
Config[Speeds and sprint multiplier] --> Target Config[Speeds and sprint multiplier] --> Target
Profile[RenderSandbox or Blizzlike335] --> Capability[Immutable debug capabilities]
Capability -->|allow or reject sprint| Target
Capability -->|allow or reject flight toggle| Current
Previous[Owned velocity] --> Integrate[move_toward by acceleration × delta] Previous[Owned velocity] --> Integrate[move_toward by acceleration × delta]
Target --> Integrate Target --> Integrate
Integrate --> Current[Updated velocity] Integrate --> Current[Updated velocity]
@@ -112,7 +129,8 @@ flowchart LR
```mermaid ```mermaid
stateDiagram-v2 stateDiagram-v2
[*] --> GroundedSandbox [*] --> GroundedSandbox
GroundedSandbox --> FlyingSandbox: toggle / clear velocity GroundedSandbox --> FlyingSandbox: toggle [debug flight allowed] / clear velocity
GroundedSandbox --> GroundedSandbox: toggle [debug flight rejected]
FlyingSandbox --> GroundedSandbox: toggle / clear velocity FlyingSandbox --> GroundedSandbox: toggle / clear velocity
GroundedSandbox --> GroundedSandbox: advance GroundedSandbox --> GroundedSandbox: advance
FlyingSandbox --> FlyingSandbox: advance FlyingSandbox --> FlyingSandbox: advance
@@ -128,8 +146,14 @@ sequenceDiagram
participant Input as PlayerInputSource participant Input as PlayerInputSource
participant Scene as ThirdPersonWowController participant Scene as ThirdPersonWowController
participant Move as LocalPlayerMovementController participant Move as LocalPlayerMovementController
participant Cap as PlayerMovementCapabilities
participant Present as CharacterAnimationPresenter adapter participant Present as CharacterAnimationPresenter adapter
Input-->>Scene: MoveIntent 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) Scene->>Move: advance(intent, selected basis, delta)
Move-->>Scene: displacement Move-->>Scene: displacement
Scene->>Scene: apply global position and ground snap Scene->>Scene: apply global position and ground snap
@@ -140,6 +164,8 @@ sequenceDiagram
## Ownership, threading and resources ## Ownership, threading and resources
- The player scene creates and exclusively owns one movement controller. - The player scene creates and exclusively owns one movement controller.
- The composition root creates one immutable capability value; the controller
retains it and exposes only its effective profile ID.
- Velocity and flight state have the same lifetime as that controller. - Velocity and flight state have the same lifetime as that controller.
- All mutations currently occur on the main input/physics thread. - All mutations currently occur on the main input/physics thread.
- Read-only value returns are copies; consumers cannot mutate owned state. - Read-only value returns are copies; consumers cannot mutate owned state.
@@ -153,6 +179,8 @@ sequenceDiagram
| Negative physics delta | `advance` guard | Returns zero displacement; velocity unchanged | Unit regression | Resume with valid non-negative delta | | Negative physics delta | `advance` guard | Returns zero displacement; velocity unchanged | Unit regression | Resume with valid non-negative delta |
| Missing scene basis owner | Scene composition defect | Controller cannot be called correctly | Scene/parser error or boundary test | Restore explicit scene dependency | | Missing scene basis owner | Scene composition defect | Controller cannot be called correctly | Scene/parser error or boundary test | Restore explicit scene dependency |
| Flight toggle during motion | Command path | Velocity clears before next step | State-transition regression | Continue from zero velocity | | Flight toggle during motion | Command path | Velocity clears before next step | State-transition regression | Continue from zero velocity |
| Flight/sprint request in `Blizzlike335` | Capability check | Request has no effect | Profile regression | Select explicit `RenderSandbox` only for tooling/debug scenes |
| Unknown/null profile capability | Factory/constructor safe default | Uses `Blizzlike335`; debug movement disabled | Capability regression | Correct composition profile ID |
| Missing terrain data | `TerrainGroundSample.is_available == false` | Scene skips existing ground snap | Stable terrain failure code | Replace/recover query backend | | Missing terrain data | `TerrainGroundSample.is_available == false` | Scene skips existing ground snap | Stable terrain failure code | Replace/recover query backend |
There is no asynchronous cancellation, retry or rollback. Recreating the There is no asynchronous cancellation, retry or rollback. Recreating the
@@ -169,6 +197,10 @@ controller resets velocity and flight state deterministically.
| Flight vertical speed | `7.0` units/s | Sandbox debug | Fixed for controller lifetime | Vertical free-flight target | | Flight vertical speed | `7.0` units/s | Sandbox debug | Fixed for controller lifetime | Vertical free-flight target |
| Sprint multiplier | `6.0` | Sandbox debug | Fixed for controller lifetime | Multiplies planar and vertical targets | | Sprint multiplier | `6.0` | Sandbox debug | Fixed for controller lifetime | Multiplies planar and vertical targets |
| Camera-relative basis | Selected only while flight is enabled | Sandbox debug | Per tick | Preserves existing pitched flight direction | | Camera-relative basis | Selected only while flight is enabled | Sandbox debug | Per tick | Preserves existing pitched flight direction |
| Debug sprint permission | Enabled | `RenderSandbox` only | Immutable | Allows sprint request multiplier |
| Debug free-flight permission | Enabled | `RenderSandbox` only | Immutable | Allows toggle and vertical movement |
| Compatibility-safe debug permissions | Disabled | `Blizzlike335` | Immutable | Ignores sprint/toggle requests |
| Keyboard turn rate | `3.141594` rad/s | `Blizzlike335` composition | Fixed for controller lifetime | Server-contract default applied to signed turn intent |
## Persistence, cache and migration ## Persistence, cache and migration
@@ -187,13 +219,17 @@ separate versioned movement snapshot contract.
- Unit/contract: `src/tools/verify_local_player_movement.gd` covers forward, - Unit/contract: `src/tools/verify_local_player_movement.gd` covers forward,
backward, strafe, rotated basis, acceleration, deceleration, sprint, flight, 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 - Boundary: verifier rejects Node/Input/Camera/ADT dependencies and movement
integration/state remaining in the player scene. integration/state remaining in the player scene.
- Scene integration: `verify_player_input.gd` drives the asset-free player - Scene integration: `verify_player_input.gd` drives the asset-free player
regression scene through forward motion and immediate flight toggle. regression scene through forward motion and immediate flight toggle.
- Fidelity evidence: default values and formulas are regression-locked against - Fidelity evidence: translation defaults remain regression-locked against the
the pre-extraction sandbox. No original-client comparison is claimed. 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; - Performance budget: constant-time vector math, no per-tick I/O or scene lookup;
`MoveIntent` allocation remains owned by the input package. `MoveIntent` allocation remains owned by the input package.
@@ -202,7 +238,8 @@ separate versioned movement snapshot contract.
- Terrain/collision policy can consume displacement without changing input or velocity ownership. - Terrain/collision policy can consume displacement without changing input or velocity ownership.
- A future server-aware predictor may replace this controller behind the scene composition boundary. - A future server-aware predictor may replace this controller behind the scene composition boundary.
- A typed movement snapshot may expose deterministic replay state when M08/M09 require it. - A typed movement snapshot may expose deterministic replay state when M08/M09 require it.
- Sandbox debug capability gating can reject sprint/flight before commands reach this controller. - A future application profile can map to this narrow capability value without
coupling the movement controller to the application shell.
## Capability status ## Capability status
@@ -210,7 +247,9 @@ separate versioned movement snapshot contract.
|---|---|---|---| |---|---|---|---|
| Scene-free local velocity state | Implemented | Pure controller and source boundary regression | Integrate target checklist after review | | 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 | | Existing planar acceleration/speeds | Implemented | Exact numeric unit cases and real scene regression | Compare with build 12340 before fidelity claim |
| Existing camera-relative free flight | Implemented | Pitched-basis and toggle/reset cases | Restrict to sandbox profile | | Compatibility keyboard turn | Implemented | Pinned server-core value, signed pure cases and real-scene profile regression | Direct original-client timing comparison remains required |
| Existing camera-relative free flight | Implemented in `RenderSandbox` | Pitched-basis, profile and toggle/reset cases | Original-client flight semantics remain out of scope |
| Typed sprint/free-flight exclusion | Implemented | Pure and real-scene profile regressions | Application shell must select profile explicitly later |
| Terrain height query | Implemented | Typed `TerrainQuery` and injected player regression | Ground-snap policy remains scene-owned | | Terrain height query | Implemented | Typed `TerrainQuery` and injected player regression | Ground-snap policy remains scene-owned |
| Terrain collision movement policy | Planned | Height-only query does not model collision | Add slopes/holes/collision later | | Terrain collision movement policy | Planned | Height-only query does not model collision | Add slopes/holes/collision later |
| Jump/fall/swim | Planned | M02/M09 roadmap | Requires terrain/liquid and server contracts | | Jump/fall/swim | Planned | M02/M09 roadmap | Requires terrain/liquid and server contracts |
@@ -218,12 +257,15 @@ separate versioned movement snapshot contract.
## Known gaps and risks ## 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 - Configuration is captured at scene `_ready`; runtime mutation of exported speed
properties does not reconfigure an existing controller. properties does not reconfigure an existing controller.
- Ground snap remains in the player scene, but its ADT data access is now behind - Ground snap remains in the player scene, but its ADT data access is now behind
a replaceable `TerrainQuery`. a replaceable `TerrainQuery`.
- Sprint and free flight still lack a typed sandbox capability gate. - Runtime renderer scenes intentionally default to `RenderSandbox`; a future
game-client composition must explicitly select `Blizzlike335`.
- `Blizzlike335` currently means only debug movement exclusion and must not be
interpreted as evidence that remaining movement semantics are 1:1.
- The module uses Godot math value types; a future engine-independent gameplay - The module uses Godot math value types; a future engine-independent gameplay
predictor may require scalar/domain movement contracts. predictor may require scalar/domain movement contracts.
@@ -232,13 +274,16 @@ separate versioned movement snapshot contract.
| Path | Responsibility | | Path | Responsibility |
|---|---| |---|---|
| `src/gameplay/movement/local_player_movement_controller.gd` | Owned velocity/flight state and deterministic integration | | `src/gameplay/movement/local_player_movement_controller.gd` | Owned velocity/flight state and deterministic integration |
| `src/gameplay/movement/player_movement_capabilities.gd` | Immutable profile-to-debug-capability contract and safe defaults |
| `src/gameplay/terrain/terrain_query.gd` | Replaceable ground-height input boundary | | `src/gameplay/terrain/terrain_query.gd` | Replaceable ground-height input boundary |
| `src/scenes/player/third_person_camera_rig.gd` | Camera-relative sandbox flight basis producer | | `src/scenes/player/third_person_camera_rig.gd` | Camera-relative sandbox flight basis producer |
| `src/scenes/character/character_animation_presenter.gd` | Locomotion presentation consumer behind the scene adapter | | `src/scenes/character/character_animation_presenter.gd` | Locomotion presentation consumer behind the scene adapter |
| `src/scenes/player/third_person_wow_controller.gd` | Composition, basis selection, displacement/terrain and presentation-state adapters | | `src/scenes/player/third_person_wow_controller.gd` | Composition, basis selection, displacement/terrain and presentation-state adapters |
| `src/tools/verify_local_player_movement.gd` | Pure numeric, state-transition and dependency regression | | `src/tools/verify_local_player_movement.gd` | Pure numeric, state-transition and dependency regression |
| `src/tools/verify_player_movement_capabilities.gd` | Profile factory, allow/reject and real-scene regression |
| `src/tests/scenes/player_input_regression.tscn` | Asset-free integrated player fixture | | `src/tests/scenes/player_input_regression.tscn` | Asset-free integrated player fixture |
| `src/tools/verify_player_input.gd` | Input-to-movement scene regression | | `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 ## Related decisions and references
+54 -28
View File
@@ -5,24 +5,25 @@
| Field | Value | | Field | Value |
|---|---| |---|---|
| Status | Partial | | Status | Partial |
| Target/work package | M02 / M02-GMP-INPUT-001, M02-GMP-MOVEMENT-001 and M02-RND-CAMERA-001 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 | | Owners | Gameplay input boundary; `sindo-main-codex` for current package |
| Last verified | `8c1cf9b`, 2026-07-14 | | Last verified | M02-GMP-INPUT-PROFILE-335-001 worktree, 2026-07-15 |
| Profiles/capabilities | Current render-sandbox defaults; `Blizzlike335` binding semantics not yet verified | | Profiles/capabilities | Distinct RenderSandbox and Blizzlike335 action layouts; debug execution remains RenderSandbox-only |
## Purpose ## Purpose
Convert remappable Godot Input Map actions into an immutable, scene-free Convert remappable Godot Input Map actions into an immutable, scene-free
`MoveIntent` that can be consumed by the current player controller and a later `MoveIntent` that can be consumed by the independently testable local movement
independently testable local movement controller. controller. Select either the preserved renderer-sandbox layout or the verified
build-12340 movement bindings at the scene composition boundary.
## Non-goals ## Non-goals
- Define authoritative server movement, prediction or reconciliation. - Define authoritative server movement, prediction or reconciliation.
- Own character transform, terrain queries, camera state or animation state. - Own character transform, terrain queries, camera state or animation state.
- Claim that the current W/S/A/D sandbox mapping exactly matches build 12340. - Implement jump, autorun, left-button select/move or persisted binding settings.
- Make sprint or free flight available to the production compatibility profile; - Remove process-wide debug actions; Blizzlike335 sampling ignores them and the
their profile gate remains a later M02 package. movement capability boundary also rejects directly constructed debug requests.
## Context and boundaries ## Context and boundaries
@@ -30,8 +31,10 @@ independently testable local movement controller.
flowchart LR flowchart LR
Devices[Keyboard and mouse] --> InputMap[Godot Input Map] Devices[Keyboard and mouse] --> InputMap[Godot Input Map]
InputMap --> Source[PlayerInputSource] InputMap --> Source[PlayerInputSource]
Profile[RenderSandbox or Blizzlike335] --> Source
Source --> Intent[MoveIntent] Source --> Intent[MoveIntent]
Intent --> Movement[LocalPlayerMovementController] Intent --> Movement[LocalPlayerMovementController]
Profile[PlayerMovementCapabilities] --> Movement
Movement --> Controller[ThirdPersonWowController adapter] Movement --> Controller[ThirdPersonWowController adapter]
Controller --> Camera[ThirdPersonCameraRig] Controller --> Camera[ThirdPersonCameraRig]
``` ```
@@ -52,11 +55,13 @@ Forbidden dependencies:
| Symbol | Kind | Purpose | Thread/lifetime | Errors | | 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 | | `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 | | `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_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 ## Inputs and outputs
@@ -65,6 +70,8 @@ Forbidden dependencies:
| Input | Movement action strengths | Godot `InputMap`/`Input` | `PlayerInputSource` | Engine-owned snapshot | Sampled on main physics tick | | 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 | | 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 | `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 | | Output | Camera action names | `PlayerInputActions` | `ThirdPersonCameraRig` | Static constants | Process lifetime |
Side effects: Side effects:
@@ -81,9 +88,11 @@ flowchart LR
Project[project.godot action defaults] --> Map[InputMap] Project[project.godot action defaults] --> Map[InputMap]
Remap[User or editor remapping] --> Map Remap[User or editor remapping] --> Map
Map -->|movement strengths| Source[PlayerInputSource] Map -->|movement strengths| Source[PlayerInputSource]
Profile[Selected input profile] --> Source
Map -->|camera events| Camera[ThirdPersonCameraRig] Map -->|camera events| Camera[ThirdPersonCameraRig]
Source -->|clamp cancel normalize| Intent[MoveIntent] Source -->|clamp cancel normalize| Intent[MoveIntent]
Intent --> Consumer[LocalPlayerMovementController] Intent --> Consumer[LocalPlayerMovementController]
Profile[PlayerMovementCapabilities] --> Consumer
Consumer --> Adapter[ThirdPersonWowController] Consumer --> Adapter[ThirdPersonWowController]
``` ```
@@ -92,7 +101,9 @@ flowchart LR
The adapter is stateless. The scene composition root creates one The adapter is stateless. The scene composition root creates one
`PlayerInputSource` and `LocalPlayerMovementController` in `_ready`; each physics `PlayerInputSource` and `LocalPlayerMovementController` in `_ready`; each physics
tick produces a new immutable intent. Flight enabled/disabled state is owned by tick produces a new immutable intent. Flight enabled/disabled state is owned by
the movement controller and is not hidden in the input adapter. 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 ## Main sequence
@@ -106,7 +117,7 @@ sequenceDiagram
Engine->>Source: movement action strengths Engine->>Source: movement action strengths
Source->>Intent: compose normalized axes and requests Source->>Intent: compose normalized axes and requests
Source-->>Player: sample_move_intent() 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 Move-->>Player: displacement and velocity state
``` ```
@@ -127,6 +138,7 @@ sequenceDiagram
| Conflicting opposite actions | Pure composition | Strengths cancel deterministically | Covered by contract test | Release/remap one action | | 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 | | 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 | | 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. There is no asynchronous operation to cancel and no persisted state to roll back.
@@ -134,12 +146,15 @@ There is no asynchronous operation to cancel and no persisted state to roll back
| Setting/capability | Default | Profile | Runtime mutable | Effect | | Setting/capability | Default | Profile | Runtime mutable | Effect |
|---|---|---|---|---| |---|---|---|---|---|
| Forward/backward | Physical W/S | Current render sandbox | Yes, through Input Map | Produces signed forward axis | | Forward/backward | Physical W/S and Up/Down | Both | Yes, through Input Map | Produces signed forward axis |
| Strafe left/right | Physical A/D | Current render sandbox | Yes | Produces signed right 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 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 | | 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 sprint | Shift | Executable only in `RenderSandbox` | Yes | Requests multiplier; `Blizzlike335` ignores it |
| Debug flight up/down/toggle | E/Q/Space | Sandbox debug only by intent; profile gate pending | Yes | Requests existing free-flight controls | | Debug flight up/down/toggle | E/Q/Space | Executable only in `RenderSandbox` | Yes | Requests free flight; `Blizzlike335` remains grounded |
## Persistence, cache and migration ## Persistence, cache and migration
@@ -157,13 +172,20 @@ provide a settings migration once user settings are persisted.
## Verification ## Verification
- Unit/contract tests: `src/tools/verify_player_input.gd` validates all actions, - 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 - Integration/E2E: `src/tests/scenes/player_input_regression.tscn` is loaded by
the verifier, which drives forward, camera-zoom and immediate sandbox-flight the verifier, which drives sandbox forward/camera/flight plus compatibility
actions through the real scene controller and also rejects direct keyboard turn and right-mouse strafe through real scene controllers.
physical-key polling. - Profile integration: `verify_player_movement_capabilities.gd` drives sprint
- Fidelity evidence: current sandbox defaults retain W/S/A/D, E/Q, Shift, Space, and flight actions through real `RenderSandbox` and `Blizzlike335` scene instances.
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 - Performance budgets: no I/O, jobs or scene lookup per sample; one immutable
intent allocation per physics tick. intent allocation per physics tick.
- Manual diagnostics: run the Eastern Kingdoms scene and remap one movement - Manual diagnostics: run the Eastern Kingdoms scene and remap one movement
@@ -184,15 +206,15 @@ 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 | | 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 | | `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 |
| Production profile exclusion of sprint/flight | Partial | Debug actions are named explicitly | Add typed profile/capability gate in M02 | | 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 | | Jump/fall/swim/fly gameplay semantics | Planned | M02 target and gameplay roadmap | Implement after terrain/movement state extraction |
## Known gaps and risks ## 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.
- Sprint and free flight are still executable by the sandbox controller until a - Input Map still exposes debug bindings process-wide; the movement consumer now
typed build-profile gate is introduced. rejects their effects outside `RenderSandbox`.
- Camera state is extracted; active collision remains an explicit planned policy. - Camera state is extracted; active collision remains an explicit planned policy.
- There is no persisted user keybinding format or UI yet. - There is no persisted user keybinding format or UI yet.
@@ -205,13 +227,17 @@ 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_actions.gd` | Stable project action names |
| `src/gameplay/input/player_input_source.gd` | Engine Input Map adapter and pure composition | | `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/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_camera_rig.gd` | Camera-action consumer and orbit/zoom state |
| `src/scenes/player/third_person_wow_controller.gd` | Composition and input-event dispatcher | | `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/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_input.gd` | Headless contract/integration regression |
| `src/tools/verify_player_movement_capabilities.gd` | Sandbox versus compatibility-profile integration regression |
## Related decisions and references ## Related decisions and references
- ADR: none; this implements the existing architecture and M02 seam. - ADR: none; this implements the existing architecture and M02 seam.
- Upstream/reference: `targets/02-player-decomposition.md`, - 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`.
+6 -2
View File
@@ -5,9 +5,9 @@
| Field | Value | | Field | Value |
|---|---| |---|---|
| Status | Implemented | | 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 | | 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 | | Profiles/capabilities | Current sandbox orbit/zoom; identity collision policy |
## Purpose ## Purpose
@@ -60,6 +60,7 @@ Forbidden dependencies:
|---|---|---|---|---| |---|---|---|---|---|
| `ThirdPersonCameraRig` | `Node3D` component | Owns orbit/zoom state on CameraPivot | Main thread; scene-owned | Reports missing Camera3D | | `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 | | `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 | | `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 | | `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 | | `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 | 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 | 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 | 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 | | Input | Collision policy | Composition/test | Camera rig | Rig-held RefCounted | Until replacement |
| Output | Character yaw | Camera rig | Player transform | Scene mutation | Orbit event | | Output | Character yaw | Camera rig | Player transform | Scene mutation | Orbit event |
| Output | Pivot pitch/height | Camera rig | CameraPivot | Scene mutation | Refresh | | 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, - State regression: initial yaw/pitch/distance/height/far plane, capture/release,
mouse orbit, character yaw, pitch clamps and zoom clamps. 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 regression: fixed-distance test policy changes Camera3D Z; identity
policy restores requested distance; baseline policy contains no physics query. policy restores requested distance; baseline policy contains no physics query.
- Scene integration: Eastern Kingdoms, Kalimdor and asset-free regression scenes - Scene integration: Eastern Kingdoms, Kalimdor and asset-free regression scenes
+24
View File
@@ -24,11 +24,13 @@ enabled=PackedStringArray("res://addons/mpq_extractor/plugin.cfg")
openwc_player_move_forward={ openwc_player_move_forward={
"deadzone": 0.5, "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) "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={ openwc_player_move_backward={
"deadzone": 0.5, "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) "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={ 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) "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={ openwc_player_debug_fly_up={
"deadzone": 0.5, "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) "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)
+9 -1
View File
@@ -21,10 +21,16 @@ var is_sprint_requested: bool:
get: get:
return _is_sprint_requested 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 _forward_axis: float
var _strafe_axis: float var _strafe_axis: float
var _vertical_axis: float var _vertical_axis: float
var _is_sprint_requested: bool var _is_sprint_requested: bool
var _turn_axis: float
## Creates one frame's movement request without retaining mutable input state. ## Creates one frame's movement request without retaining mutable input state.
@@ -32,12 +38,14 @@ func _init(
forward_axis_value: float = 0.0, forward_axis_value: float = 0.0,
strafe_axis_value: float = 0.0, strafe_axis_value: float = 0.0,
vertical_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: ) -> void:
_forward_axis = clampf(forward_axis_value, -1.0, 1.0) _forward_axis = clampf(forward_axis_value, -1.0, 1.0)
_strafe_axis = clampf(strafe_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) _vertical_axis = clampf(vertical_axis_value, -1.0, 1.0)
_is_sprint_requested = sprint_requested _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. ## Returns true when the intent requests planar or vertical translation.
+10 -2
View File
@@ -2,13 +2,17 @@ class_name PlayerInputActions
extends RefCounted extends RefCounted
## Stable Input Map action names consumed by the runtime player input adapter. ## Stable Input Map action names consumed by the runtime player input adapter.
## Defaults preserve the current render-sandbox controls; users may remap every ## Separate translation actions preserve the renderer sandbox while allowing
## action through Godot's Input Map without changing gameplay code. ## Blizzlike335 defaults to match build-12340 movement bindings.
const MOVE_FORWARD := &"openwc_player_move_forward" const MOVE_FORWARD := &"openwc_player_move_forward"
const MOVE_BACKWARD := &"openwc_player_move_backward" const MOVE_BACKWARD := &"openwc_player_move_backward"
const STRAFE_LEFT := &"openwc_player_strafe_left" const STRAFE_LEFT := &"openwc_player_strafe_left"
const STRAFE_RIGHT := &"openwc_player_strafe_right" 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_UP := &"openwc_player_debug_fly_up"
const DEBUG_FLY_DOWN := &"openwc_player_debug_fly_down" const DEBUG_FLY_DOWN := &"openwc_player_debug_fly_down"
const DEBUG_SPRINT := &"openwc_player_debug_sprint" const DEBUG_SPRINT := &"openwc_player_debug_sprint"
@@ -23,6 +27,10 @@ const REQUIRED_ACTIONS: Array[StringName] = [
MOVE_BACKWARD, MOVE_BACKWARD,
STRAFE_LEFT, STRAFE_LEFT,
STRAFE_RIGHT, STRAFE_RIGHT,
BLIZZLIKE_STRAFE_LEFT,
BLIZZLIKE_STRAFE_RIGHT,
TURN_LEFT,
TURN_RIGHT,
DEBUG_FLY_UP, DEBUG_FLY_UP,
DEBUG_FLY_DOWN, DEBUG_FLY_DOWN,
DEBUG_SPRINT, DEBUG_SPRINT,
+76 -11
View File
@@ -5,21 +5,86 @@ extends RefCounted
## This adapter owns no movement state and may be replaced independently of the ## This adapter owns no movement state and may be replaced independently of the
## local movement controller. ## 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. var input_profile_id: StringName:
func sample_move_intent() -> MoveIntent: get:
return compose_move_intent( return _input_profile_id
Input.get_action_strength(PlayerInputActions.MOVE_FORWARD),
Input.get_action_strength(PlayerInputActions.MOVE_BACKWARD), var _input_profile_id: StringName
Input.get_action_strength(PlayerInputActions.STRAFE_LEFT),
Input.get_action_strength(PlayerInputActions.STRAFE_RIGHT),
Input.get_action_strength(PlayerInputActions.DEBUG_FLY_UP), ## Selects one supported action layout. Unknown profiles fail closed to Blizzlike335.
Input.get_action_strength(PlayerInputActions.DEBUG_FLY_DOWN), func _init(requested_profile_id: StringName = BLIZZLIKE_335_PROFILE_ID) -> void:
Input.is_action_pressed(PlayerInputActions.DEBUG_SPRINT) _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. ## alternative input adapters. Strengths outside `[0, 1]` are clamped.
static func compose_move_intent( static func compose_move_intent(
forward_strength: float, forward_strength: float,
@@ -13,25 +13,34 @@ var is_flight_enabled: bool:
get: get:
return _is_flight_enabled return _is_flight_enabled
var movement_profile_id: StringName:
get:
return _movement_capabilities.profile_id
var _run_speed_units_per_second: float var _run_speed_units_per_second: float
var _backward_speed_units_per_second: float var _backward_speed_units_per_second: float
var _strafe_speed_units_per_second: float var _strafe_speed_units_per_second: float
var _flight_vertical_speed_units_per_second: float var _flight_vertical_speed_units_per_second: float
var _acceleration_units_per_second_squared: float var _acceleration_units_per_second_squared: float
var _sandbox_sprint_multiplier: 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 _godot_world_velocity_units_per_second := Vector3.ZERO
var _is_flight_enabled := false var _is_flight_enabled := false
## Creates a movement state with explicit Godot-unit speeds and acceleration. ## Creates a movement state with explicit Godot-unit speeds, acceleration and capabilities.
## The current compatibility renderer treats one Godot unit as one WoW yard. ## The current compatibility renderer treats one Godot unit as one WoW yard.
## A null capability input fails closed to Blizzlike335 debug permissions.
func _init( func _init(
run_speed_units_per_second: float, run_speed_units_per_second: float,
backward_speed_units_per_second: float, backward_speed_units_per_second: float,
strafe_speed_units_per_second: float, strafe_speed_units_per_second: float,
flight_vertical_speed_units_per_second: float, flight_vertical_speed_units_per_second: float,
acceleration_units_per_second_squared: float, acceleration_units_per_second_squared: float,
sandbox_sprint_multiplier: float sandbox_sprint_multiplier: float,
movement_capabilities: PlayerMovementCapabilities = null,
keyboard_turn_speed_radians_per_second: float = PI
) -> void: ) -> void:
_run_speed_units_per_second = run_speed_units_per_second _run_speed_units_per_second = run_speed_units_per_second
_backward_speed_units_per_second = backward_speed_units_per_second _backward_speed_units_per_second = backward_speed_units_per_second
@@ -39,11 +48,32 @@ func _init(
_flight_vertical_speed_units_per_second = flight_vertical_speed_units_per_second _flight_vertical_speed_units_per_second = flight_vertical_speed_units_per_second
_acceleration_units_per_second_squared = acceleration_units_per_second_squared _acceleration_units_per_second_squared = acceleration_units_per_second_squared
_sandbox_sprint_multiplier = sandbox_sprint_multiplier _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
else PlayerMovementCapabilities.blizzlike_335()
)
## Toggles sandbox free flight and clears velocity exactly like the pre-M02 controller. ## Returns the Godot yaw delta for one signed keyboard-turn request.
## Returns the new flight state. ## 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: func toggle_sandbox_flight() -> bool:
if not _movement_capabilities.allows_debug_free_flight:
return false
_is_flight_enabled = not _is_flight_enabled _is_flight_enabled = not _is_flight_enabled
_godot_world_velocity_units_per_second = Vector3.ZERO _godot_world_velocity_units_per_second = Vector3.ZERO
return _is_flight_enabled return _is_flight_enabled
@@ -83,7 +113,11 @@ func _calculate_target_velocity(move_intent: MoveIntent, godot_world_movement_ba
if move_intent.forward_axis > 0.0 if move_intent.forward_axis > 0.0
else _backward_speed_units_per_second else _backward_speed_units_per_second
) )
var speed_multiplier := _sandbox_sprint_multiplier if move_intent.is_sprint_requested else 1.0 var speed_multiplier := (
_sandbox_sprint_multiplier
if move_intent.is_sprint_requested and _movement_capabilities.allows_debug_sprint
else 1.0
)
var target_velocity := ( var target_velocity := (
forward_direction * move_intent.forward_axis * forward_speed forward_direction * move_intent.forward_axis * forward_speed
+ right_direction * move_intent.strafe_axis * _strafe_speed_units_per_second + right_direction * move_intent.strafe_axis * _strafe_speed_units_per_second
@@ -0,0 +1,53 @@
class_name PlayerMovementCapabilities
extends RefCounted
## Immutable local-player movement capability selection.
## Unknown or missing profile identifiers collapse to the safe Blizzlike335
## capability set; only RenderSandbox enables debug sprint and free flight.
const BLIZZLIKE_335_PROFILE_ID: StringName = &"Blizzlike335"
const RENDER_SANDBOX_PROFILE_ID: StringName = &"RenderSandbox"
var profile_id: StringName:
get:
return _profile_id
var allows_debug_sprint: bool:
get:
return _allows_debug_sprint
var allows_debug_free_flight: bool:
get:
return _allows_debug_free_flight
var _profile_id: StringName
var _allows_debug_sprint: bool
var _allows_debug_free_flight: bool
func _init(requested_profile_id: StringName = BLIZZLIKE_335_PROFILE_ID) -> void:
if requested_profile_id == RENDER_SANDBOX_PROFILE_ID:
_profile_id = RENDER_SANDBOX_PROFILE_ID
_allows_debug_sprint = true
_allows_debug_free_flight = true
return
_profile_id = BLIZZLIKE_335_PROFILE_ID
_allows_debug_sprint = false
_allows_debug_free_flight = false
## Returns the debug-enabled capability set used by current renderer scenes.
static func render_sandbox() -> PlayerMovementCapabilities:
return PlayerMovementCapabilities.new(RENDER_SANDBOX_PROFILE_ID)
## Returns the compatibility-safe capability set with debug movement disabled.
## This name identifies the intended compatibility profile, not proven 1:1 movement.
static func blizzlike_335() -> PlayerMovementCapabilities:
return PlayerMovementCapabilities.new(BLIZZLIKE_335_PROFILE_ID)
## Maps scene/application profile configuration to a known capability set.
## Unknown identifiers fail closed to Blizzlike335.
static func for_profile_id(requested_profile_id: StringName) -> PlayerMovementCapabilities:
return PlayerMovementCapabilities.new(requested_profile_id)
@@ -0,0 +1 @@
uid://cdnulu22drbdw
@@ -94,6 +94,12 @@ func initialize_for_character(character_body: Node3D) -> void:
refresh_camera_transform() 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. ## Replaces camera-distance collision behavior and immediately refreshes the camera.
## Passing null is rejected and preserves the current policy. ## Passing null is rejected and preserves the current policy.
func set_collision_policy(collision_policy: CameraCollisionPolicy) -> void: func set_collision_policy(collision_policy: CameraCollisionPolicy) -> void:
@@ -7,6 +7,7 @@ const ADT_TILE_LOCAL_POSITION_SCRIPT := preload("res://src/domain/coordinates/ad
const PLAYER_INPUT_SOURCE_SCRIPT := preload("res://src/gameplay/input/player_input_source.gd") const PLAYER_INPUT_SOURCE_SCRIPT := preload("res://src/gameplay/input/player_input_source.gd")
const PLAYER_INPUT_ACTIONS_SCRIPT := preload("res://src/gameplay/input/player_input_actions.gd") const PLAYER_INPUT_ACTIONS_SCRIPT := preload("res://src/gameplay/input/player_input_actions.gd")
const LOCAL_PLAYER_MOVEMENT_CONTROLLER_SCRIPT := preload("res://src/gameplay/movement/local_player_movement_controller.gd") const LOCAL_PLAYER_MOVEMENT_CONTROLLER_SCRIPT := preload("res://src/gameplay/movement/local_player_movement_controller.gd")
const PLAYER_MOVEMENT_CAPABILITIES_SCRIPT := preload("res://src/gameplay/movement/player_movement_capabilities.gd")
const ADT_TERRAIN_QUERY_SCRIPT := preload("res://src/gameplay/terrain/adt_terrain_query.gd") const ADT_TERRAIN_QUERY_SCRIPT := preload("res://src/gameplay/terrain/adt_terrain_query.gd")
@export var extracted_dir: String = "res://data/extracted" @export var extracted_dir: String = "res://data/extracted"
@@ -21,6 +22,11 @@ const ADT_TERRAIN_QUERY_SCRIPT := preload("res://src/gameplay/terrain/adt_terrai
@export var sprint_multiplier: float = 6.0 @export var sprint_multiplier: float = 6.0
@export var flight_vertical_speed: float = 7.0 @export var flight_vertical_speed: float = 7.0
@export var acceleration: float = 28.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"
@export var ground_offset: float = 0.05 @export var ground_offset: float = 0.05
@export var ground_snap_speed: float = 24.0 @export var ground_snap_speed: float = 24.0
@@ -46,7 +52,7 @@ func set_terrain_query(terrain_query: TerrainQuery) -> void:
func _ready() -> 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: if _terrain_query == null:
_terrain_query = ADT_TERRAIN_QUERY_SCRIPT.new(extracted_dir, map_name) _terrain_query = ADT_TERRAIN_QUERY_SCRIPT.new(extracted_dir, map_name)
_local_movement_controller = LOCAL_PLAYER_MOVEMENT_CONTROLLER_SCRIPT.new( _local_movement_controller = LOCAL_PLAYER_MOVEMENT_CONTROLLER_SCRIPT.new(
@@ -55,7 +61,9 @@ func _ready() -> void:
strafe_speed, strafe_speed,
flight_vertical_speed, flight_vertical_speed,
acceleration, acceleration,
sprint_multiplier sprint_multiplier,
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 _camera_rig = get_node_or_null(camera_pivot_path) as ThirdPersonCameraRig
_appearance_presenter = get_node_or_null(visual_path) as CharacterAppearancePresenter _appearance_presenter = get_node_or_null(visual_path) as CharacterAppearancePresenter
@@ -91,6 +99,14 @@ func _unhandled_input(event: InputEvent) -> void:
func _physics_process(delta: float) -> void: func _physics_process(delta: float) -> void:
var move_intent := _player_input_source.sample_move_intent() 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 var godot_world_movement_basis := global_basis
if _local_movement_controller.is_flight_enabled and _camera_rig != null: if _local_movement_controller.is_flight_enabled and _camera_rig != null:
godot_world_movement_basis = _camera_rig.godot_world_flight_movement_basis() godot_world_movement_basis = _camera_rig.godot_world_flight_movement_basis()
+57
View File
@@ -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."
]
}
+26 -4
View File
@@ -12,6 +12,7 @@ func _initialize() -> void:
_verify_acceleration_and_deceleration(failures) _verify_acceleration_and_deceleration(failures)
_verify_sandbox_sprint(failures) _verify_sandbox_sprint(failures)
_verify_flight_state_and_basis(failures) _verify_flight_state_and_basis(failures)
_verify_keyboard_turn(failures)
_verify_invalid_delta(failures) _verify_invalid_delta(failures)
_verify_scene_boundary(failures) _verify_scene_boundary(failures)
@@ -21,7 +22,7 @@ func _initialize() -> void:
quit(1) quit(1)
return 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) 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) _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: func _verify_scene_boundary(failures: Array[String]) -> void:
var movement_source := _read_text(MOVEMENT_CONTROLLER_PATH, failures) var movement_source := _read_text(MOVEMENT_CONTROLLER_PATH, failures)
for forbidden_text in ["extends Node", "extends Resource", "Input.", "Camera3D", "ADTLoader", "global_position"]: for forbidden_text in ["extends Node", "extends Resource", "Input.", "Camera3D", "ADTLoader", "global_position"]:
@@ -102,16 +110,25 @@ func _verify_scene_boundary(failures: Array[String]) -> void:
func _new_controller() -> LocalPlayerMovementController: func _new_controller() -> LocalPlayerMovementController:
return LocalPlayerMovementController.new(7.0, 4.5, 4.5, 7.0, 28.0, 6.0) return LocalPlayerMovementController.new(
7.0,
4.5,
4.5,
7.0,
28.0,
6.0,
PlayerMovementCapabilities.render_sandbox()
)
func _intent( func _intent(
forward_axis: float, forward_axis: float,
strafe_axis: float, strafe_axis: float,
vertical_axis: float = 0.0, vertical_axis: float = 0.0,
sprint_requested: bool = false sprint_requested: bool = false,
turn_axis: float = 0.0
) -> MoveIntent: ) -> 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: func _read_text(path: String, failures: Array[String]) -> String:
@@ -132,6 +149,11 @@ func _expect_vector_near(
failures.append("%s expected %s, got %s" % [label, expected_value, actual_value]) 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: func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
if not actual_value: if not actual_value:
failures.append("%s expected true" % label) failures.append("%s expected true" % label)
+82 -1
View File
@@ -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_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 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 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: func _initialize() -> void:
@@ -15,6 +16,7 @@ func _initialize() -> void:
func _run_verification() -> void: func _run_verification() -> void:
var failures: Array[String] = [] var failures: Array[String] = []
_verify_default_input_actions(failures) _verify_default_input_actions(failures)
_verify_profile_fixture(failures)
_verify_move_intent_composition(failures) _verify_move_intent_composition(failures)
_verify_controller_boundary(failures) _verify_controller_boundary(failures)
_verify_regression_scene(failures) _verify_regression_scene(failures)
@@ -25,7 +27,7 @@ func _run_verification() -> void:
quit(1) quit(1)
return 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) 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_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_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_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_LEFT, KEY_A, true, failures)
_expect_key_binding(PlayerInputActions.STRAFE_RIGHT, KEY_D, 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_UP, KEY_E, true, failures)
_expect_key_binding(PlayerInputActions.DEBUG_FLY_DOWN, KEY_Q, true, failures) _expect_key_binding(PlayerInputActions.DEBUG_FLY_DOWN, KEY_Q, true, failures)
_expect_key_binding(PlayerInputActions.DEBUG_SPRINT, KEY_SHIFT, false, 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) _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: 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) 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) _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) 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) _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: func _verify_controller_boundary(failures: Array[String]) -> void:
var controller_source := _read_text(PLAYER_CONTROLLER_PATH, failures) 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) _expect_true(movement_controller.is_flight_enabled, "flight action preserves immediate sandbox toggle", failures)
player.free() 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( func _expect_key_binding(
action_name: StringName, action_name: StringName,
@@ -0,0 +1,148 @@
extends SceneTree
## Headless M02 profile/capability gate regression for debug player movement.
const MOVEMENT_CAPABILITIES_PATH := "res://src/gameplay/movement/player_movement_capabilities.gd"
const MOVEMENT_CONTROLLER_PATH := "res://src/gameplay/movement/local_player_movement_controller.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"
func _initialize() -> void:
_run_verification.call_deferred()
func _run_verification() -> void:
var failures: Array[String] = []
_verify_capability_profiles(failures)
_verify_movement_gate(failures)
_verify_real_scene_profiles(failures)
_verify_source_boundary(failures)
if not failures.is_empty():
for failure in failures:
push_error("PLAYER_MOVEMENT_CAPABILITIES: %s" % failure)
quit(1)
return
print("PLAYER_MOVEMENT_CAPABILITIES PASS profiles=3 sprint=2 flight=2 scenes=2 boundary=1")
quit(0)
func _verify_capability_profiles(failures: Array[String]) -> void:
var render_sandbox := PlayerMovementCapabilities.render_sandbox()
_expect_true(render_sandbox.profile_id == PlayerMovementCapabilities.RENDER_SANDBOX_PROFILE_ID, "sandbox profile identity", failures)
_expect_true(render_sandbox.allows_debug_sprint, "sandbox sprint capability", failures)
_expect_true(render_sandbox.allows_debug_free_flight, "sandbox flight capability", failures)
var blizzlike := PlayerMovementCapabilities.blizzlike_335()
_expect_true(blizzlike.profile_id == PlayerMovementCapabilities.BLIZZLIKE_335_PROFILE_ID, "blizzlike profile identity", failures)
_expect_true(not blizzlike.allows_debug_sprint, "blizzlike rejects sprint", failures)
_expect_true(not blizzlike.allows_debug_free_flight, "blizzlike rejects flight", failures)
var unknown_profile := PlayerMovementCapabilities.for_profile_id(&"UnknownProfile")
_expect_true(unknown_profile.profile_id == PlayerMovementCapabilities.BLIZZLIKE_335_PROFILE_ID, "unknown profile fails closed", failures)
_expect_true(not unknown_profile.allows_debug_sprint, "unknown profile rejects sprint", failures)
_expect_true(not unknown_profile.allows_debug_free_flight, "unknown profile rejects flight", failures)
func _verify_movement_gate(failures: Array[String]) -> void:
var sprint_intent := MoveIntent.new(1.0, 0.0, 0.0, true)
var sandbox_controller := _new_controller(PlayerMovementCapabilities.render_sandbox())
sandbox_controller.advance(sprint_intent, Basis.IDENTITY, 2.0)
_expect_vector_near(sandbox_controller.godot_world_velocity_units_per_second, Vector3(0.0, 0.0, -42.0), "sandbox sprint applied", failures)
_expect_true(sandbox_controller.toggle_sandbox_flight(), "sandbox flight enabled", failures)
var blizzlike_controller := _new_controller(PlayerMovementCapabilities.blizzlike_335())
blizzlike_controller.advance(sprint_intent, Basis.IDENTITY, 2.0)
_expect_vector_near(blizzlike_controller.godot_world_velocity_units_per_second, Vector3(0.0, 0.0, -7.0), "blizzlike sprint ignored", failures)
_expect_true(not blizzlike_controller.toggle_sandbox_flight(), "blizzlike flight rejected", failures)
_expect_true(not blizzlike_controller.is_flight_enabled, "blizzlike remains grounded", failures)
var safe_default_controller := LocalPlayerMovementController.new(7.0, 4.5, 4.5, 7.0, 28.0, 6.0)
_expect_true(safe_default_controller.movement_profile_id == PlayerMovementCapabilities.BLIZZLIKE_335_PROFILE_ID, "missing capability defaults safe", failures)
func _verify_real_scene_profiles(failures: Array[String]) -> void:
var sandbox_player := _instantiate_player("RenderSandbox", failures)
if sandbox_player != null:
_press_sprint_forward_for_two_seconds(sandbox_player)
var sandbox_movement: LocalPlayerMovementController = sandbox_player.get("_local_movement_controller")
_expect_vector_near(sandbox_movement.godot_world_velocity_units_per_second, Vector3(0.0, 0.0, -42.0), "sandbox scene sprint", failures)
sandbox_player.call("_unhandled_input", _action_event(PlayerInputActions.DEBUG_TOGGLE_FLIGHT))
_expect_true(sandbox_movement.is_flight_enabled, "sandbox scene flight", failures)
sandbox_player.free()
var blizzlike_player := _instantiate_player("Blizzlike335", failures)
if blizzlike_player != null:
_press_sprint_forward_for_two_seconds(blizzlike_player)
var blizzlike_movement: LocalPlayerMovementController = blizzlike_player.get("_local_movement_controller")
_expect_vector_near(blizzlike_movement.godot_world_velocity_units_per_second, Vector3(0.0, 0.0, -7.0), "blizzlike scene sprint rejected", failures)
blizzlike_player.call("_unhandled_input", _action_event(PlayerInputActions.DEBUG_TOGGLE_FLIGHT))
_expect_true(not blizzlike_movement.is_flight_enabled, "blizzlike scene flight rejected", failures)
blizzlike_player.free()
func _instantiate_player(profile_id: String, failures: Array[String]) -> CharacterBody3D:
var packed_scene := load(REGRESSION_SCENE_PATH) as PackedScene
if packed_scene == null:
failures.append("regression scene loads")
return null
var player := packed_scene.instantiate() as CharacterBody3D
if player == null:
failures.append("regression player instantiates")
return null
player.set("movement_profile_id", profile_id)
root.add_child(player)
player.set_physics_process(false)
return player
func _press_sprint_forward_for_two_seconds(player: CharacterBody3D) -> void:
Input.action_press(PlayerInputActions.MOVE_FORWARD)
Input.action_press(PlayerInputActions.DEBUG_SPRINT)
player.call("_physics_process", 2.0)
Input.action_release(PlayerInputActions.DEBUG_SPRINT)
Input.action_release(PlayerInputActions.MOVE_FORWARD)
func _action_event(action_name: StringName) -> InputEventAction:
var input_event := InputEventAction.new()
input_event.action = action_name
input_event.pressed = true
return input_event
func _verify_source_boundary(failures: Array[String]) -> void:
var capability_source := _read_text(MOVEMENT_CAPABILITIES_PATH, failures)
for forbidden_text in ["extends Node", "extends Resource", "Input.", "CharacterBody3D"]:
_expect_true(not capability_source.contains(forbidden_text), "capability omits %s" % forbidden_text, failures)
_expect_true(not capability_source.contains("\n\tset:"), "capability exposes no property setters", failures)
var movement_source := _read_text(MOVEMENT_CONTROLLER_PATH, failures)
_expect_true(movement_source.contains("allows_debug_sprint"), "movement gates sprint capability", failures)
_expect_true(movement_source.contains("allows_debug_free_flight"), "movement gates flight capability", failures)
var player_source := _read_text(PLAYER_CONTROLLER_PATH, failures)
_expect_true(player_source.contains("for_profile_id(StringName(movement_profile_id))"), "scene maps profile once", failures)
_expect_true(not player_source.contains('movement_profile_id == "'), "scene omits profile branches", failures)
func _new_controller(capabilities: PlayerMovementCapabilities) -> LocalPlayerMovementController:
return LocalPlayerMovementController.new(7.0, 4.5, 4.5, 7.0, 28.0, 6.0, capabilities)
func _read_text(path: String, failures: Array[String]) -> String:
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
failures.append("cannot open %s" % path)
return ""
return file.get_as_text()
func _expect_vector_near(actual_value: Vector3, expected_value: Vector3, label: String, failures: Array[String]) -> void:
if not actual_value.is_equal_approx(expected_value):
failures.append("%s expected %s, got %s" % [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)