Files
open-wc/src/tools/verify_character_presentation.gd
T
sindoring 97bb53f6cd rnd(M02): extract character presenters
Work-Package: M02-RND-CHARACTER-PRESENTATION-001

Agent: sindo-main-codex

Tests: presentation/input/movement/terrain/camera regressions; coordinate/streaming/docs/coordination gates; renderer dry-run 7/7

Fidelity: preserves current sandbox model/outfit and Stand/Idle/Run/Walk behavior; build-12340 parity remains unverified
2026-07-14 23:42:12 +04:00

165 lines
8.4 KiB
GDScript

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)