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="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="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"]
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="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="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"]
material = ExtResource("2_bw67x")
@@ -39,3 +41,15 @@ shadow_normal_bias = 5.0
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=936572897]
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)
+30 -11
View File
@@ -418,6 +418,7 @@ func _init_managers():
movement_manager.name = "MovementManager"
add_child(movement_manager)
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.name = "RaceManager"
@@ -646,8 +647,10 @@ const ANIMATION_SPEED: float = 2.0
func play_walk_animation() -> void:
"""Play walking animation at increased speed."""
if is_carrying_tekton and anim_player and anim_player.has_animation("animation-pack/holding_1"):
return # Let dasher_hold keep playing
if is_carrying_tekton:
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"):
anim_player.play("animation-pack/walk_forward", -1, ANIMATION_SPEED)
@@ -668,11 +671,16 @@ func play_special_animation() -> void:
func play_idle_animation() -> void:
"""Play idle animation at normal speed."""
if is_carrying_tekton and anim_player and anim_player.has_animation("animation-pack/holding_1"):
return # Let dasher_hold keep playing
if is_carrying_tekton:
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"):
anim_player.play("animation-pack/idle")
func _on_movement_finished() -> void:
play_idle_animation()
# =============================================================================
# Network-Synced Animation Functions
# =============================================================================
@@ -2378,6 +2386,8 @@ func grab_tekton():
if tekton:
if is_multiplayer_authority() and can_rpc():
rpc("sync_grab_tekton", tekton.get_path())
elif is_multiplayer_authority():
sync_grab_tekton(tekton.get_path())
func snatch_tekton(target_carrier: Node3D):
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 is_multiplayer_authority() and can_rpc():
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")
func sync_snatch_tekton(carrier_path: NodePath, tekton_path: NodePath):
@@ -2487,16 +2499,23 @@ func sync_throw_tekton(target_pos: Vector2i):
tekton.set_thrown(true)
else:
tekton.set_carried(false)
# Face tekton toward throw direction
var end_world_pos = Vector3(
target_pos.x * cell_size.x + cell_size.x * 0.5,
cell_size.y,
target_pos.y * cell_size.z + cell_size.z * 0.5
) + 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
# Target world position
var end_world_pos = Vector3(
target_pos.x * cell_size.x + cell_size.x * 0.5,
cell_size.y, # Floor Y
target_pos.y * cell_size.z + cell_size.z * 0.5
) + cell_offset
# end_world_pos already computed above for facing
var mid_pos = (start_pos + end_world_pos) / 2.0
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")
# Initial State
if tekton.has_method("play_animation"):
tekton.play_animation("tekton_idle")
# No idle anim for static tekton mesh, just ensure stopped
var ap = tekton.get_node_or_null("Visuals/tekton/AnimationPlayer")
if ap: ap.stop()
# Setup Timer
timer = Timer.new()
@@ -69,7 +70,7 @@ func _attempt_throw():
# 2. Play Animation
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)
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
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():
tekton.rpc("play_animation_rpc", "tekton_idle")
# No idle anim for static tekton mesh, just stop
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()
+9 -6
View File
@@ -439,10 +439,12 @@ func spawn_tiles_around(count: int = 4):
play_animation("tekton_idle")
)
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():
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])
@@ -557,12 +559,13 @@ func spawn_projectile_rpc(target_pos: Vector3, duration: float):
func play_animation(anim_name: String):
# Try specific user path first
var anim_player = get_node_or_null("Visuals/tekton/Armature/AnimationPlayer")
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:
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 anim_player:
anim_player = find_child("AnimationPlayer", true, false)
+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
## Headless script: loads animation-0.glb, extracts animations with
## original Blender bone names, applies rest-pose correction per bone,
## and saves animation-pack.res.
## Build animation-pack.res from animation-0.glb (original Blender bone names).
##
## 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 TARGET_SKELETON_GLB := "res://assets/characters/Bob.glb"
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:
print("[build_animation_pack] Starting...")
print("[build_animation_pack] Starting build...")
# Step 1: Load source skeleton (animation-0.glb)
var gltf_doc := GLTFDocument.new()
var gltf_state := GLTFState.new()
var err := gltf_doc.append_from_file(
ProjectSettings.globalize_path(SOURCE_GLB), gltf_state
# Step 1: Load source animations
var src_doc := GLTFDocument.new()
var src_state := GLTFState.new()
var err := src_doc.append_from_file(
ProjectSettings.globalize_path(SOURCE_GLB), src_state
)
if err != OK:
push_error("[build_animation_pack] Failed to load source GLB: error %d" % err)
quit(1)
return
var src_scene := gltf_doc.generate_scene(gltf_state)
var src_scene: Node = src_doc.generate_scene(src_state)
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)
return
var src_skel: Skeleton3D = src_scene.find_child("Skeleton3D", true, false)
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).
# Step 2: Extract animations
var anim_player: AnimationPlayer = src_scene.find_child("AnimationPlayer", true, false)
if not anim_player:
push_error("[build_animation_pack] No AnimationPlayer in source")
src_scene.queue_free()
tgt_scene.queue_free()
quit(1)
return
var out_lib := AnimationLibrary.new()
var count := 0
var total_tracks := 0
var corrected_keys := 0
for lib_name in anim_player.get_animation_library_list():
var src_lib := anim_player.get_animation_library(lib_name)
@@ -110,87 +51,166 @@ func _init() -> void:
continue
var new_anim := Animation.new()
new_anim.length = src_anim.length
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():
var path_str: String = str(src_anim.track_get_path(i))
var track_type: int = src_anim.track_get_type(i)
# Parse "retarget/Skeleton3D:bone_name" -> bone_name
var bone_name := ""
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)
# Skip the GLB root "retarget" node
if "retarget" in path_str and ":" not in path_str:
continue
# Skip the GLB scene root "retarget" — not a real bone.
if bone_name == "retarget":
continue
# Convert track path: "retarget/Skeleton3D:bone" -> "Character/Skeleton3D:bone"
if "retarget/Skeleton3D:" in path_str:
path_str = path_str.replace("retarget/Skeleton3D:", "Character/Skeleton3D:")
# Use original bone name with character-rig-relative path.
# All characters have rig root renamed to "Character" via GLB modification,
# so the path "Character/Skeleton3D:bone" is consistent across all of them.
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)
var track_last := _copy_track(src_anim, i, new_anim, path_str, track_type)
if track_last > max_time:
max_time = track_last
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)
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)
if err != OK:
push_error("[build_animation_pack] Failed to save: error %d" % err)
# Step 5: Save animation-pack.res
var err_save := ResourceSaver.save(out_lib, ProjectSettings.globalize_path(OUTPUT_PATH))
if err_save != OK:
push_error("[build_animation_pack] Failed to save: error %d" % err_save)
src_scene.queue_free()
tgt_scene.queue_free()
quit(1)
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()
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)
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:
if track_type < 0:
track_type = src_anim.track_get_type(src_idx)
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))
func _copy_track(src: Animation, src_idx: int, dst: Animation, new_path: String, track_type: int) -> float:
var track_idx := dst.add_track(track_type)
dst.track_set_path(track_idx, new_path)
var key_count := src_anim.track_get_key_count(src_idx)
_last_key_count = 0
var key_count := src.track_get_key_count(src_idx)
var last_time := 0.0
for k in key_count:
var time := src_anim.track_get_key_time(src_idx, k)
var value = src_anim.track_get_key_value(src_idx, k)
var transition := src_anim.track_get_key_transition(src_idx, k)
var t := src.track_get_key_time(src_idx, k)
dst.track_insert_key(track_idx, t, src.track_get_key_value(src_idx, k))
if t > last_time:
last_time = t
return last_time
match track_type:
Animation.TYPE_POSITION_3D:
var src_pos: Vector3 = value
var src_rest_pos: Vector3 = _correction_src_rest.get(new_path, Vector3.ZERO)
var tgt_rest_pos: Vector3 = _correction_tgt_rest.get(new_path, Vector3.ZERO)
var corrected_pos: Vector3 = correction * (src_pos - src_rest_pos) + tgt_rest_pos
dst_anim.position_track_insert_key(dst_idx, time, corrected_pos)
Animation.TYPE_ROTATION_3D:
var src_q: Quaternion = value
var corrected_q: Quaternion = correction * src_q
dst_anim.rotation_track_insert_key(dst_idx, time, corrected_q)
_last_key_count += 1
Animation.TYPE_SCALE_3D:
dst_anim.scale_track_insert_key(dst_idx, time, value)
Animation.TYPE_BLEND_SHAPE:
dst_anim.blend_shape_track_insert_key(dst_idx, time, value)
_:
dst_anim.track_insert_key(dst_idx, time, value, transition)
func _build_holding_walk(walk: Animation, hold: Animation) -> Animation:
"""Merge walk_forward (full body) with holding_1 (upper body override).
Lower body bones (spine, thighs, legs) come from walk_forward.
Upper body bones (spine.001, head, shoulders, arms) come from holding_1.
"""
# Upper body bone prefixes — holding_1 overrides these
var upper_bones := [
"spine.001", "head",
"shoulder.L", "upper_arm.L", "forearm.L", "hand.L",
"shoulder.R", "upper_arm.R", "forearm.R", "hand.R",
]
var result := Animation.new()
result.loop_mode = Animation.LOOP_LINEAR
result.length = walk.length
# Collect all track bone names from both animations
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