freemode fishing tekton autoplay + static tekton throw fix

- Added tekton_fishing_autoplay.gd: plays both GLB animations simultaneously, looped
- Attached autoplay script to 3 fishing tekton instances in freemode.tscn
- Fixed static tekton throw: faces throw direction + plays throw animation
- Fixed AnimationPlayer path in tekton.gd for static turrets
- Fixed animation names (tekton_throw_tile -> ted_bones_001|Tekton Throwing Tiles|Anima_Layer)
- Fixed static_tekton_controller.gd idle resume
- Rebuilt animation-pack.res with new animations (holding_1, put_1, stun_1, etc.)
- Fixed GutBottomPanel.tscn broken UID
This commit is contained in:
2026-06-15 18:15:39 +08:00
parent 844ec194cb
commit 5354d8b30f
25 changed files with 242 additions and 156 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
[ext_resource type="Script" uid="uid://dtvnb0xatk0my" path="res://addons/gut/gui/GutBottomPanel.gd" id="1"] [ext_resource type="Script" uid="uid://dtvnb0xatk0my" path="res://addons/gut/gui/GutBottomPanel.gd" id="1"]
[ext_resource type="PackedScene" uid="uid://0yunjxtaa8iw" path="res://addons/gut/gui/RunAtCursor.tscn" id="3"] [ext_resource type="PackedScene" uid="uid://0yunjxtaa8iw" path="res://addons/gut/gui/RunAtCursor.tscn" id="3"]
[ext_resource type="Texture2D" uid="uid://cr6tvdv0ve6cv" path="res://addons/gut/gui/play.png" id="4"] [ext_resource type="Texture2D" path="res://addons/gut/gui/play.png" id="4"]
[ext_resource type="Texture2D" uid="uid://bvo0uao7deu0q" path="res://addons/gut/icon.png" id="4_xv2r3"] [ext_resource type="Texture2D" uid="uid://bvo0uao7deu0q" path="res://addons/gut/icon.png" id="4_xv2r3"]
[ext_resource type="PackedScene" uid="uid://4gyyn12um08h" path="res://addons/gut/gui/RunResults.tscn" id="5"] [ext_resource type="PackedScene" uid="uid://4gyyn12um08h" path="res://addons/gut/gui/RunResults.tscn" id="5"]
[ext_resource type="Texture2D" uid="uid://ljc2viafngwd" path="res://addons/gut/images/HSplitContainer.svg" id="5_qdqpf"] [ext_resource type="Texture2D" uid="uid://ljc2viafngwd" path="res://addons/gut/images/HSplitContainer.svg" id="5_qdqpf"]
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+14
View File
@@ -3,6 +3,8 @@
[ext_resource type="PackedScene" uid="uid://b1l0x4yf3lbx8" path="res://assets/models/arena/free_mode/Terrainv2.gltf" id="1_37t6b"] [ext_resource type="PackedScene" uid="uid://b1l0x4yf3lbx8" path="res://assets/models/arena/free_mode/Terrainv2.gltf" id="1_37t6b"]
[ext_resource type="Material" uid="uid://xifgjdr8285d" path="res://assets/models/arena/free_mode/water_shader.tres" id="2_bw67x"] [ext_resource type="Material" uid="uid://xifgjdr8285d" path="res://assets/models/arena/free_mode/water_shader.tres" id="2_bw67x"]
[ext_resource type="Texture2D" uid="uid://dep1ng3aqb2jw" path="res://assets/models/arena/free_mode/sky_sea_01.png" id="3_8esiu"] [ext_resource type="Texture2D" uid="uid://dep1ng3aqb2jw" path="res://assets/models/arena/free_mode/sky_sea_01.png" id="3_8esiu"]
[ext_resource type="PackedScene" uid="uid://cuporokvsp4ml" path="res://assets/characters/tektons/tekton_fishing_animation.glb" id="4_xj13a"]
[ext_resource type="Script" path="res://scripts/tekton_fishing_autoplay.gd" id="5_fish"]
[sub_resource type="PlaneMesh" id="PlaneMesh_8esiu"] [sub_resource type="PlaneMesh" id="PlaneMesh_8esiu"]
material = ExtResource("2_bw67x") material = ExtResource("2_bw67x")
@@ -39,3 +41,15 @@ shadow_normal_bias = 5.0
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=936572897] [node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=936572897]
environment = SubResource("Environment_8v6kw") environment = SubResource("Environment_8v6kw")
[node name="tekton_fishing_animation1" parent="." unique_id=1803834250 instance=ExtResource("4_xj13a")]
script = ExtResource("5_fish")
transform = Transform3D(-0.0012733412, 0, 0.0015422717, 0, 0.002, 0, -0.0015422717, 0, -0.0012733412, 0.04044947, 0.02995101, -0.059924163)
[node name="tekton_fishing_animation2" parent="." unique_id=376449997 instance=ExtResource("4_xj13a")]
script = ExtResource("5_fish")
transform = Transform3D(-0.0017457078, 0, 0.0009759627, 0, 0.0019999999, 0, -0.0009759627, 0, -0.0017457078, -0.11821492, 0.006670659, 0.040420167)
[node name="tekton_fishing_animation3" parent="." unique_id=1696737351 instance=ExtResource("4_xj13a")]
script = ExtResource("5_fish")
transform = Transform3D(-0.0012249037, 0, 0.0015810153, 0, 0.002, 0, -0.0015810153, 0, -0.0012249037, 0.035826474, 0.008033583, 0.0059019327)
+27 -8
View File
@@ -418,6 +418,7 @@ func _init_managers():
movement_manager.name = "MovementManager" movement_manager.name = "MovementManager"
add_child(movement_manager) add_child(movement_manager)
movement_manager.initialize(self , enhanced_gridmap) movement_manager.initialize(self , enhanced_gridmap)
movement_manager.movement_finished.connect(_on_movement_finished)
race_manager = load("res://scripts/managers/player_race_manager.gd").new() race_manager = load("res://scripts/managers/player_race_manager.gd").new()
race_manager.name = "RaceManager" race_manager.name = "RaceManager"
@@ -646,8 +647,10 @@ const ANIMATION_SPEED: float = 2.0
func play_walk_animation() -> void: func play_walk_animation() -> void:
"""Play walking animation at increased speed.""" """Play walking animation at increased speed."""
if is_carrying_tekton and anim_player and anim_player.has_animation("animation-pack/holding_1"): if is_carrying_tekton:
return # Let dasher_hold keep playing if anim_player and anim_player.has_animation("animation-pack/holding_walk"):
anim_player.play("animation-pack/holding_walk", -1, ANIMATION_SPEED)
return
if anim_player and anim_player.has_animation("animation-pack/walk_forward"): if anim_player and anim_player.has_animation("animation-pack/walk_forward"):
anim_player.play("animation-pack/walk_forward", -1, ANIMATION_SPEED) anim_player.play("animation-pack/walk_forward", -1, ANIMATION_SPEED)
@@ -668,11 +671,16 @@ func play_special_animation() -> void:
func play_idle_animation() -> void: func play_idle_animation() -> void:
"""Play idle animation at normal speed.""" """Play idle animation at normal speed."""
if is_carrying_tekton and anim_player and anim_player.has_animation("animation-pack/holding_1"): if is_carrying_tekton:
return # Let dasher_hold keep playing if anim_player and anim_player.has_animation("animation-pack/holding_1"):
anim_player.play("animation-pack/holding_1")
return
if anim_player and anim_player.has_animation("animation-pack/idle"): if anim_player and anim_player.has_animation("animation-pack/idle"):
anim_player.play("animation-pack/idle") anim_player.play("animation-pack/idle")
func _on_movement_finished() -> void:
play_idle_animation()
# ============================================================================= # =============================================================================
# Network-Synced Animation Functions # Network-Synced Animation Functions
# ============================================================================= # =============================================================================
@@ -2378,6 +2386,8 @@ func grab_tekton():
if tekton: if tekton:
if is_multiplayer_authority() and can_rpc(): if is_multiplayer_authority() and can_rpc():
rpc("sync_grab_tekton", tekton.get_path()) rpc("sync_grab_tekton", tekton.get_path())
elif is_multiplayer_authority():
sync_grab_tekton(tekton.get_path())
func snatch_tekton(target_carrier: Node3D): func snatch_tekton(target_carrier: Node3D):
if not is_multiplayer_authority() or not target_carrier.is_carrying_tekton: if not is_multiplayer_authority() or not target_carrier.is_carrying_tekton:
@@ -2387,6 +2397,8 @@ func snatch_tekton(target_carrier: Node3D):
if tekton: if tekton:
if is_multiplayer_authority() and can_rpc(): if is_multiplayer_authority() and can_rpc():
rpc("sync_snatch_tekton", target_carrier.get_path(), tekton.get_path()) rpc("sync_snatch_tekton", target_carrier.get_path(), tekton.get_path())
elif is_multiplayer_authority():
sync_snatch_tekton(target_carrier.get_path(), tekton.get_path())
@rpc("any_peer", "call_local", "reliable") @rpc("any_peer", "call_local", "reliable")
func sync_snatch_tekton(carrier_path: NodePath, tekton_path: NodePath): func sync_snatch_tekton(carrier_path: NodePath, tekton_path: NodePath):
@@ -2488,14 +2500,21 @@ func sync_throw_tekton(target_pos: Vector2i):
else: else:
tekton.set_carried(false) tekton.set_carried(false)
# Visual Arc Tween # Face tekton toward throw direction
var start_pos = tekton.global_position
# Target world position
var end_world_pos = Vector3( var end_world_pos = Vector3(
target_pos.x * cell_size.x + cell_size.x * 0.5, target_pos.x * cell_size.x + cell_size.x * 0.5,
cell_size.y, # Floor Y cell_size.y,
target_pos.y * cell_size.z + cell_size.z * 0.5 target_pos.y * cell_size.z + cell_size.z * 0.5
) + cell_offset ) + cell_offset
tekton.look_at(Vector3(end_world_pos.x, tekton.global_position.y, end_world_pos.z), Vector3.UP)
# Play throw animation on the tekton
if tekton.has_method("play_animation"):
tekton.play_animation("ted_bones_001|Tekton Throwing Tiles|Anima_Layer")
# Visual Arc Tween
var start_pos = tekton.global_position
# end_world_pos already computed above for facing
var mid_pos = (start_pos + end_world_pos) / 2.0 var mid_pos = (start_pos + end_world_pos) / 2.0
mid_pos.y += 4.0 # Arc height mid_pos.y += 4.0 # Arc height
+9 -5
View File
@@ -20,8 +20,9 @@ func _ready():
enhanced_gridmap = main.get_node_or_null("EnhancedGridMap") enhanced_gridmap = main.get_node_or_null("EnhancedGridMap")
# Initial State # Initial State
if tekton.has_method("play_animation"): # No idle anim for static tekton mesh, just ensure stopped
tekton.play_animation("tekton_idle") var ap = tekton.get_node_or_null("Visuals/tekton/AnimationPlayer")
if ap: ap.stop()
# Setup Timer # Setup Timer
timer = Timer.new() timer = Timer.new()
@@ -69,7 +70,7 @@ func _attempt_throw():
# 2. Play Animation # 2. Play Animation
if tekton.has_method("play_animation_rpc") and tekton.has_method("can_rpc") and tekton.can_rpc(): if tekton.has_method("play_animation_rpc") and tekton.has_method("can_rpc") and tekton.can_rpc():
tekton.rpc("play_animation_rpc", "tekton_throw_tile") tekton.rpc("play_animation_rpc", "ted_bones_001|Tekton Throwing Tiles|Anima_Layer")
# 3. Create Projectile Visual (Synced) # 3. Create Projectile Visual (Synced)
if tekton.has_method("spawn_projectile_rpc") and tekton.has_method("can_rpc") and tekton.can_rpc(): if tekton.has_method("spawn_projectile_rpc") and tekton.has_method("can_rpc") and tekton.can_rpc():
@@ -86,8 +87,11 @@ func _attempt_throw():
# 5. Resume Idle # 5. Resume Idle
await get_tree().create_timer(0.5).timeout # Small delay after throw await get_tree().create_timer(0.5).timeout # Small delay after throw
if tekton.has_method("play_animation_rpc") and tekton.has_method("can_rpc") and tekton.can_rpc(): # No idle anim for static tekton mesh, just stop
tekton.rpc("play_animation_rpc", "tekton_idle") if tekton.has_method("can_rpc") and tekton.can_rpc():
# Stop animation on all peers via a direct call (AnimationPlayer.stop is visual-only, safe)
var ap = tekton.get_node_or_null("Visuals/tekton/AnimationPlayer")
if ap: ap.stop()
_start_timer() _start_timer()
+7 -4
View File
@@ -439,10 +439,12 @@ func spawn_tiles_around(count: int = 4):
play_animation("tekton_idle") play_animation("tekton_idle")
) )
elif is_static_turret and not is_carried and not is_thrown: elif is_static_turret and not is_carried and not is_thrown:
play_animation("Armature|tekton_throw_tile") play_animation("ted_bones_001|Tekton Throwing Tiles|Anima_Layer")
get_tree().create_timer(1.0).timeout.connect(func(): get_tree().create_timer(1.0).timeout.connect(func():
if not is_moving and not is_carried and not is_thrown: if not is_moving and not is_carried and not is_thrown:
play_animation("Armature|tekton_idle") # No idle anim in static_tekton_mesh, just stop
var ap = get_node_or_null("Visuals/tekton/AnimationPlayer")
if ap: ap.stop()
) )
print("[Tekton] Spawning %d tiles around %s" % [count, current_position]) print("[Tekton] Spawning %d tiles around %s" % [count, current_position])
@@ -559,9 +561,10 @@ func play_animation(anim_name: String):
var anim_player = get_node_or_null("Visuals/tekton/Armature/AnimationPlayer") var anim_player = get_node_or_null("Visuals/tekton/Armature/AnimationPlayer")
if is_static_turret: if is_static_turret:
anim_player = get_node_or_null("Visuals/static_tekton_mesh/tekton_throwing_tiles/AnimationPlayer") # static_tekton_mesh.tscn has AnimationPlayer at root level
anim_player = get_node_or_null("Visuals/tekton/AnimationPlayer")
if not anim_player: if not anim_player:
anim_player = get_node_or_null("Visuals/tekton_throwing_tiles/AnimationPlayer") # Check direct child just in case anim_player = get_node_or_null("Visuals/AnimationPlayer")
# If not found, try finding recursive # If not found, try finding recursive
if not anim_player: if not anim_player:
+25
View File
@@ -0,0 +1,25 @@
extends Node3D
## Autoplays all animations in the AnimationPlayer simultaneously and loops them.
## Attach to any node that has an AnimationPlayer child (e.g. tekton_fishing_animation instances).
@onready var anim_player: AnimationPlayer = $AnimationPlayer
func _ready() -> void:
if not anim_player:
push_warning("tekton_fishing_autoplay: No AnimationPlayer found")
return
# Wait one frame for the scene tree to settle
await get_tree().process_frame
# Play every animation simultaneously, all looping
for anim_name in anim_player.get_animation_list():
anim_player.play(anim_name)
anim_player.advance(0.0) # ensure it starts
# Set all to loop
for anim_name in anim_player.get_animation_list():
var anim: Animation = anim_player.get_animation(anim_name)
if anim:
anim.loop_mode = Animation.LOOP_LINEAR
+1
View File
@@ -0,0 +1 @@
uid://dcnuf06soevte
+153 -133
View File
@@ -1,106 +1,47 @@
extends SceneTree extends SceneTree
## Build animation-pack.res from animation-0.glb (original Blender bone names).
## Headless script: loads animation-0.glb, extracts animations with
## original Blender bone names, applies rest-pose correction per bone,
## and saves animation-pack.res.
## ##
## Usage: godot --headless --path <project> --script tools/build_animation_pack.gd ## Usage:
## godot --headless --path <project> --script tools/build_animation_pack.gd
##
## No rest-pose correction — animations are copied verbatim from the source GLB.
## All characters share the same rig structure (identical 25 bones), so the same
## animation works on all of them.
const SOURCE_GLB := "res://assets/characters/animation-0.glb" const SOURCE_GLB := "res://assets/characters/animation-0.glb"
const TARGET_SKELETON_GLB := "res://assets/characters/Bob.glb"
const OUTPUT_PATH := "res://assets/characters/animations/animation-pack.res" const OUTPUT_PATH := "res://assets/characters/animations/animation-pack.res"
var _corrections: Dictionary = {} # bone_name -> Quaternion correction
var _correction_src_rest: Dictionary = {} # "Skeleton3D:bone" -> Vector3
var _correction_tgt_rest: Dictionary = {} # "Skeleton3D:bone" -> Vector3
func _init() -> void: func _init() -> void:
print("[build_animation_pack] Starting...") print("[build_animation_pack] Starting build...")
# Step 1: Load source skeleton (animation-0.glb) # Step 1: Load source animations
var gltf_doc := GLTFDocument.new() var src_doc := GLTFDocument.new()
var gltf_state := GLTFState.new() var src_state := GLTFState.new()
var err := gltf_doc.append_from_file( var err := src_doc.append_from_file(
ProjectSettings.globalize_path(SOURCE_GLB), gltf_state ProjectSettings.globalize_path(SOURCE_GLB), src_state
) )
if err != OK: if err != OK:
push_error("[build_animation_pack] Failed to load source GLB: error %d" % err) push_error("[build_animation_pack] Failed to load source GLB: error %d" % err)
quit(1) quit(1)
return return
var src_scene := gltf_doc.generate_scene(gltf_state) var src_scene: Node = src_doc.generate_scene(src_state)
if not src_scene: if not src_scene:
push_error("[build_animation_pack] source generate_scene returned null") push_error("[build_animation_pack] Failed to generate source scene")
quit(1) quit(1)
return return
var src_skel: Skeleton3D = src_scene.find_child("Skeleton3D", true, false) # Step 2: Extract animations
if not src_skel:
push_error("[build_animation_pack] No Skeleton3D in source")
src_scene.queue_free()
quit(1)
return
# Step 2: Load target skeleton (Bob, raw)
var tgt_gltf_doc := GLTFDocument.new()
var tgt_gltf_state := GLTFState.new()
err = tgt_gltf_doc.append_from_file(
ProjectSettings.globalize_path(TARGET_SKELETON_GLB), tgt_gltf_state
)
if err != OK:
push_error("[build_animation_pack] Failed to load target GLB: error %d" % err)
src_scene.queue_free()
quit(1)
return
var tgt_scene: Node = tgt_gltf_doc.generate_scene(tgt_gltf_state)
if not tgt_scene:
push_error("[build_animation_pack] target generate_scene returned null")
src_scene.queue_free()
quit(1)
return
var tgt_skel: Skeleton3D = tgt_scene.find_child("Skeleton3D", true, false)
if not tgt_skel:
push_error("[build_animation_pack] No Skeleton3D in target")
src_scene.queue_free()
tgt_scene.queue_free()
quit(1)
return
# Step 3: Build per-bone correction quaternions (original bone names, 1:1)
var src_bone_index: Dictionary = {}
for i in src_skel.get_bone_count():
src_bone_index[src_skel.get_bone_name(i)] = i
for i in tgt_skel.get_bone_count():
var bone_name: String = tgt_skel.get_bone_name(i)
if not src_bone_index.has(bone_name):
continue
var src_idx: int = src_bone_index[bone_name]
var src_rest: Transform3D = src_skel.get_bone_rest(src_idx)
var tgt_rest: Transform3D = tgt_skel.get_bone_rest(i)
# correction = tgt_rest.rot * src_rest.rot.inverse()
var correction: Quaternion = Quaternion(tgt_rest.basis) * Quaternion(src_rest.basis.inverse())
_corrections[bone_name] = correction
_correction_src_rest["Character/Skeleton3D:%s" % bone_name] = src_rest.origin
_correction_tgt_rest["Character/Skeleton3D:%s" % bone_name] = tgt_rest.origin
print("[build_animation_pack] Built %d rest-pose corrections" % _corrections.size())
# Step 4: Extract animations with original bone names
# We use the generated scene's AnimationPlayer, which contains the
# already-stripped tracks (immutable ones removed by GLTF importer).
var anim_player: AnimationPlayer = src_scene.find_child("AnimationPlayer", true, false) var anim_player: AnimationPlayer = src_scene.find_child("AnimationPlayer", true, false)
if not anim_player: if not anim_player:
push_error("[build_animation_pack] No AnimationPlayer in source") push_error("[build_animation_pack] No AnimationPlayer in source")
src_scene.queue_free() src_scene.queue_free()
tgt_scene.queue_free()
quit(1) quit(1)
return return
var out_lib := AnimationLibrary.new() var out_lib := AnimationLibrary.new()
var count := 0 var count := 0
var total_tracks := 0 var total_tracks := 0
var corrected_keys := 0
for lib_name in anim_player.get_animation_library_list(): for lib_name in anim_player.get_animation_library_list():
var src_lib := anim_player.get_animation_library(lib_name) var src_lib := anim_player.get_animation_library(lib_name)
@@ -110,87 +51,166 @@ func _init() -> void:
continue continue
var new_anim := Animation.new() var new_anim := Animation.new()
new_anim.length = src_anim.length
new_anim.loop_mode = src_anim.loop_mode new_anim.loop_mode = src_anim.loop_mode
# holding_1 must loop while player carries tekton
if anim_name == "holding_1":
new_anim.loop_mode = Animation.LOOP_LINEAR
var max_time := 0.0
for i in src_anim.get_track_count(): for i in src_anim.get_track_count():
var path_str: String = str(src_anim.track_get_path(i)) var path_str: String = str(src_anim.track_get_path(i))
var track_type: int = src_anim.track_get_type(i) var track_type: int = src_anim.track_get_type(i)
# Parse "retarget/Skeleton3D:bone_name" -> bone_name # Skip the GLB root "retarget" node
var bone_name := "" if "retarget" in path_str and ":" not in path_str:
if ":" in path_str:
bone_name = path_str.split(":")[1]
else:
# Non-bone track: skip GLB root "retarget"
if "retarget" in path_str:
continue
_copy_track(src_anim, i, new_anim, path_str, Quaternion.IDENTITY, track_type)
continue continue
# Skip the GLB scene root "retarget" — not a real bone. # Convert track path: "retarget/Skeleton3D:bone" -> "Character/Skeleton3D:bone"
if bone_name == "retarget": if "retarget/Skeleton3D:" in path_str:
continue path_str = path_str.replace("retarget/Skeleton3D:", "Character/Skeleton3D:")
# Use original bone name with character-rig-relative path. var track_last := _copy_track(src_anim, i, new_anim, path_str, track_type)
# All characters have rig root renamed to "Character" via GLB modification, if track_last > max_time:
# so the path "Character/Skeleton3D:bone" is consistent across all of them. max_time = track_last
var new_path := "Character/Skeleton3D:%s" % bone_name
var correction: Quaternion = _corrections.get(bone_name, Quaternion())
_copy_track(src_anim, i, new_anim, new_path, correction, track_type)
total_tracks += 1 total_tracks += 1
corrected_keys += _last_key_count
new_anim.length = max_time
if out_lib.has_animation(anim_name):
push_warning("[build_animation_pack] Duplicate: %s" % anim_name)
continue
out_lib.add_animation(anim_name, new_anim) out_lib.add_animation(anim_name, new_anim)
count += 1 count += 1
print(" + %s (%d tracks)" % [anim_name, new_anim.get_track_count()]) print(" [%d] %s (%d tracks, %.3fs)" % [count, anim_name, new_anim.get_track_count(), max_time])
print("[build_animation_pack] Extracted %d animations, %d tracks, corrected %d rotation keys" % [count, total_tracks, corrected_keys]) # Step 4: Build holding_walk (walk legs + holding arms)
if out_lib.has_animation("walk_forward") and out_lib.has_animation("holding_1"):
var holding_walk := _build_holding_walk(
out_lib.get_animation("walk_forward"),
out_lib.get_animation("holding_1")
)
out_lib.add_animation("holding_walk", holding_walk)
var hw_path := "res://assets/characters/animations/holding_walk.res"
var err_hw := ResourceSaver.save(holding_walk, ProjectSettings.globalize_path(hw_path))
if err_hw != OK:
push_warning("[build_animation_pack] Failed to save holding_walk: %d" % err_hw)
else:
print(" [+] holding_walk (%d tracks, %.3fs, looped)" % [holding_walk.get_track_count(), holding_walk.length])
err = ResourceSaver.save(out_lib, OUTPUT_PATH) # Step 5: Save animation-pack.res
if err != OK: var err_save := ResourceSaver.save(out_lib, ProjectSettings.globalize_path(OUTPUT_PATH))
push_error("[build_animation_pack] Failed to save: error %d" % err) if err_save != OK:
push_error("[build_animation_pack] Failed to save: error %d" % err_save)
src_scene.queue_free() src_scene.queue_free()
tgt_scene.queue_free()
quit(1) quit(1)
return return
print("[build_animation_pack] Saved to %s" % OUTPUT_PATH) # Step 4: Save individual clip .res files alongside animation-pack.res
for anim_name in out_lib.get_animation_list():
var clip_path := "res://assets/characters/animations/%s.res" % anim_name
var clip_res: Animation = out_lib.get_animation(anim_name)
var err_clip := ResourceSaver.save(clip_res, ProjectSettings.globalize_path(clip_path))
if err_clip != OK:
push_warning("[build_animation_pack] Failed to save clip %s: %d" % [anim_name, err_clip])
# Step 5: Build holding_walk (walk legs + holding arms)
if out_lib.has_animation("walk_forward") and out_lib.has_animation("holding_1"):
var holding_walk := _build_holding_walk(
out_lib.get_animation("walk_forward"),
out_lib.get_animation("holding_1")
)
out_lib.add_animation("holding_walk", holding_walk)
var hw_path := "res://assets/characters/animations/holding_walk.res"
var err_hw := ResourceSaver.save(holding_walk, ProjectSettings.globalize_path(hw_path))
if err_hw != OK:
push_warning("[build_animation_pack] Failed to save holding_walk: %d" % err_hw)
else:
print(" [+] holding_walk (%d tracks, %.3fs, looped)" % [holding_walk.get_track_count(), holding_walk.length])
src_scene.queue_free() src_scene.queue_free()
tgt_scene.queue_free() print("[build_animation_pack] Extracted %d animations, %d tracks" % [count, total_tracks])
print("[build_animation_pack] Saved to %s + %d clips" % [OUTPUT_PATH, count])
quit(0) quit(0)
var _last_key_count: int = 0
func _copy_track(src_anim: Animation, src_idx: int, dst_anim: Animation, new_path: String, correction: Quaternion, track_type: int = -1) -> void: func _copy_track(src: Animation, src_idx: int, dst: Animation, new_path: String, track_type: int) -> float:
if track_type < 0: var track_idx := dst.add_track(track_type)
track_type = src_anim.track_get_type(src_idx) dst.track_set_path(track_idx, new_path)
var dst_idx := dst_anim.add_track(track_type)
dst_anim.track_set_path(dst_idx, NodePath(new_path))
dst_anim.track_set_interpolation_type(dst_idx, src_anim.track_get_interpolation_type(src_idx))
var key_count := src_anim.track_get_key_count(src_idx) var key_count := src.track_get_key_count(src_idx)
_last_key_count = 0 var last_time := 0.0
for k in key_count: for k in key_count:
var time := src_anim.track_get_key_time(src_idx, k) var t := src.track_get_key_time(src_idx, k)
var value = src_anim.track_get_key_value(src_idx, k) dst.track_insert_key(track_idx, t, src.track_get_key_value(src_idx, k))
var transition := src_anim.track_get_key_transition(src_idx, k) if t > last_time:
last_time = t
return last_time
match track_type:
Animation.TYPE_POSITION_3D: func _build_holding_walk(walk: Animation, hold: Animation) -> Animation:
var src_pos: Vector3 = value """Merge walk_forward (full body) with holding_1 (upper body override).
var src_rest_pos: Vector3 = _correction_src_rest.get(new_path, Vector3.ZERO) Lower body bones (spine, thighs, legs) come from walk_forward.
var tgt_rest_pos: Vector3 = _correction_tgt_rest.get(new_path, Vector3.ZERO) Upper body bones (spine.001, head, shoulders, arms) come from holding_1.
var corrected_pos: Vector3 = correction * (src_pos - src_rest_pos) + tgt_rest_pos """
dst_anim.position_track_insert_key(dst_idx, time, corrected_pos) # Upper body bone prefixes — holding_1 overrides these
Animation.TYPE_ROTATION_3D: var upper_bones := [
var src_q: Quaternion = value "spine.001", "head",
var corrected_q: Quaternion = correction * src_q "shoulder.L", "upper_arm.L", "forearm.L", "hand.L",
dst_anim.rotation_track_insert_key(dst_idx, time, corrected_q) "shoulder.R", "upper_arm.R", "forearm.R", "hand.R",
_last_key_count += 1 ]
Animation.TYPE_SCALE_3D: var result := Animation.new()
dst_anim.scale_track_insert_key(dst_idx, time, value) result.loop_mode = Animation.LOOP_LINEAR
Animation.TYPE_BLEND_SHAPE: result.length = walk.length
dst_anim.blend_shape_track_insert_key(dst_idx, time, value)
_: # Collect all track bone names from both animations
dst_anim.track_insert_key(dst_idx, time, value, transition) var walk_tracks: Dictionary = {} # bone_name -> [track_idx, ...]
for i in walk.get_track_count():
var path := str(walk.track_get_path(i))
if "Skeleton3D:" in path:
var bone := path.split(":")[1]
if not walk_tracks.has(bone):
walk_tracks[bone] = []
walk_tracks[bone].append(i)
var hold_tracks: Dictionary = {}
for i in hold.get_track_count():
var path := str(hold.track_get_path(i))
if "Skeleton3D:" in path:
var bone := path.split(":")[1]
if not hold_tracks.has(bone):
hold_tracks[bone] = []
hold_tracks[bone].append(i)
# Add all walk_forward tracks as base
for bone in walk_tracks:
for idx in walk_tracks[bone]:
var path := str(walk.track_get_path(idx))
var track_type := walk.track_get_type(idx)
_copy_track(walk, idx, result, path, track_type)
# Override upper body tracks with holding_1 values
for bone in upper_bones:
if hold_tracks.has(bone) and walk_tracks.has(bone):
# Replace matching walk tracks with hold values
var h_indices: Array = hold_tracks[bone]
var w_indices: Array = walk_tracks[bone]
for hi in h_indices:
var hold_path := str(hold.track_get_path(hi))
var hold_type := hold.track_get_type(hi)
# Find matching walk track (same type)
for wi in w_indices:
if walk.track_get_type(wi) == hold_type:
# Remove the walk track from result, add hold track
var result_path := str(result.track_get_path(wi))
# Find this track in result and remove it
for ri in result.get_track_count():
if str(result.track_get_path(ri)) == result_path and result.track_get_type(ri) == hold_type:
result.remove_track(ri)
break
# Copy hold track
_copy_track(hold, hi, result, hold_path, hold_type)
w_indices.erase(wi)
break
return result