class_name CharacterAnimationPresenter extends Node ## Selects and plays the current sandbox idle or moving animation. ## Animation import, movement simulation and model composition remain external. const MOVING_ANIMATION_CANDIDATES: Array[String] = ["Run", "Walk"] const STATIONARY_ANIMATION_CANDIDATES: Array[String] = ["Stand", "Idle"] ## Cross-fade time retained from the current sandbox controller. @export var animation_blend_time_seconds: float = 0.15 var active_animation_name: String: get: return _active_animation_name var has_animation_player: bool: get: return _animation_player != null and is_instance_valid(_animation_player) var _animation_player: AnimationPlayer var _active_animation_name := "" ## Finds the first AnimationPlayer below a newly composed character root. ## Binding null or a root without animations clears the current presentation. func bind_character_root(character_root: Node) -> bool: _animation_player = ( _find_animation_player(character_root) if character_root != null and is_instance_valid(character_root) else null ) _active_animation_name = "" if _animation_player == null: return false _prepare_animation_player(_animation_player) present_locomotion(false) return true ## Presents the current moving/stationary locomotion state. ## Returns true only when a different animation starts playing. func present_locomotion(is_moving: bool) -> bool: if _animation_player == null or not is_instance_valid(_animation_player): _animation_player = null _active_animation_name = "" return false var candidates := MOVING_ANIMATION_CANDIDATES if is_moving else STATIONARY_ANIMATION_CANDIDATES var selected_animation := _choose_animation(candidates) if selected_animation.is_empty() or selected_animation == _active_animation_name: return false _active_animation_name = selected_animation _animation_player.play(selected_animation, animation_blend_time_seconds) return true func _prepare_animation_player(animation_player: AnimationPlayer) -> void: animation_player.playback_default_blend_time = animation_blend_time_seconds for animation_name in animation_player.get_animation_list(): var lowercase_name := String(animation_name).to_lower() if ( lowercase_name == "stand" or lowercase_name == "idle" or lowercase_name == "run" or lowercase_name == "walk" or lowercase_name.contains("stand") or lowercase_name.contains("run") or lowercase_name.contains("walk") ): var animation := animation_player.get_animation(animation_name) if animation != null: animation.loop_mode = Animation.LOOP_LINEAR func _choose_animation(candidates: Array[String]) -> String: for candidate in candidates: if _animation_player.has_animation(candidate): return candidate var animation_names := _animation_player.get_animation_list() for candidate in candidates: var lowercase_candidate := candidate.to_lower() for animation_name in animation_names: if String(animation_name).to_lower().contains(lowercase_candidate): return String(animation_name) return "" func _find_animation_player(root: Node) -> AnimationPlayer: if root is AnimationPlayer: return root for child in root.get_children(): var animation_player := _find_animation_player(child) if animation_player != null: return animation_player return null