gmp(M02): gate debug movement capabilities
Work-Package: M02-GMP-SANDBOX-CAPABILITIES-001 Agent: sindo-main-codex Tests: capability/input/movement/terrain/camera/presentation; coordinate/streaming/docs/coordination; renderer dry-run 7/7 Fidelity: proves debug sprint/free-flight exclusion in Blizzlike335; no positive movement parity claim
This commit is contained in:
@@ -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