extends SceneTree ## Asset-free M02 appearance, animation and player-boundary regression. const MODEL_FIXTURE_PATH := "res://src/tests/scenes/character_presentation_model_fixture.tscn" const PLAYER_CONTROLLER_PATH := "res://src/scenes/player/third_person_wow_controller.gd" const APPEARANCE_PRESENTER_PATH := "res://src/scenes/character/character_appearance_presenter.gd" const ANIMATION_PRESENTER_PATH := "res://src/scenes/character/character_animation_presenter.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", ] func _initialize() -> void: _run_verification.call_deferred() func _run_verification() -> void: var failures: Array[String] = [] await _verify_appearance_presenter(failures) _verify_animation_presenter(failures) _verify_source_boundaries(failures) _verify_scene_wiring(failures) if not failures.is_empty(): for failure in failures: push_error("CHARACTER_PRESENTATION: %s" % failure) quit(1) return print("CHARACTER_PRESENTATION PASS appearance_cases=9 animation_cases=10 scenes=3 player_boundary=1") quit(0) func _verify_appearance_presenter(failures: Array[String]) -> void: var presenter := CharacterAppearancePresenter.new() presenter.character_model_path = MODEL_FIXTURE_PATH presenter.character_scale = 2.0 presenter.character_yaw_offset_degrees = 45.0 root.add_child(presenter) var ready_roots: Array[Node3D] = [] presenter.character_appearance_ready.connect(func(character_root: Node3D) -> void: ready_roots.append(character_root)) _expect_true(presenter.load_character_appearance("res://missing-extracted-fixture"), "fixture load starts", failures) await process_frame var character_root := presenter.character_root _expect_true(character_root != null, "character root composed", failures) if character_root != null: _expect_near(character_root.scale.x, 2.0, "uniform character scale", failures) _expect_near(character_root.rotation_degrees.y, 45.0, "character yaw offset", failures) _expect_near(character_root.position.y, -2.0, "character grounded after scale", failures) _expect_true(character_root.has_node("CharacterTextureCompositor"), "texture compositor composed", failures) _expect_true(ready_roots.size() == 1, "appearance ready emitted", failures) _expect_true(presenter.extracted_data_directory == "res://missing-extracted-fixture", "content root override retained", failures) var previous_root := character_root _expect_true(presenter.load_character_appearance(), "replacement load starts", failures) _expect_true(presenter.character_root != previous_root, "replacement swaps owned root", failures) await process_frame _expect_true(ready_roots.size() == 2, "replacement ready emitted", failures) presenter.character_model_path = "" _expect_true(not presenter.load_character_appearance(), "empty model rejected", failures) _expect_true(presenter.character_root == null, "empty model clears old root", failures) await process_frame presenter.free() func _verify_animation_presenter(failures: Array[String]) -> void: var animation_root := Node.new() var nested_node := Node.new() var animation_player := AnimationPlayer.new() animation_root.add_child(nested_node) nested_node.add_child(animation_player) var animation_library := AnimationLibrary.new() for animation_name in ["Stand", "Run", "Emote"]: animation_library.add_animation(animation_name, Animation.new()) animation_player.add_animation_library("", animation_library) var presenter := CharacterAnimationPresenter.new() presenter.animation_blend_time_seconds = 0.25 _expect_true(presenter.bind_character_root(animation_root), "nested animation player bound", failures) _expect_true(presenter.has_animation_player, "animation player state exposed", failures) _expect_true(presenter.active_animation_name == "Stand", "stationary animation selected", failures) _expect_true(animation_player.current_animation == "Stand", "stationary animation playing", failures) _expect_near(animation_player.playback_default_blend_time, 0.25, "default blend configured", failures) _expect_true(animation_player.get_animation("Stand").loop_mode == Animation.LOOP_LINEAR, "stand loops", failures) _expect_true(animation_player.get_animation("Run").loop_mode == Animation.LOOP_LINEAR, "run loops", failures) _expect_true(animation_player.get_animation("Emote").loop_mode == Animation.LOOP_NONE, "unrelated animation unchanged", failures) _expect_true(presenter.present_locomotion(true), "moving transition starts", failures) _expect_true(presenter.active_animation_name == "Run", "moving animation selected", failures) _expect_true(not presenter.present_locomotion(true), "duplicate moving state ignored", failures) var fallback_root := Node.new() var fallback_player := AnimationPlayer.new() fallback_root.add_child(fallback_player) var fallback_library := AnimationLibrary.new() fallback_library.add_animation("CharacterStandLoop", Animation.new()) fallback_library.add_animation("FastRunCycle", Animation.new()) fallback_player.add_animation_library("", fallback_library) _expect_true(presenter.bind_character_root(fallback_root), "fallback animation player bound", failures) _expect_true(presenter.active_animation_name == "CharacterStandLoop", "substring stationary fallback", failures) presenter.present_locomotion(true) _expect_true(presenter.active_animation_name == "FastRunCycle", "substring moving fallback", failures) fallback_root.free() _expect_true(not presenter.present_locomotion(false), "freed animation root rejected", failures) _expect_true(not presenter.has_animation_player, "freed animation binding cleared", failures) var missing_animation_root := Node.new() _expect_true(not presenter.bind_character_root(missing_animation_root), "missing animation player rejected", failures) _expect_true(not presenter.has_animation_player, "missing binding clears state", failures) presenter.free() animation_root.free() missing_animation_root.free() func _verify_source_boundaries(failures: Array[String]) -> void: var player_source := _read_text(PLAYER_CONTROLLER_PATH, failures) for forbidden_text in [ "GEOSET_CONTROLLER_SCRIPT", "TEXTURE_COMPOSITOR_SCRIPT", "OUTFIT_RESOLVER_SCRIPT", "func _load_character_visual", "func _find_animation_player", "func _calculate_aabb", "_animation_player", ]: _expect_true(not player_source.contains(forbidden_text), "player omits %s" % forbidden_text, failures) _expect_true(player_source.contains("_appearance_presenter.load_character_appearance"), "player delegates appearance", failures) _expect_true(player_source.contains("_animation_presenter.present_locomotion"), "player delegates animation", failures) var appearance_source := _read_text(APPEARANCE_PRESENTER_PATH, failures) for forbidden_text in ["MoveIntent", "TerrainQuery", "ThirdPersonCameraRig", "AnimationPlayer"]: _expect_true(not appearance_source.contains(forbidden_text), "appearance omits %s" % forbidden_text, failures) var animation_source := _read_text(ANIMATION_PRESENTER_PATH, failures) for forbidden_text in ["load(character_model_path)", "TerrainQuery", "MoveIntent", "Camera3D"]: _expect_true(not animation_source.contains(forbidden_text), "animation 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("character_appearance_presenter.gd"), "%s references appearance presenter" % scene_path, failures) _expect_true(scene_source.contains("character_animation_presenter.gd"), "%s references animation presenter" % scene_path, failures) _expect_true(scene_source.contains('name="AnimationPresenter"'), "%s composes animation presenter" % scene_path, failures) 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)