extends SceneTree ## Regression gate for the authored emote catalog. ## ## The project used to synthesize five routines by adding sine-wave bone ## offsets to one dance clip. That made every emote look like programmer ## animation. The new contract is deliberately simpler: every wheel entry must ## select its own imported clip, the clip must move, and no DanceModifier may ## exist in the live character. const SETTLE_FRAMES := 24 const SAMPLE_FRAMES := 36 const MIN_FRAME_MOTION := 0.01 var _failures := 0 func _init() -> void: await process_frame var manager = root.get_node_or_null("SkinManager") var skin = manager.get_skin("taila") if manager else null if skin == null or skin.model_path == "": _expect(false, "Taila is available for the authored-emote test") _done() return var model := SkinnedPlayerModel.new() root.add_child(model) model.skin_id = "taila" model.load_model(skin.model_path) for _i in 60: model.update_state("idle", 0.0, false) await process_frame _expect(model.loaded and model.skeleton != null, "the authored character and skeleton load") _expect(DanceRoutines.count() == 5, "the wheel exposes five authored emotes (%d)" % DanceRoutines.count()) _expect(model.find_children("*", "DanceModifier", true, false).is_empty(), "no procedural DanceModifier is present") var resolved_clips: Array[String] = [] for index in DanceRoutines.count(): var canonical := DanceRoutines.clip_of(index) var resolved: String = String(model._resolved_clips.get(canonical, "")) _expect(resolved != "", "%s resolves to an imported clip" % DanceRoutines.name_of(index)) _expect(not resolved_clips.has(resolved), "%s uses its own authored clip" % DanceRoutines.name_of(index)) resolved_clips.append(resolved) model.set_dancing(true, index) for _i in SETTLE_FRAMES: model.update_state("idle", 0.0, false) await process_frame _expect(model._current_clip == resolved, "%s selects %s" % [DanceRoutines.name_of(index), canonical]) var previous := _pose(model.skeleton) var greatest_motion := 0.0 for _i in SAMPLE_FRAMES: model.update_state("idle", 0.0, false) await process_frame var current := _pose(model.skeleton) greatest_motion = maxf(greatest_motion, _pose_distance(previous, current)) previous = current _expect(greatest_motion >= MIN_FRAME_MOTION, "%s visibly animates the skeleton (%.4f rad/frame)" % [DanceRoutines.name_of(index), greatest_motion]) model.set_dancing(false) for _i in 12: model.update_state("idle", 0.0, false) await process_frame model.queue_free() _done() func _pose(skeleton: Skeleton3D) -> Array[Quaternion]: var result: Array[Quaternion] = [] result.resize(skeleton.get_bone_count()) for bone in skeleton.get_bone_count(): result[bone] = skeleton.get_bone_pose_rotation(bone) return result func _pose_distance(a: Array, b: Array) -> float: var count := mini(a.size(), b.size()) if count == 0: return 0.0 var greatest := 0.0 for index in count: var qa: Quaternion = a[index] var qb: Quaternion = b[index] greatest = maxf(greatest, qa.angle_to(qb)) return greatest func _expect(ok: bool, description: String) -> void: if ok: print(" OK: ", description) else: printerr(" FAIL: ", description) _failures += 1 func _done() -> void: print("\n=== AUTHORED EMOTE SUMMARY ===") print("Failures: %d" % _failures) quit(1 if _failures > 0 else 0)