gmp(M02): gate debug movement capabilities #26

Merged
sindoring merged 2 commits from work/sindo-main-codex/m02-sandbox-capabilities into master 2026-07-14 22:58:55 +03:00
9 changed files with 426 additions and 27 deletions
@@ -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.
+39 -9
View File
@@ -5,10 +5,10 @@
| 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 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 | `742c415`, 2026-07-14 |
| Profiles/capabilities | Current render sandbox; production/debug profile gate pending | | Profiles/capabilities | `RenderSandbox` debug movement; `Blizzlike335` rejects debug sprint/flight |
## Purpose ## Purpose
@@ -32,6 +32,8 @@ 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 --> Velocity[Read-only velocity state] Movement --> Velocity[Read-only velocity state]
Scene[ThirdPersonWowController] --> Basis Scene[ThirdPersonWowController] --> Basis
@@ -57,10 +59,14 @@ 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 |
Constructor units: Constructor units:
@@ -79,6 +85,7 @@ Constructor units:
| 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 |
@@ -100,6 +107,9 @@ flowchart LR
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 +122,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 +139,11 @@ 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: 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 +154,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 +169,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 +187,9 @@ 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 |
## Persistence, cache and migration ## Persistence, cache and migration
@@ -188,6 +209,8 @@ 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 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
@@ -202,7 +225,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 +234,8 @@ 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 | | 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 |
@@ -223,7 +248,10 @@ separate versioned movement snapshot contract.
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,11 +260,13 @@ 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 |
+21 -11
View File
@@ -5,10 +5,10 @@
| 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 / input, movement, camera and sandbox-capability consumer updates |
| 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 | `742c415`, 2026-07-14 |
| Profiles/capabilities | Current render-sandbox defaults; `Blizzlike335` binding semantics not yet verified | | Profiles/capabilities | Debug requests sampled in all profiles; execution allowed only by `RenderSandbox` movement capabilities |
## Purpose ## Purpose
@@ -21,8 +21,8 @@ independently testable local movement controller.
- 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. - Claim that the current W/S/A/D sandbox mapping exactly matches build 12340.
- Make sprint or free flight available to the production compatibility profile; - Suppress debug action sampling by physical device or UI context; execution is
their profile gate remains a later M02 package. rejected at the typed movement capability boundary.
## Context and boundaries ## Context and boundaries
@@ -32,6 +32,7 @@ flowchart LR
InputMap --> Source[PlayerInputSource] InputMap --> Source[PlayerInputSource]
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]
``` ```
@@ -65,6 +66,7 @@ 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 | Camera action names | `PlayerInputActions` | `ThirdPersonCameraRig` | Static constants | Process lifetime | | Output | Camera action names | `PlayerInputActions` | `ThirdPersonCameraRig` | Static constants | Process lifetime |
Side effects: Side effects:
@@ -84,6 +86,7 @@ flowchart LR
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 +95,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. Debug requests
remain data in the intent; the immutable movement capability value decides
whether they execute.
## Main sequence ## Main sequence
@@ -127,6 +132,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` | Movement capability check | Request is sampled but has no movement effect | Capability regression | 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.
@@ -138,8 +144,8 @@ There is no asynchronous operation to cancel and no persisted state to roll back
| Strafe left/right | Physical A/D | Current render sandbox | Yes | Produces signed right axis | | Strafe left/right | Physical A/D | Current render sandbox | Yes | Produces signed right axis |
| Camera rotate | Right mouse button | Current render sandbox | Yes | Captures/releases mouse for orbit | | Camera 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
@@ -162,6 +168,8 @@ provide a settings migration once user settings are persisted.
the verifier, which drives forward, camera-zoom and immediate sandbox-flight the verifier, which drives forward, camera-zoom and immediate sandbox-flight
actions through the real scene controller and also rejects direct actions through the real scene controller and also rejects direct
physical-key polling. physical-key polling.
- Profile integration: `verify_player_movement_capabilities.gd` drives sprint
and flight actions through real `RenderSandbox` and `Blizzlike335` scene instances.
- Fidelity evidence: current sandbox defaults retain W/S/A/D, E/Q, Shift, Space, - Fidelity evidence: current sandbox defaults retain W/S/A/D, E/Q, Shift, Space,
right mouse and wheel bindings. This is observable-regression coverage only. right mouse and wheel bindings. This is observable-regression coverage only.
- 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
@@ -185,14 +193,14 @@ 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 | | Exact 3.3.5a default semantics | Unknown | No original-client binding fixture in this package | Capture build-12340 defaults and mouse-turn/strafe policy |
| Production profile exclusion of sprint/flight | Partial | Debug actions are named explicitly | Add typed profile/capability gate in M02 | | 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. - The defaults preserve the pre-M02 sandbox behavior, not verified original-client semantics.
- Sprint and free flight are still executable by the sandbox controller until a - 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,10 +213,12 @@ 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` | Execution gate for sampled 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/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
@@ -13,25 +13,32 @@ 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 _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
) -> 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 +46,18 @@ 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
_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. ## Toggles sandbox free flight and clears velocity when the profile permits it.
## Returns the new flight state. ## 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 +97,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
@@ -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,9 @@ 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
## 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
@@ -55,7 +59,8 @@ 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))
) )
_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
+9 -1
View File
@@ -102,7 +102,15 @@ 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(
@@ -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)