extends SceneTree ## Imported geometry, independent morphology, stable identities, and real animation sockets. const PLAYER := preload("res://player/PlayerVisual.tscn") const NPC := preload("res://player/npc/NpcVisual.tscn") const PROFILE_DIR := "res://assets/characters/profiles/" const PRESETS := [ "reference_cat", "fox_rambler", "rabbit_scholar", "badger_baker", "otter_courier", "lop_gardener" ] var failures: Array[String] = [] var checks := 0 func _initialize() -> void: call_deferred("_run") func _run() -> void: var first := PLAYER.instantiate() as Node3D var second := PLAYER.instantiate() as Node3D root.add_child(first) root.add_child(second) first.set_process(false) second.set_process(false) var lean := CharacterAppearanceProfile.new() lean.body_fullness = 0.0 var stout := CharacterAppearanceProfile.new() stout.body_fullness = 1.0 AnimalAppearance.apply_to(first, 0, false, lean) AnimalAppearance.apply_to(second, 0, false, stout) for path in ["Body", "Body/Bag", "Body/Scarf"]: var a := first.get_node(path) as MeshInstance3D var b := second.get_node(path) as MeshInstance3D _check(a.mesh == b.mesh, path + " must share imported geometry") var shape := a.find_blend_shape_by_name("Stout") _check(shape >= 0, path + " must retain its Blender Stout shape") if shape >= 0: _check( is_zero_approx(a.get_blend_shape_value(shape)), "Lean shape weight must remain independent" ) _check( is_equal_approx(b.get_blend_shape_value(shape), 1.0), "Stout shape weight must remain independent" ) _check( first.get_node("Body").material_override != second.get_node("Body").material_override, "Palettes must be per actor" ) var body := first.get_node("Body") as MeshInstance3D var arrays := body.mesh.surface_get_arrays(0) var roles: PackedVector2Array = arrays[Mesh.ARRAY_TEX_UV2] _check(not roles.is_empty(), "Palette roles must survive GLB import in UV2") var coat_vertices := 0 for role in roles: if is_equal_approx(role.x, 3.0): coat_vertices += 1 _check(coat_vertices > 0, "Coat must have an independently recolorable palette role") var child_count := first.find_children("*", "MeshInstance3D", true, false).size() for preset in PRESETS: var profile: CharacterAppearanceProfile = load(PROFILE_DIR + preset + ".tres") AnimalAppearance.apply_to(first, 0, false, profile) _check( first.find_children("*", "MeshInstance3D", true, false).size() == child_count, "Changing profiles must reuse attachment nodes" ) _check( first.get_node("Head/Ears").get_parent() == first.get_node("Head"), "Ears must follow the head" ) _check(first.get_node("Body/Bag").get_parent() == body, "Pack must follow the torso") var triangles := 0 var draws := 0 for mesh_node: MeshInstance3D in first.find_children("*", "MeshInstance3D", true, false): if mesh_node.mesh == null or mesh_node.name in ["Blade", "Grip"]: continue _check( mesh_node.mesh.get_surface_count() == 1, "Each character part must remain one surface" ) triangles += mesh_node.mesh.surface_get_array_index_len(0) / 3 draws += 1 _check( triangles < 23000 and draws <= 12, "Selected character must stay within geometry and surface budgets" ) for extreme in [0, 1]: var profile := CharacterAppearanceProfile.new() profile.height = 0.80 if extreme == 0 else 1.15 profile.leg_length = 0.85 if extreme == 0 else 1.15 profile.head_size = 0.88 if extreme == 0 else 1.12 profile.body_fullness = float(extreme) profile.ear_length = 0.65 if extreme == 0 else 1.20 profile.species = 2 AnimalAppearance.apply_to(first, 0, false, profile) for side in ["Left", "Right"]: var leg := first.get_node("Leg" + side) as MeshInstance3D var bounds := leg.transform * leg.get_aabb() _check( absf(bounds.position.y) < 0.03, "Leg-length variation must keep feet on the ground" ) _check( first.get_meta("character_top") > first.get_node("Head").position.y, "Visible height must include ears" ) # An ID lookup must neither consume global random state nor change its result after another lookup. seed(8123) var expected := randf() seed(8123) var identity := CharacterAppearanceProfile.for_identity(731) for value in 30: CharacterAppearanceProfile.for_identity(value) var rebuilt := CharacterAppearanceProfile.for_identity(731) _check( ( identity.height == rebuilt.height and identity.body_fullness == rebuilt.body_fullness and identity.species == rebuilt.species ), "Identity must rebuild deterministically" ) _check(randf() == expected, "Appearance must not consume global RNG") first.free() second.free() _test_animation_and_cues() await process_frame if failures.is_empty(): print("[TEST] PASS character_appearance (", checks, " checks)") quit(0) else: for failure in failures: push_error(failure) quit(1) func _test_animation_and_cues() -> void: var profile := CharacterAppearanceProfile.new() profile.species = 2 profile.height = 1.15 profile.head_size = 1.12 profile.ear_length = 1.2 profile.leg_length = 1.15 var parent := CharacterBody3D.new() root.add_child(parent) var player := PLAYER.instantiate() as Node3D player.appearance = profile parent.add_child(player) player.set_process(false) parent.velocity = Vector3(2, 0, 0) var heights := AnimalAppearance.rest_heights(player) for frame in 30: player.call("_process", 1.0 / 60.0) _check( player.get_node("Head").position.y >= heights.y, "Player gait must retain configured head height" ) _check( player.get_node("Body").position.y >= heights.x, "Player gait must retain configured torso height" ) var npc := NPC.instantiate() as Node3D npc.appearance = profile root.add_child(npc) npc.set_physics_process(false) npc.set_process(false) npc.call("_apply_personal_details") var cue_height: float = npc.get_node("TaskGlyphRoot").position.y npc.call("_apply_personal_details") _check( is_equal_approx(cue_height, npc.get_node("TaskGlyphRoot").position.y), "Reapplying appearance must not accumulate cue lift" ) _check( cue_height > float(npc.get_meta("character_top")) + 0.2, "NPC cues must clear the tallest supported ears" ) npc.velocity = Vector3(2, 0, 0) for frame in 30: npc.call("_process", 1.0 / 60.0) _check( npc.get_node("Head").position.y >= heights.y, "NPC gait must retain configured head height" ) parent.free() npc.free() func _check(condition: bool, message: String) -> void: checks += 1 if not condition: failures.append(message)