diff --git a/addons/gut/gui/GutBottomPanel.tscn b/addons/gut/gui/GutBottomPanel.tscn index f11677b..fc2be96 100644 --- a/addons/gut/gui/GutBottomPanel.tscn +++ b/addons/gut/gui/GutBottomPanel.tscn @@ -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"] diff --git a/assets/characters/animations/animation-pack.res b/assets/characters/animations/animation-pack.res index 6938d2f..8d344d2 100644 Binary files a/assets/characters/animations/animation-pack.res and b/assets/characters/animations/animation-pack.res differ diff --git a/assets/characters/animations/backflip_1.res b/assets/characters/animations/backflip_1.res index 0edbaf4..8e0458d 100644 Binary files a/assets/characters/animations/backflip_1.res and b/assets/characters/animations/backflip_1.res differ diff --git a/assets/characters/animations/drop_tile_1.res b/assets/characters/animations/drop_tile_1.res index 2d6c85b..144eb9c 100644 Binary files a/assets/characters/animations/drop_tile_1.res and b/assets/characters/animations/drop_tile_1.res differ diff --git a/assets/characters/animations/drop_tile_2.res b/assets/characters/animations/drop_tile_2.res index 7b4afe3..ac7b0fd 100644 Binary files a/assets/characters/animations/drop_tile_2.res and b/assets/characters/animations/drop_tile_2.res differ diff --git a/assets/characters/animations/getting_hit_1.res b/assets/characters/animations/getting_hit_1.res new file mode 100644 index 0000000..df129d3 Binary files /dev/null and b/assets/characters/animations/getting_hit_1.res differ diff --git a/assets/characters/animations/hit_1.res b/assets/characters/animations/hit_1.res new file mode 100644 index 0000000..c8ee031 Binary files /dev/null and b/assets/characters/animations/hit_1.res differ diff --git a/assets/characters/animations/hitted_1.res b/assets/characters/animations/hitted_1.res new file mode 100644 index 0000000..15ab6db Binary files /dev/null and b/assets/characters/animations/hitted_1.res differ diff --git a/assets/characters/animations/holding_1.res b/assets/characters/animations/holding_1.res new file mode 100644 index 0000000..45fc2b1 Binary files /dev/null and b/assets/characters/animations/holding_1.res differ diff --git a/assets/characters/animations/holding_walk.res b/assets/characters/animations/holding_walk.res new file mode 100644 index 0000000..1f635b9 Binary files /dev/null and b/assets/characters/animations/holding_walk.res differ diff --git a/assets/characters/animations/idle.res b/assets/characters/animations/idle.res index 1f15b44..57d17fd 100644 Binary files a/assets/characters/animations/idle.res and b/assets/characters/animations/idle.res differ diff --git a/assets/characters/animations/pickup_1.res b/assets/characters/animations/pickup_1.res new file mode 100644 index 0000000..d878906 Binary files /dev/null and b/assets/characters/animations/pickup_1.res differ diff --git a/assets/characters/animations/put_1.res b/assets/characters/animations/put_1.res new file mode 100644 index 0000000..caf3454 Binary files /dev/null and b/assets/characters/animations/put_1.res differ diff --git a/assets/characters/animations/spawn_tile_1.res b/assets/characters/animations/spawn_tile_1.res index c239ea1..6699fce 100644 Binary files a/assets/characters/animations/spawn_tile_1.res and b/assets/characters/animations/spawn_tile_1.res differ diff --git a/assets/characters/animations/stun_1.res b/assets/characters/animations/stun_1.res new file mode 100644 index 0000000..46b57e7 Binary files /dev/null and b/assets/characters/animations/stun_1.res differ diff --git a/assets/characters/animations/take_tile_1.res b/assets/characters/animations/take_tile_1.res index 73c202c..78ac442 100644 Binary files a/assets/characters/animations/take_tile_1.res and b/assets/characters/animations/take_tile_1.res differ diff --git a/assets/characters/animations/take_tile_2.res b/assets/characters/animations/take_tile_2.res index 41daee2..6a25149 100644 Binary files a/assets/characters/animations/take_tile_2.res and b/assets/characters/animations/take_tile_2.res differ diff --git a/assets/characters/animations/walk_forward.res b/assets/characters/animations/walk_forward.res index 1d4d73c..4394e02 100644 Binary files a/assets/characters/animations/walk_forward.res and b/assets/characters/animations/walk_forward.res differ diff --git a/scenes/arena/freemode.tscn b/scenes/arena/freemode.tscn index 88eba88..8f84618 100644 --- a/scenes/arena/freemode.tscn +++ b/scenes/arena/freemode.tscn @@ -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) diff --git a/scenes/player.gd b/scenes/player.gd index e5d4d5f..473928d 100644 --- a/scenes/player.gd +++ b/scenes/player.gd @@ -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 diff --git a/scripts/static_tekton_controller.gd b/scripts/static_tekton_controller.gd index 5718202..e924b33 100644 --- a/scripts/static_tekton_controller.gd +++ b/scripts/static_tekton_controller.gd @@ -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() diff --git a/scripts/tekton.gd b/scripts/tekton.gd index e521af7..428bfd3 100644 --- a/scripts/tekton.gd +++ b/scripts/tekton.gd @@ -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) diff --git a/scripts/tekton_fishing_autoplay.gd b/scripts/tekton_fishing_autoplay.gd new file mode 100644 index 0000000..5abb802 --- /dev/null +++ b/scripts/tekton_fishing_autoplay.gd @@ -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 diff --git a/scripts/tekton_fishing_autoplay.gd.uid b/scripts/tekton_fishing_autoplay.gd.uid new file mode 100644 index 0000000..337403f --- /dev/null +++ b/scripts/tekton_fishing_autoplay.gd.uid @@ -0,0 +1 @@ +uid://dcnuf06soevte diff --git a/tools/build_animation_pack.gd b/tools/build_animation_pack.gd index 66dd4ab..e975626 100644 --- a/tools/build_animation_pack.gd +++ b/tools/build_animation_pack.gd @@ -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 --script tools/build_animation_pack.gd +## Usage: +## godot --headless --path --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