435e1c95d2
Work-Package: M02-GMP-MOVEMENT-001 Agent: sindo-main-codex Tests: local movement and player input regressions; asset-free scene; renderer dry-run; coordinate, streaming, documentation and coordination gates Fidelity: preserves current sandbox speed, acceleration, sprint and camera-relative flight behavior; build-12340 parity remains unverified
163 lines
7.5 KiB
GDScript
163 lines
7.5 KiB
GDScript
extends SceneTree
|
|
|
|
## Headless M02 contract regression for remappable actions and MoveIntent composition.
|
|
|
|
const MOVE_INTENT_PATH := "res://src/domain/input/move_intent.gd"
|
|
const PLAYER_INPUT_SOURCE_PATH := "res://src/gameplay/input/player_input_source.gd"
|
|
const PLAYER_CONTROLLER_PATH := "res://src/scenes/player/third_person_wow_controller.gd"
|
|
const REGRESSION_SCENE_PATH := "res://src/tests/scenes/player_input_regression.tscn"
|
|
|
|
|
|
func _initialize() -> void:
|
|
_run_verification.call_deferred()
|
|
|
|
|
|
func _run_verification() -> void:
|
|
var failures: Array[String] = []
|
|
_verify_default_input_actions(failures)
|
|
_verify_move_intent_composition(failures)
|
|
_verify_controller_boundary(failures)
|
|
_verify_regression_scene(failures)
|
|
|
|
if not failures.is_empty():
|
|
for failure in failures:
|
|
push_error("PLAYER_INPUT: %s" % failure)
|
|
quit(1)
|
|
return
|
|
|
|
print("PLAYER_INPUT PASS actions=%d intent_cases=6 controller=1 regression_scene=1" % PlayerInputActions.REQUIRED_ACTIONS.size())
|
|
quit(0)
|
|
|
|
|
|
func _verify_default_input_actions(failures: Array[String]) -> void:
|
|
for action_name in PlayerInputActions.REQUIRED_ACTIONS:
|
|
_expect_true(InputMap.has_action(action_name), "Input Map contains %s" % action_name, failures)
|
|
|
|
_expect_key_binding(PlayerInputActions.MOVE_FORWARD, KEY_W, true, failures)
|
|
_expect_key_binding(PlayerInputActions.MOVE_BACKWARD, KEY_S, true, failures)
|
|
_expect_key_binding(PlayerInputActions.STRAFE_LEFT, KEY_A, true, failures)
|
|
_expect_key_binding(PlayerInputActions.STRAFE_RIGHT, KEY_D, true, failures)
|
|
_expect_key_binding(PlayerInputActions.DEBUG_FLY_UP, KEY_E, true, failures)
|
|
_expect_key_binding(PlayerInputActions.DEBUG_FLY_DOWN, KEY_Q, true, failures)
|
|
_expect_key_binding(PlayerInputActions.DEBUG_SPRINT, KEY_SHIFT, false, failures)
|
|
_expect_key_binding(PlayerInputActions.DEBUG_TOGGLE_FLIGHT, KEY_SPACE, true, failures)
|
|
_expect_key_binding(PlayerInputActions.RELEASE_CURSOR, KEY_ESCAPE, false, failures)
|
|
_expect_mouse_binding(PlayerInputActions.CAMERA_ROTATE, MOUSE_BUTTON_RIGHT, failures)
|
|
_expect_mouse_binding(PlayerInputActions.CAMERA_ZOOM_IN, MOUSE_BUTTON_WHEEL_UP, failures)
|
|
_expect_mouse_binding(PlayerInputActions.CAMERA_ZOOM_OUT, MOUSE_BUTTON_WHEEL_DOWN, failures)
|
|
|
|
|
|
func _verify_move_intent_composition(failures: Array[String]) -> void:
|
|
var neutral := PlayerInputSource.compose_move_intent(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false)
|
|
_expect_near(neutral.forward_axis, 0.0, "neutral forward", failures)
|
|
_expect_true(not neutral.has_translation(), "neutral has no translation", failures)
|
|
|
|
var forward := PlayerInputSource.compose_move_intent(1.0, 0.0, 0.0, 0.0, 0.0, 0.0, false)
|
|
_expect_near(forward.forward_axis, 1.0, "forward axis", failures)
|
|
|
|
var canceled := PlayerInputSource.compose_move_intent(1.0, 1.0, 1.0, 1.0, 1.0, 1.0, false)
|
|
_expect_true(not canceled.has_translation(), "opposite actions cancel", failures)
|
|
|
|
var diagonal := PlayerInputSource.compose_move_intent(1.0, 0.0, 0.0, 1.0, 0.0, 0.0, false)
|
|
_expect_near(Vector2(diagonal.strafe_axis, diagonal.forward_axis).length(), 1.0, "diagonal normalized", failures)
|
|
|
|
var vertical := PlayerInputSource.compose_move_intent(0.0, 0.0, 0.0, 0.0, 1.5, -0.5, false)
|
|
_expect_near(vertical.vertical_axis, 1.0, "vertical strengths clamped", failures)
|
|
|
|
var debug_requests := PlayerInputSource.compose_move_intent(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, true)
|
|
_expect_true(debug_requests.is_sprint_requested, "sprint request retained", failures)
|
|
|
|
|
|
func _verify_controller_boundary(failures: Array[String]) -> void:
|
|
var controller_source := _read_text(PLAYER_CONTROLLER_PATH, failures)
|
|
_expect_true(controller_source.contains("sample_move_intent()"), "controller samples PlayerInputSource", failures)
|
|
_expect_true(not controller_source.contains("Input.is_key_pressed"), "controller omits direct keyboard polling", failures)
|
|
_expect_true(not controller_source.contains("KEY_"), "controller omits hardcoded key constants", failures)
|
|
|
|
var move_intent_source := _read_text(MOVE_INTENT_PATH, failures)
|
|
_expect_true(not move_intent_source.contains("extends Node"), "MoveIntent does not inherit Node", failures)
|
|
_expect_true(not move_intent_source.contains("extends Resource"), "MoveIntent does not inherit Resource", failures)
|
|
_expect_true(not move_intent_source.contains("Input."), "MoveIntent does not read engine input", failures)
|
|
|
|
var input_source := _read_text(PLAYER_INPUT_SOURCE_PATH, failures)
|
|
_expect_true(input_source.contains("Input.get_action_strength"), "input source reads action strengths", failures)
|
|
_expect_true(not input_source.contains("is_key_pressed"), "input source omits physical-key polling", failures)
|
|
|
|
|
|
func _verify_regression_scene(failures: Array[String]) -> void:
|
|
var packed_scene := load(REGRESSION_SCENE_PATH) as PackedScene
|
|
_expect_true(packed_scene != null, "regression scene loads", failures)
|
|
if packed_scene == null:
|
|
return
|
|
|
|
var player := packed_scene.instantiate() as CharacterBody3D
|
|
_expect_true(player != null, "regression player instantiates", failures)
|
|
if player == null:
|
|
return
|
|
root.add_child(player)
|
|
player.set_physics_process(false)
|
|
|
|
Input.action_press(PlayerInputActions.MOVE_FORWARD)
|
|
player.call("_physics_process", 1.0)
|
|
Input.action_release(PlayerInputActions.MOVE_FORWARD)
|
|
_expect_near(player.position.z, -7.0, "forward action moves current sandbox controller", failures)
|
|
|
|
var camera_distance_before: float = player.get("camera_distance")
|
|
var zoom_event := InputEventMouseButton.new()
|
|
zoom_event.button_index = MOUSE_BUTTON_WHEEL_UP
|
|
zoom_event.pressed = true
|
|
player.call("_unhandled_input", zoom_event)
|
|
_expect_near(player.get("camera_distance"), camera_distance_before - 1.0, "zoom action updates camera distance", failures)
|
|
|
|
var flight_toggle_event := InputEventAction.new()
|
|
flight_toggle_event.action = PlayerInputActions.DEBUG_TOGGLE_FLIGHT
|
|
flight_toggle_event.pressed = true
|
|
player.call("_unhandled_input", flight_toggle_event)
|
|
var movement_controller: LocalPlayerMovementController = player.get("_local_movement_controller")
|
|
_expect_true(movement_controller.is_flight_enabled, "flight action preserves immediate sandbox toggle", failures)
|
|
player.free()
|
|
|
|
|
|
func _expect_key_binding(
|
|
action_name: StringName,
|
|
expected_keycode: Key,
|
|
uses_physical_keycode: bool,
|
|
failures: Array[String]
|
|
) -> void:
|
|
for input_event in InputMap.action_get_events(action_name):
|
|
if input_event is InputEventKey:
|
|
var key_event := input_event as InputEventKey
|
|
var configured_keycode := key_event.physical_keycode if uses_physical_keycode else key_event.keycode
|
|
if configured_keycode == expected_keycode:
|
|
return
|
|
failures.append("%s missing expected key binding %s" % [action_name, expected_keycode])
|
|
|
|
|
|
func _expect_mouse_binding(
|
|
action_name: StringName,
|
|
expected_button: MouseButton,
|
|
failures: Array[String]
|
|
) -> void:
|
|
for input_event in InputMap.action_get_events(action_name):
|
|
if input_event is InputEventMouseButton and input_event.button_index == expected_button:
|
|
return
|
|
failures.append("%s missing expected mouse binding %s" % [action_name, expected_button])
|
|
|
|
|
|
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_near(actual_value: float, expected_value: float, label: String, failures: Array[String]) -> void:
|
|
if absf(actual_value - expected_value) > 0.000001:
|
|
failures.append("%s expected %.6f, got %.6f" % [label, expected_value, actual_value])
|
|
|
|
|
|
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
|
|
if not actual_value:
|
|
failures.append("%s expected true" % label)
|