extends Node3D @export var model_path: NodePath = NodePath("AshlingSwarmerModel") @export var default_animation: StringName = &"Idle" @export var autoplay: bool = true var _animation_player: AnimationPlayer var _model_node: Node3D var _current_animation: StringName = &"" func _ready() -> void: call_deferred("_setup_animation") func _setup_animation() -> void: _model_node = get_node_or_null(model_path) as Node3D _animation_player = _find_animation_player(_model_node) if _animation_player == null: if autoplay: push_warning("AshlingSwarmer: no AnimationPlayer found under %s." % model_path) return if autoplay: play_animation(default_animation) func play_animation(animation_name: StringName = &"") -> void: if _animation_player == null: _animation_player = _find_animation_player(get_node_or_null(model_path)) if _animation_player == null: return var requested_animation := animation_name if animation_name != StringName() else default_animation var resolved_animation := _resolve_animation_name(requested_animation) if resolved_animation == StringName(): push_warning("AshlingSwarmer: animation '%s' not found. Available animations: %s" % [requested_animation, _animation_player.get_animation_list()]) return var animation := _animation_player.get_animation(resolved_animation) if animation != null: animation.loop_mode = Animation.LOOP_LINEAR _animation_player.play(resolved_animation) _current_animation = _classify_animation(resolved_animation) func play_idle() -> void: play_animation(&"Idle") func play_run() -> void: play_animation(&"Run") func play_attack() -> void: play_animation(&"Attack_Leap") func play_hit() -> void: play_animation(&"Hit") func play_death() -> void: play_animation(&"Death_Explode") func _resolve_animation_name(animation_name: StringName) -> StringName: if _animation_player.has_animation(animation_name): return animation_name var desired := String(animation_name).to_lower() for candidate in _animation_player.get_animation_list(): var candidate_text := String(candidate) if candidate_text.to_lower() == desired: return StringName(candidate) for candidate in _animation_player.get_animation_list(): var candidate_text := String(candidate) if candidate_text.to_lower().contains(desired): return StringName(candidate) for candidate in _animation_player.get_animation_list(): var candidate_text := String(candidate) if candidate_text.to_lower().contains("idle"): return StringName(candidate) var animations := _animation_player.get_animation_list() if animations.size() > 0: return StringName(animations[0]) return StringName() func _classify_animation(animation_name: StringName) -> StringName: var text := String(animation_name).to_lower() if text.contains("run"): return &"Run" if text.contains("idle"): return &"Idle" if text.contains("attack"): return &"Attack_Leap" if text.contains("hit"): return &"Hit" if text.contains("death"): return &"Death_Explode" return animation_name func _find_animation_player(root: Node) -> AnimationPlayer: if root == null: return null if root is AnimationPlayer: return root for child in root.get_children(): var found := _find_animation_player(child) if found != null: return found return null