@tool
class_name PoseExtractor extends Node
## Tool script to extract poses from tumble.glb animations into a PoseLibrary.
## Attach to a scene containing the tumble model, configure extraction_map,
## then press "Extract Poses" in the inspector.
##
## Usage:
## 1. Add this node as a child of the scene root
## 2. Set target_skeleton to the Skeleton3D inside TumbleModel
## 3. Set target_anim_player to the AnimationPlayer inside TumbleModel
## 4. Click "Extract Poses" button
## 5. PoseLibrary is saved to output_path
@export var target_skeleton: Skeleton3D
@export var target_anim_player: AnimationPlayer
@export var output_path: String = "res://scenes/player/procedural/tumble_poses.tres"
@export var extract_now: bool = false: set = _on_extract
## Map of pose_name -> { anim: "animation_name", time: seconds_or_normalized }
## You will fill this in based on what animations tumble.glb has.
@export var extraction_map: Dictionary = {
# These are starting guesses — you'll adjust frame numbers after inspecting
# the actual tumble.glb animations in the Godot editor.
#
# Format: "pose_name": { "anim": "anim_name", "time": 0.0 }
# time can be absolute seconds or use "frame" key for frame number
}
func _on_extract(value: bool) -> void:
if not value:
return
extract_now = false
_do_extract()
func _do_extract() -> void:
if not target_skeleton:
push_error("[PoseExtractor] No target_skeleton set")
return
if not target_anim_player:
push_error("[PoseExtractor] No target_anim_player set")
return
print("[PoseExtractor] Starting extraction...")
print("[PoseExtractor] Skeleton: ", target_skeleton.get_path())
print("[PoseExtractor] AnimPlayer: ", target_anim_player.get_path())
# List available animations
var anims: PackedStringArray = target_anim_player.get_animation_list()
print("[PoseExtractor] Available animations: ", anims)
if extraction_map.is_empty():
print("[PoseExtractor] extraction_map is empty. Listing animations with durations:")
for anim_name in anims:
var anim: Animation = target_anim_player.get_animation(anim_name)
if anim:
print(" '", anim_name, "': length=", anim.length, "s, tracks=", anim.get_track_count())
print("[PoseExtractor] Fill in extraction_map and try again.")
return
var lib := PoseLibrary.new()
for pose_name: String in extraction_map:
var entry: Dictionary = extraction_map[pose_name]
var anim_name: String = entry.get("anim", "")
var seek_time: float = entry.get("time", 0.0)
if not target_anim_player.has_animation(anim_name):
push_warning("[PoseExtractor] Animation '", anim_name, "' not found, skipping pose '", pose_name, "'")
continue
# Seek to the target time
target_anim_player.play(anim_name)
target_anim_player.seek(seek_time, true)
# Force skeleton update
target_anim_player.advance(0.0)
# Capture all bone poses
var pose: Dictionary = {}
for bone_idx in target_skeleton.get_bone_count():
var bone_name: String = target_skeleton.get_bone_name(bone_idx)
var pos: Vector3 = target_skeleton.get_bone_pose_position(bone_idx)
var rot: Quaternion = target_skeleton.get_bone_pose_rotation(bone_idx)
var scl: Vector3 = target_skeleton.get_bone_pose_scale(bone_idx)
pose[bone_name] = Transform3D(Basis(rot).scaled(scl), pos)
lib.poses[pose_name] = pose
print("[PoseExtractor] Captured pose '", pose_name, "' from '", anim_name, "' at t=", seek_time, " (", pose.size(), " bones)")
# Reset animation
target_anim_player.stop()
# Save
var err: int = ResourceSaver.save(lib, output_path)
if err == OK:
print("[PoseExtractor] Saved PoseLibrary to ", output_path, " with ", lib.poses.size(), " poses")
else:
push_error("[PoseExtractor] Failed to save: error ", err)
## Utility: list all bones in the skeleton (for debugging)
func list_bones() -> void:
if not target_skeleton:
push_error("[PoseExtractor] No skeleton")
return
print("[PoseExtractor] Bones (", target_skeleton.get_bone_count(), "):")
for i in target_skeleton.get_bone_count():
var parent: int = target_skeleton.get_bone_parent(i)
var parent_name: String = target_skeleton.get_bone_name(parent) if parent >= 0 else "(root)"
print(" [", i, "] ", target_skeleton.get_bone_name(i), " <- ", parent_name)