97bb53f6cd
Work-Package: M02-RND-CHARACTER-PRESENTATION-001 Agent: sindo-main-codex Tests: presentation/input/movement/terrain/camera regressions; coordinate/streaming/docs/coordination gates; renderer dry-run 7/7 Fidelity: preserves current sandbox model/outfit and Stand/Idle/Run/Walk behavior; build-12340 parity remains unverified
96 lines
3.3 KiB
GDScript
96 lines
3.3 KiB
GDScript
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
|