Implement rig animation controls in the test harness

- Added a new animation specification document for Phase 9 Task 5 detailing the requirements for rig animation controls.
- Introduced a new `StickmanRig` script to manage the facing direction and joint bending for the rig.
- Implemented UI elements in the test harness for selecting animations, controlling playback (play/pause/resume/stop), and toggling loop mode.
- Enhanced the `test_harness.gd` script to handle animation playback state and UI interactions.
- Updated documentation in `AGENTS.md`, `README.md`, and `RIGGING.md` to reflect the new animation features.
This commit is contained in:
2026-08-24 23:41:23 -04:00
parent 273090993e
commit 07bef66703
12 changed files with 1695 additions and 283 deletions
+42 -39
View File
@@ -1,60 +1,63 @@
@tool
extends Node2D
extends EditorScript
@export var build_walk_animation: bool = false:
set(value):
if value:
_create_walk_right_animation()
func _create_walk_right_animation() -> void:
var anim_player: AnimationPlayer = get_node_or_null("AnimationPlayer")
if not anim_player:
push_error("AnimationPlayer node not found as direct child of Master!")
func _run() -> void:
var root = EditorInterface.get_edited_scene_root()
if not root:
push_error("EditorScript: No active scene open.")
return
var anim_player = root.get_node_or_null("AnimationPlayer") as AnimationPlayer
if not anim_player:
push_error("EditorScript: AnimationPlayer node not found under root.")
return
_generate_walk_animation(anim_player, "walk_right", 1, false) # FacingProfile.RIGHT (1)
_generate_walk_animation(anim_player, "walk_left", 0, true) # FacingProfile.LEFT (0)
func _generate_walk_animation(anim_player: AnimationPlayer, anim_name: String, profile_enum: int, flip_x: bool) -> void:
var anim = Animation.new()
anim.length = 0.8
anim.loop_mode = Animation.LOOP_LINEAR
var dir_mult: float = -1.0 if flip_x else 1.0
var times = [0.0, 0.2, 0.4, 0.6, 0.8]
# Keyframe tracks mapped relative to root Master node
var tracks = {
"IK_Targets/Torso:position": [
Vector2(0, 10), Vector2(0, -15), Vector2(0, 10), Vector2(0, -15), Vector2(0, 10)
],
"IK_Targets/Head:position": [
Vector2(100, -614), Vector2(100, -639), Vector2(100, -614), Vector2(100, -639), Vector2(100, -614)
],
"IK_Targets/Right_Leg:position": [
Vector2(110, 390), Vector2(0, 397), Vector2(-110, 380), Vector2(-20, 320), Vector2(110, 390)
],
"IK_Targets/Left_Leg:position": [
Vector2(-110, 380), Vector2(-20, 320), Vector2(110, 390), Vector2(0, 397), Vector2(-110, 380)
],
"IK_Targets/Right_Hand:position": [
Vector2(-90, 110), Vector2(0, 115), Vector2(90, 110), Vector2(0, 115), Vector2(-90, 110)
],
"IK_Targets/Left_Hand:position": [
Vector2(90, 110), Vector2(0, 115), Vector2(-90, 110), Vector2(0, 115), Vector2(-90, 110)
]
# 1. Profile Track (0 = LEFT, 1 = RIGHT)
var profile_track = anim.add_track(Animation.TYPE_VALUE)
anim.track_set_path(profile_track, ".:facing_profile")
anim.value_track_set_update_mode(profile_track, Animation.UPDATE_DISCRETE)
anim.track_insert_key(profile_track, 0.0, profile_enum)
# 2. Keyframe positions
var raw_tracks = {
"IK_Targets/Torso:position": [Vector2(0, 10), Vector2(0, -15), Vector2(0, 10), Vector2(0, -15), Vector2(0, 10)],
"IK_Targets/Head:position": [Vector2(100, -614), Vector2(100, -639), Vector2(100, -614), Vector2(100, -639), Vector2(100, -614)],
"IK_Targets/Right_Leg:position": [Vector2(110, 390), Vector2(0, 397), Vector2(-110, 380), Vector2(-20, 320), Vector2(110, 390)],
"IK_Targets/Left_Leg:position": [Vector2(-110, 380), Vector2(-20, 320), Vector2(110, 390), Vector2(0, 397), Vector2(-110, 380)],
"IK_Targets/Right_Hand:position": [Vector2(-90, 110), Vector2(0, 115), Vector2(90, 110), Vector2(0, 115), Vector2(-90, 110)],
"IK_Targets/Left_Hand:position": [Vector2(90, 110), Vector2(0, 115), Vector2(-90, 110), Vector2(0, 115), Vector2(90, 110)]
}
for path in tracks:
var times = [0.0, 0.2, 0.4, 0.6, 0.8]
for path in raw_tracks:
var track_idx = anim.add_track(Animation.TYPE_VALUE)
anim.track_set_path(track_idx, path)
anim.track_set_interpolation_type(track_idx, Animation.INTERPOLATION_CUBIC)
for i in range(times.size()):
anim.track_insert_key(track_idx, times[i], tracks[path][i])
var orig_pos: Vector2 = raw_tracks[path][i]
var mirrored_pos = Vector2(orig_pos.x * dir_mult, orig_pos.y)
anim.track_insert_key(track_idx, times[i], mirrored_pos)
# Add or retrieve default library
# 3. Attach animation to AnimationPlayer library
var lib = anim_player.get_animation_library("")
if not lib:
lib = AnimationLibrary.new()
anim_player.add_animation_library("", lib)
if lib.has_animation("walk_right"):
lib.remove_animation("walk_right")
if lib.has_animation(anim_name):
lib.remove_animation(anim_name)
lib.add_animation("walk_right", anim)
print("Successfully generated 'walk_right' animation in AnimationPlayer!")
lib.add_animation(anim_name, anim)
print("Successfully generated '%s' animation!" % anim_name)