extends SceneTree ## Headless M02 camera state, scene wiring and collision-policy regression. const PLAYER_CONTROLLER_PATH := "res://src/scenes/player/third_person_wow_controller.gd" const CAMERA_RIG_PATH := "res://src/scenes/player/third_person_camera_rig.gd" const NO_COLLISION_POLICY_PATH := "res://src/scenes/player/no_camera_collision_policy.gd" const REGRESSION_SCENE_PATH := "res://src/tests/scenes/player_input_regression.tscn" const RUNTIME_SCENE_PATHS: Array[String] = [ "res://src/scenes/streaming/eastern_kingdoms_streaming.tscn", "res://src/scenes/streaming/kalimdor_streaming.tscn", ] class FixedDistanceCameraCollisionPolicy extends CameraCollisionPolicy: var _fixed_distance_units: float func _init(fixed_distance_units: float) -> void: _fixed_distance_units = fixed_distance_units func resolve_camera_distance( _camera_pivot: Node3D, _requested_distance_units: float ) -> float: return _fixed_distance_units func _initialize() -> void: _run_verification.call_deferred() func _run_verification() -> void: var failures: Array[String] = [] var player := _instantiate_regression_player(failures) if player != null: _verify_initial_state(player, failures) _verify_zoom_clamps(player, failures) _verify_orbit_and_capture(player, failures) _verify_collision_policy(player, failures) player.free() _verify_source_boundaries(failures) _verify_scene_wiring(failures) if not failures.is_empty(): for failure in failures: push_error("THIRD_PERSON_CAMERA: %s" % failure) quit(1) return print("THIRD_PERSON_CAMERA PASS state_cases=12 policy_cases=3 scenes=3 player_boundary=1") quit(0) func _instantiate_regression_player(failures: Array[String]) -> CharacterBody3D: var packed_scene := load(REGRESSION_SCENE_PATH) as PackedScene _expect_true(packed_scene != null, "regression scene loads", failures) if packed_scene == null: return null var player := packed_scene.instantiate() as CharacterBody3D _expect_true(player != null, "regression player instantiates", failures) if player == null: return null root.add_child(player) player.set_physics_process(false) var camera_rig := player.get_node("CameraPivot") as ThirdPersonCameraRig camera_rig.set_physics_process(false) return player func _verify_initial_state(player: CharacterBody3D, failures: Array[String]) -> void: var camera_rig := player.get_node("CameraPivot") as ThirdPersonCameraRig var camera := camera_rig.get_node("Camera3D") as Camera3D _expect_near(camera_rig.yaw_radians, 0.0, "initial yaw", failures) _expect_near(camera_rig.pitch_radians, deg_to_rad(-18.0), "initial pitch", failures) _expect_near(camera_rig.requested_distance_units, 8.0, "initial requested distance", failures) _expect_near(camera_rig.resolved_distance_units, 8.0, "initial resolved distance", failures) _expect_near(camera_rig.position.y, 1.7, "initial pivot height", failures) _expect_near(camera.position.z, 8.0, "initial camera local Z", failures) _expect_near(camera.far, 50000.0, "camera far plane", failures) _expect_true(camera_rig.godot_world_flight_movement_basis().is_equal_approx(camera_rig.global_basis), "flight basis is pivot basis", failures) func _verify_zoom_clamps(player: CharacterBody3D, failures: Array[String]) -> void: var camera_rig := player.get_node("CameraPivot") as ThirdPersonCameraRig var zoom_in_event := _action_event(PlayerInputActions.CAMERA_ZOOM_IN, true) for _step in range(20): camera_rig.handle_camera_input(zoom_in_event) _expect_near(camera_rig.requested_distance_units, 2.0, "minimum zoom clamp", failures) for _step in range(30): camera_rig.handle_camera_input(_action_event(PlayerInputActions.CAMERA_ZOOM_OUT, true)) _expect_near(camera_rig.requested_distance_units, 18.0, "maximum zoom clamp", failures) func _verify_orbit_and_capture(player: CharacterBody3D, failures: Array[String]) -> void: var camera_rig := player.get_node("CameraPivot") as ThirdPersonCameraRig _expect_true(camera_rig.handle_camera_input(_action_event(PlayerInputActions.CAMERA_ROTATE, true)), "capture action consumed", failures) _expect_true(camera_rig.is_mouse_captured, "mouse capture state", failures) var mouse_motion := InputEventMouseMotion.new() mouse_motion.relative = Vector2(100.0, -50.0) _expect_true(camera_rig.handle_camera_input(mouse_motion), "captured mouse motion consumed", failures) _expect_near(camera_rig.yaw_radians, -0.3, "orbit yaw", failures) _expect_near(camera_rig.pitch_radians, deg_to_rad(-18.0) + 0.15, "orbit pitch", failures) _expect_near(player.rotation.y, -0.3, "character follows orbit yaw", failures) var clamp_up_motion := InputEventMouseMotion.new() clamp_up_motion.relative = Vector2(0.0, -100000.0) camera_rig.handle_camera_input(clamp_up_motion) _expect_near(camera_rig.pitch_radians, deg_to_rad(35.0), "maximum pitch clamp", failures) var clamp_down_motion := InputEventMouseMotion.new() clamp_down_motion.relative = Vector2(0.0, 100000.0) camera_rig.handle_camera_input(clamp_down_motion) _expect_near(camera_rig.pitch_radians, deg_to_rad(-65.0), "minimum pitch clamp", failures) camera_rig.handle_camera_input(_action_event(PlayerInputActions.CAMERA_ROTATE, false)) _expect_true(not camera_rig.is_mouse_captured, "mouse release state", failures) func _verify_collision_policy(player: CharacterBody3D, failures: Array[String]) -> void: var camera_rig := player.get_node("CameraPivot") as ThirdPersonCameraRig var camera := camera_rig.get_node("Camera3D") as Camera3D camera_rig.set_collision_policy(FixedDistanceCameraCollisionPolicy.new(3.0)) _expect_near(camera_rig.resolved_distance_units, 3.0, "fixed policy distance", failures) _expect_near(camera.position.z, 3.0, "fixed policy camera transform", failures) camera_rig.set_collision_policy(NoCameraCollisionPolicy.new()) _expect_near(camera_rig.resolved_distance_units, 18.0, "identity policy restores request", failures) func _verify_source_boundaries(failures: Array[String]) -> void: var player_source := _read_text(PLAYER_CONTROLLER_PATH, failures) for forbidden_text in ["var _captured", "var _yaw", "var _pitch", "func _apply_camera_transform", "camera_distance"]: _expect_true(not player_source.contains(forbidden_text), "player omits %s" % forbidden_text, failures) _expect_true(player_source.contains("_camera_rig.handle_camera_input"), "player delegates camera input", failures) var rig_source := _read_text(CAMERA_RIG_PATH, failures) _expect_true(rig_source.contains("extends Node3D"), "camera rig owns pivot Node3D", failures) for forbidden_text in ["ADTLoader", "TerrainQuery", "MoveIntent", "AnimationPlayer"]: _expect_true(not rig_source.contains(forbidden_text), "camera rig omits %s" % forbidden_text, failures) var no_collision_source := _read_text(NO_COLLISION_POLICY_PATH, failures) for forbidden_text in ["intersect_ray", "direct_space_state", "PhysicsRayQueryParameters3D"]: _expect_true(not no_collision_source.contains(forbidden_text), "baseline policy omits %s" % forbidden_text, failures) func _verify_scene_wiring(failures: Array[String]) -> void: for scene_path in RUNTIME_SCENE_PATHS + [REGRESSION_SCENE_PATH]: var scene_source := _read_text(scene_path, failures) _expect_true(scene_source.contains("third_person_camera_rig.gd"), "%s references camera rig" % scene_path, failures) _expect_true(scene_source.contains('script = ExtResource("4_camera_rig")') or scene_source.contains('script = ExtResource("2_camera_rig")'), "%s wires CameraPivot script" % scene_path, failures) func _action_event(action_name: StringName, is_pressed: bool) -> InputEventAction: var input_event := InputEventAction.new() input_event.action = action_name input_event.pressed = is_pressed return input_event 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)