Files
Papay-Shooter/debug/dance_check.gd
2026-08-02 02:20:02 -04:00

114 lines
3.3 KiB
GDScript

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)