gmp(M02): gate debug movement capabilities #26
@@ -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.
|
||||
@@ -5,10 +5,10 @@
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| 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 |
|
||||
| Last verified | `97bb53f`, 2026-07-14 |
|
||||
| Profiles/capabilities | Current render sandbox; production/debug profile gate pending |
|
||||
| Last verified | `742c415`, 2026-07-14 |
|
||||
| Profiles/capabilities | `RenderSandbox` debug movement; `Blizzlike335` rejects debug sprint/flight |
|
||||
|
||||
## Purpose
|
||||
|
||||
@@ -32,6 +32,8 @@ flowchart LR
|
||||
Input[MoveIntent] --> Movement[LocalPlayerMovementController]
|
||||
Basis[Explicit local movement Basis] --> Movement
|
||||
Delta[Physics delta seconds] --> Movement
|
||||
Profile[Scene profile ID] --> Capabilities[PlayerMovementCapabilities]
|
||||
Capabilities --> Movement
|
||||
Movement --> Displacement[Godot-world displacement]
|
||||
Movement --> Velocity[Read-only velocity state]
|
||||
Scene[ThirdPersonWowController] --> Basis
|
||||
@@ -57,10 +59,14 @@ Forbidden dependencies:
|
||||
|
||||
| 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 |
|
||||
| `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 |
|
||||
|
||||
Constructor units:
|
||||
@@ -79,6 +85,7 @@ Constructor units:
|
||||
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||
|---|---|---|---|---|---|
|
||||
| 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 | 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 |
|
||||
@@ -100,6 +107,9 @@ flowchart LR
|
||||
Basis[Player or camera-pivot Basis] --> Directions[Forward/right directions]
|
||||
Directions --> 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]
|
||||
Target --> Integrate
|
||||
Integrate --> Current[Updated velocity]
|
||||
@@ -112,7 +122,8 @@ flowchart LR
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> 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
|
||||
GroundedSandbox --> GroundedSandbox: advance
|
||||
FlyingSandbox --> FlyingSandbox: advance
|
||||
@@ -128,8 +139,11 @@ sequenceDiagram
|
||||
participant Input as PlayerInputSource
|
||||
participant Scene as ThirdPersonWowController
|
||||
participant Move as LocalPlayerMovementController
|
||||
participant Cap as PlayerMovementCapabilities
|
||||
participant Present as CharacterAnimationPresenter adapter
|
||||
Input-->>Scene: MoveIntent
|
||||
Scene->>Cap: map configured profile once
|
||||
Scene->>Move: construct with immutable capabilities
|
||||
Scene->>Move: advance(intent, selected basis, delta)
|
||||
Move-->>Scene: displacement
|
||||
Scene->>Scene: apply global position and ground snap
|
||||
@@ -140,6 +154,8 @@ sequenceDiagram
|
||||
## Ownership, threading and resources
|
||||
|
||||
- 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.
|
||||
- All mutations currently occur on the main input/physics thread.
|
||||
- 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 |
|
||||
| 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/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 |
|
||||
|
||||
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 |
|
||||
| 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 |
|
||||
| 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
|
||||
|
||||
@@ -188,6 +209,8 @@ separate versioned movement snapshot contract.
|
||||
- Unit/contract: `src/tools/verify_local_player_movement.gd` covers forward,
|
||||
backward, strafe, rotated basis, acceleration, deceleration, sprint, flight,
|
||||
pitched camera basis, state reset and invalid delta.
|
||||
- Capability: `verify_player_movement_capabilities.gd` covers three profile-ID
|
||||
cases, safe defaults, sprint/flight allow/reject behavior and two real scene compositions.
|
||||
- Boundary: verifier rejects Node/Input/Camera/ADT dependencies and movement
|
||||
integration/state remaining in the player scene.
|
||||
- Scene integration: `verify_player_input.gd` drives the asset-free player
|
||||
@@ -202,7 +225,8 @@ separate versioned movement snapshot contract.
|
||||
- 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 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
|
||||
|
||||
@@ -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 |
|
||||
| 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 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 |
|
||||
@@ -223,7 +248,10 @@ separate versioned movement snapshot contract.
|
||||
properties does not reconfigure an existing controller.
|
||||
- Ground snap remains in the player scene, but its ADT data access is now behind
|
||||
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
|
||||
predictor may require scalar/domain movement contracts.
|
||||
|
||||
@@ -232,11 +260,13 @@ separate versioned movement snapshot contract.
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `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/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/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_player_movement_capabilities.gd` | Profile factory, allow/reject and real-scene regression |
|
||||
| `src/tests/scenes/player_input_regression.tscn` | Asset-free integrated player fixture |
|
||||
| `src/tools/verify_player_input.gd` | Input-to-movement scene regression |
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| 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 |
|
||||
| Last verified | `8c1cf9b`, 2026-07-14 |
|
||||
| Profiles/capabilities | Current render-sandbox defaults; `Blizzlike335` binding semantics not yet verified |
|
||||
| Last verified | `742c415`, 2026-07-14 |
|
||||
| Profiles/capabilities | Debug requests sampled in all profiles; execution allowed only by `RenderSandbox` movement capabilities |
|
||||
|
||||
## Purpose
|
||||
|
||||
@@ -21,8 +21,8 @@ independently testable local movement controller.
|
||||
- Define authoritative server movement, prediction or reconciliation.
|
||||
- Own character transform, terrain queries, camera state or animation state.
|
||||
- Claim that the current W/S/A/D sandbox mapping exactly matches build 12340.
|
||||
- Make sprint or free flight available to the production compatibility profile;
|
||||
their profile gate remains a later M02 package.
|
||||
- Suppress debug action sampling by physical device or UI context; execution is
|
||||
rejected at the typed movement capability boundary.
|
||||
|
||||
## Context and boundaries
|
||||
|
||||
@@ -32,6 +32,7 @@ flowchart LR
|
||||
InputMap --> Source[PlayerInputSource]
|
||||
Source --> Intent[MoveIntent]
|
||||
Intent --> Movement[LocalPlayerMovementController]
|
||||
Profile[PlayerMovementCapabilities] --> Movement
|
||||
Movement --> Controller[ThirdPersonWowController adapter]
|
||||
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 | Mouse action events | Godot `_unhandled_input` dispatch | `ThirdPersonCameraRig` through player dispatch | Engine-owned event | Current input dispatch only |
|
||||
| Output | `MoveIntent` | `PlayerInputSource` | `LocalPlayerMovementController` through scene composition | Immutable caller-held value | One physics tick |
|
||||
| Output | Debug sprint request/vertical axes | `PlayerInputSource` | Capability-gated movement controller | Immutable intent fields | One physics tick |
|
||||
| Output | Camera action names | `PlayerInputActions` | `ThirdPersonCameraRig` | Static constants | Process lifetime |
|
||||
|
||||
Side effects:
|
||||
@@ -84,6 +86,7 @@ flowchart LR
|
||||
Map -->|camera events| Camera[ThirdPersonCameraRig]
|
||||
Source -->|clamp cancel normalize| Intent[MoveIntent]
|
||||
Intent --> Consumer[LocalPlayerMovementController]
|
||||
Profile[PlayerMovementCapabilities] --> Consumer
|
||||
Consumer --> Adapter[ThirdPersonWowController]
|
||||
```
|
||||
|
||||
@@ -92,7 +95,9 @@ flowchart LR
|
||||
The adapter is stateless. The scene composition root creates one
|
||||
`PlayerInputSource` and `LocalPlayerMovementController` in `_ready`; each physics
|
||||
tick produces a new immutable intent. Flight enabled/disabled state is owned by
|
||||
the movement controller and is not hidden in the input adapter.
|
||||
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
|
||||
|
||||
@@ -127,6 +132,7 @@ sequenceDiagram
|
||||
| Conflicting opposite actions | Pure composition | Strengths cancel deterministically | Covered by contract test | Release/remap one action |
|
||||
| Strength outside range | Pure composition | Value clamps to `[0, 1]` before axis calculation | Covered by contract test | Input adapter may be corrected independently |
|
||||
| Controller disabled/unloaded | Scene lifecycle | Sampling stops with physics processing | Existing scene lifecycle | Re-enable or recreate scene |
|
||||
| Debug request in `Blizzlike335` | Movement capability check | Request is sampled but has no movement effect | Capability regression | Use `RenderSandbox` only when debug movement is intended |
|
||||
|
||||
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 |
|
||||
| Camera rotate | Right mouse button | Current render sandbox | Yes | Captures/releases mouse for orbit |
|
||||
| Camera zoom | Mouse wheel | Current render sandbox | Yes | Adjusts current camera distance |
|
||||
| Debug sprint | Shift | Sandbox debug only by intent; profile gate pending | Yes | Requests existing speed multiplier |
|
||||
| Debug flight up/down/toggle | E/Q/Space | Sandbox debug only by intent; profile gate pending | Yes | Requests existing free-flight controls |
|
||||
| Debug sprint | Shift | Executable only in `RenderSandbox` | Yes | Requests multiplier; `Blizzlike335` ignores it |
|
||||
| Debug flight up/down/toggle | E/Q/Space | Executable only in `RenderSandbox` | Yes | Requests free flight; `Blizzlike335` remains grounded |
|
||||
|
||||
## Persistence, cache and migration
|
||||
|
||||
@@ -162,6 +168,8 @@ provide a settings migration once user settings are persisted.
|
||||
the verifier, which drives forward, camera-zoom and immediate sandbox-flight
|
||||
actions through the real scene controller and also rejects direct
|
||||
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,
|
||||
right mouse and wheel bindings. This is observable-regression coverage only.
|
||||
- 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 |
|
||||
| `PlayerInputSource → MoveIntent` seam | Implemented | Pure composition and controller boundary checks | Local movement consumer is now extracted |
|
||||
| Exact 3.3.5a default semantics | Unknown | No original-client binding fixture in this package | Capture build-12340 defaults and mouse-turn/strafe policy |
|
||||
| Production profile exclusion of sprint/flight | Partial | Debug actions are named explicitly | Add typed profile/capability gate in M02 |
|
||||
| Compatibility-profile exclusion of sprint/flight | Implemented | Typed capability and two real-scene regressions | Application shell selection remains future work |
|
||||
| Jump/fall/swim/fly gameplay semantics | Planned | M02 target and gameplay roadmap | Implement after terrain/movement state extraction |
|
||||
|
||||
## Known gaps and risks
|
||||
|
||||
- The defaults preserve the pre-M02 sandbox behavior, not verified original-client semantics.
|
||||
- Sprint and free flight are still executable by the sandbox controller until a
|
||||
typed build-profile gate is introduced.
|
||||
- Input Map still exposes debug bindings process-wide; the movement consumer now
|
||||
rejects their effects outside `RenderSandbox`.
|
||||
- Camera state is extracted; active collision remains an explicit planned policy.
|
||||
- There is no persisted user keybinding format or UI yet.
|
||||
|
||||
@@ -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_source.gd` | Engine Input Map adapter and pure composition |
|
||||
| `src/gameplay/movement/local_player_movement_controller.gd` | Scene-free movement consumer |
|
||||
| `src/gameplay/movement/player_movement_capabilities.gd` | Execution gate for sampled debug requests |
|
||||
| `src/scenes/player/third_person_camera_rig.gd` | Camera-action consumer and orbit/zoom state |
|
||||
| `src/scenes/player/third_person_wow_controller.gd` | Composition and input-event dispatcher |
|
||||
| `src/tests/scenes/player_input_regression.tscn` | Asset-free controller movement/camera regression fixture |
|
||||
| `src/tools/verify_player_input.gd` | Headless contract/integration regression |
|
||||
| `src/tools/verify_player_movement_capabilities.gd` | Sandbox versus compatibility-profile integration regression |
|
||||
|
||||
## Related decisions and references
|
||||
|
||||
|
||||
@@ -13,25 +13,32 @@ var is_flight_enabled: bool:
|
||||
get:
|
||||
return _is_flight_enabled
|
||||
|
||||
var movement_profile_id: StringName:
|
||||
get:
|
||||
return _movement_capabilities.profile_id
|
||||
|
||||
var _run_speed_units_per_second: float
|
||||
var _backward_speed_units_per_second: float
|
||||
var _strafe_speed_units_per_second: float
|
||||
var _flight_vertical_speed_units_per_second: float
|
||||
var _acceleration_units_per_second_squared: float
|
||||
var _sandbox_sprint_multiplier: float
|
||||
var _movement_capabilities: PlayerMovementCapabilities
|
||||
var _godot_world_velocity_units_per_second := Vector3.ZERO
|
||||
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.
|
||||
## A null capability input fails closed to Blizzlike335 debug permissions.
|
||||
func _init(
|
||||
run_speed_units_per_second: float,
|
||||
backward_speed_units_per_second: float,
|
||||
strafe_speed_units_per_second: float,
|
||||
flight_vertical_speed_units_per_second: float,
|
||||
acceleration_units_per_second_squared: float,
|
||||
sandbox_sprint_multiplier: float
|
||||
sandbox_sprint_multiplier: float,
|
||||
movement_capabilities: PlayerMovementCapabilities = null
|
||||
) -> void:
|
||||
_run_speed_units_per_second = run_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
|
||||
_acceleration_units_per_second_squared = acceleration_units_per_second_squared
|
||||
_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.
|
||||
## Returns the new flight state.
|
||||
## 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:
|
||||
if not _movement_capabilities.allows_debug_free_flight:
|
||||
return false
|
||||
_is_flight_enabled = not _is_flight_enabled
|
||||
_godot_world_velocity_units_per_second = Vector3.ZERO
|
||||
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
|
||||
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 := (
|
||||
forward_direction * move_intent.forward_axis * forward_speed
|
||||
+ 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_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 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")
|
||||
|
||||
@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 flight_vertical_speed: float = 7.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_snap_speed: float = 24.0
|
||||
|
||||
@@ -55,7 +59,8 @@ func _ready() -> void:
|
||||
strafe_speed,
|
||||
flight_vertical_speed,
|
||||
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
|
||||
_appearance_presenter = get_node_or_null(visual_path) as CharacterAppearancePresenter
|
||||
|
||||
@@ -102,7 +102,15 @@ func _verify_scene_boundary(failures: Array[String]) -> void:
|
||||
|
||||
|
||||
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(
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user