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" const PROFILE_FIXTURE_PATH := "res://src/tests/fixtures/player_input_profile_335.json" func _initialize() -> void: _run_verification.call_deferred() func _run_verification() -> void: var failures: Array[String] = [] _verify_default_input_actions(failures) _verify_profile_fixture(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=9 profiles=3 fixture=1 controller=1 regression_scene=3" % 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_FORWARD, KEY_UP, false, failures) _expect_key_binding(PlayerInputActions.MOVE_BACKWARD, KEY_S, true, failures) _expect_key_binding(PlayerInputActions.MOVE_BACKWARD, KEY_DOWN, false, failures) _expect_key_binding(PlayerInputActions.STRAFE_LEFT, KEY_A, true, failures) _expect_key_binding(PlayerInputActions.STRAFE_RIGHT, KEY_D, true, failures) _expect_key_binding(PlayerInputActions.BLIZZLIKE_STRAFE_LEFT, KEY_Q, true, failures) _expect_key_binding(PlayerInputActions.BLIZZLIKE_STRAFE_RIGHT, KEY_E, true, failures) _expect_key_binding(PlayerInputActions.TURN_LEFT, KEY_A, true, failures) _expect_key_binding(PlayerInputActions.TURN_LEFT, KEY_LEFT, false, failures) _expect_key_binding(PlayerInputActions.TURN_RIGHT, KEY_D, true, failures) _expect_key_binding(PlayerInputActions.TURN_RIGHT, KEY_RIGHT, false, failures) _expect_key_binding(PlayerInputActions.DEBUG_FLY_UP, KEY_E, true, failures) _expect_key_binding(PlayerInputActions.DEBUG_FLY_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_profile_fixture(failures: Array[String]) -> void: var fixture_text := _read_text(PROFILE_FIXTURE_PATH, failures) var parsed_fixture: Variant = JSON.parse_string(fixture_text) _expect_true(parsed_fixture is Dictionary, "profile fixture parses", failures) if not parsed_fixture is Dictionary: return var fixture := parsed_fixture as Dictionary _expect_true(fixture.get("schema_version") == 1, "profile fixture schema", failures) _expect_true(fixture.get("profile_id") == "Blizzlike335", "profile fixture identity", failures) var sources := fixture.get("sources", {}) as Dictionary var bindings_source := sources.get("original_client_default_bindings", {}) as Dictionary _expect_true(bindings_source.get("build") == 12340, "fixture client build", failures) _expect_true( bindings_source.get("sha256") == "35FFA0E4E2356A13A0633A848F9E57ABE7DF9D8A16C2245E15D33013AD2F56A5", "fixture binding source hash", failures ) var commands_source := sources.get("original_client_binding_commands", {}) as Dictionary _expect_true( commands_source.get("sha256") == "2E01276AFB7462F1417710F13B4BF1A456341F7F28CC8AE0D6D9C7442D7F6793", "fixture command source hash", failures ) var turn_source := sources.get("trinitycore_player_turn_rate", {}) as Dictionary _expect_near(float(turn_source.get("radians_per_second", 0.0)), 3.141594, "fixture turn rate", failures) var mouse_strafe_source := sources.get("wowee_mouse_strafe_reference", {}) as Dictionary _expect_true( mouse_strafe_source.get("revision") == "626243e937fb93965fa583a6507ed5a1aa7dda4b", "fixture mouse-strafe reference revision", failures ) _expect_true((fixture.get("selected_default_bindings", []) as Array).size() == 12, "fixture selected binding count", failures) func _verify_move_intent_composition(failures: Array[String]) -> void: 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) var blizzlike_strafe := PlayerInputSource.compose_blizzlike_335_move_intent(0.0, 0.0, 1.0, 0.0, 0.0, 0.0, false) _expect_near(blizzlike_strafe.strafe_axis, -1.0, "blizzlike Q strafe", failures) var blizzlike_turn := PlayerInputSource.compose_blizzlike_335_move_intent(0.0, 0.0, 0.0, 0.0, 0.0, 1.0, false) _expect_near(blizzlike_turn.turn_axis, 1.0, "blizzlike D turn", failures) var mouse_strafe := PlayerInputSource.compose_blizzlike_335_move_intent(0.0, 0.0, 0.0, 0.0, 0.0, 1.0, true) _expect_near(mouse_strafe.strafe_axis, 1.0, "camera-held D strafe", failures) _expect_near(mouse_strafe.turn_axis, 0.0, "camera-held D suppresses turn", failures) _expect_true(PlayerInputSource.new(&"RenderSandbox").input_profile_id == &"RenderSandbox", "sandbox profile selected", failures) _expect_true(PlayerInputSource.new(&"Blizzlike335").input_profile_id == &"Blizzlike335", "blizzlike profile selected", failures) _expect_true(PlayerInputSource.new(&"Unknown").input_profile_id == &"Blizzlike335", "unknown profile fails closed", failures) func _verify_controller_boundary(failures: Array[String]) -> void: 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_rig := player.get_node("CameraPivot") as ThirdPersonCameraRig var camera_distance_before := camera_rig.requested_distance_units 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(camera_rig.requested_distance_units, 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() var blizzlike_player := packed_scene.instantiate() as CharacterBody3D blizzlike_player.set("movement_profile_id", "Blizzlike335") root.add_child(blizzlike_player) blizzlike_player.set_physics_process(false) Input.action_press(PlayerInputActions.TURN_RIGHT) blizzlike_player.call("_physics_process", 0.5) Input.action_release(PlayerInputActions.TURN_RIGHT) _expect_near(blizzlike_player.rotation.y, -3.141594 * 0.5, "blizzlike D turns right", failures) var blizzlike_camera := blizzlike_player.get_node("CameraPivot") as ThirdPersonCameraRig _expect_near(blizzlike_camera.yaw_radians, blizzlike_player.rotation.y, "keyboard turn synchronizes camera yaw", failures) blizzlike_player.free() var mouse_strafe_player := packed_scene.instantiate() as CharacterBody3D mouse_strafe_player.set("movement_profile_id", "Blizzlike335") root.add_child(mouse_strafe_player) mouse_strafe_player.set_physics_process(false) Input.action_press(PlayerInputActions.CAMERA_ROTATE) Input.action_press(PlayerInputActions.TURN_RIGHT) mouse_strafe_player.call("_physics_process", 0.5) Input.action_release(PlayerInputActions.TURN_RIGHT) Input.action_release(PlayerInputActions.CAMERA_ROTATE) _expect_near(mouse_strafe_player.rotation.y, 0.0, "camera-held D does not turn", failures) _expect_near(mouse_strafe_player.position.x, 2.25, "camera-held D strafes right", failures) mouse_strafe_player.free() func _expect_key_binding( 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)