use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnimNode {
pub name: String,
pub clip_index: usize,
pub speed: f32,
pub looping: bool,
}
impl AnimNode {
#[must_use]
pub fn new(name: impl Into<String>, clip_index: usize) -> Self {
Self {
name: name.into(),
clip_index,
speed: 1.0,
looping: true,
}
}
#[must_use]
pub fn once(name: impl Into<String>, clip_index: usize) -> Self {
Self {
name: name.into(),
clip_index,
speed: 1.0,
looping: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnimTransition {
pub from: usize,
pub to: usize,
pub duration: f32,
pub trigger: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AnimState {
pub nodes: Vec<AnimNode>,
pub transitions: Vec<AnimTransition>,
pub current: usize,
pub time: f32,
blend_target: Option<usize>,
blend_alpha: f32,
blend_duration: f32,
params: Vec<(String, bool)>,
}
impl AnimState {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn add_node(&mut self, node: AnimNode) -> usize {
let idx = self.nodes.len();
self.nodes.push(node);
idx
}
pub fn add_transition(
&mut self,
from: usize,
to: usize,
duration: f32,
trigger: impl Into<String>,
) {
self.transitions.push(AnimTransition {
from,
to,
duration,
trigger: trigger.into(),
});
}
pub fn set_param(&mut self, name: &str, value: bool) {
if let Some(p) = self.params.iter_mut().find(|(n, _)| n == name) {
p.1 = value;
} else {
self.params.push((name.to_string(), value));
}
}
#[must_use]
pub fn get_param(&self, name: &str) -> bool {
self.params
.iter()
.find(|(n, _)| n == name)
.map(|(_, v)| *v)
.unwrap_or(false)
}
#[must_use]
pub fn tick(&mut self, dt: f32) -> AnimPlayback {
if self.blend_target.is_none() {
for t in &self.transitions {
if t.from == self.current && self.get_param(&t.trigger) {
self.blend_target = Some(t.to);
self.blend_alpha = 0.0;
self.blend_duration = t.duration;
break;
}
}
}
if let Some(target) = self.blend_target {
if self.blend_duration <= 0.0 {
self.current = target;
self.blend_target = None;
self.blend_alpha = 0.0;
self.time = 0.0;
self.reset_trigger_for(target);
} else {
self.blend_alpha += dt / self.blend_duration;
if self.blend_alpha >= 1.0 {
self.current = target;
self.blend_target = None;
self.blend_alpha = 0.0;
self.time = 0.0;
self.reset_trigger_for(target);
}
}
}
if let Some(node) = self.nodes.get(self.current) {
self.time += dt * node.speed;
}
AnimPlayback {
clip_index: self
.nodes
.get(self.current)
.map(|n| n.clip_index)
.unwrap_or(0),
time: self.time,
blend: self.blend_target.map(|t| {
let clip = self.nodes.get(t).map(|n| n.clip_index).unwrap_or(0);
(clip, self.blend_alpha)
}),
}
}
#[must_use]
pub fn current_name(&self) -> &str {
self.nodes
.get(self.current)
.map(|n| n.name.as_str())
.unwrap_or("")
}
#[must_use]
#[inline]
pub fn is_blending(&self) -> bool {
self.blend_target.is_some()
}
#[must_use]
#[inline]
pub fn node_count(&self) -> usize {
self.nodes.len()
}
fn reset_trigger_for(&mut self, target: usize) {
if let Some(trigger) = self
.transitions
.iter()
.find(|t| t.to == target)
.map(|t| t.trigger.clone())
{
self.set_param(&trigger, false);
}
}
}
#[derive(Debug, Clone)]
pub struct AnimPlayback {
pub clip_index: usize,
pub time: f32,
pub blend: Option<(usize, f32)>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn anim_state_basic() {
let mut state = AnimState::new();
let idle = state.add_node(AnimNode::new("idle", 0));
let walk = state.add_node(AnimNode::new("walk", 1));
assert_eq!(idle, 0);
assert_eq!(walk, 1);
assert_eq!(state.node_count(), 2);
assert_eq!(state.current_name(), "idle");
}
#[test]
fn anim_state_tick_advances_time() {
let mut state = AnimState::new();
state.add_node(AnimNode::new("idle", 0));
let playback = state.tick(0.5);
assert!((playback.time - 0.5).abs() < 0.01);
assert_eq!(playback.clip_index, 0);
assert!(playback.blend.is_none());
}
#[test]
fn anim_state_transition() {
let mut state = AnimState::new();
state.add_node(AnimNode::new("idle", 0));
state.add_node(AnimNode::new("walk", 1));
state.add_transition(0, 1, 0.5, "walk");
let _ = state.tick(0.1);
assert_eq!(state.current_name(), "idle");
assert!(!state.is_blending());
state.set_param("walk", true);
let playback = state.tick(0.1);
assert!(state.is_blending());
assert!(playback.blend.is_some());
let (blend_clip, alpha) = playback.blend.unwrap();
assert_eq!(blend_clip, 1);
assert!(alpha > 0.0);
}
#[test]
fn anim_state_transition_completes() {
let mut state = AnimState::new();
state.add_node(AnimNode::new("idle", 0));
state.add_node(AnimNode::new("walk", 1));
state.add_transition(0, 1, 0.2, "go");
state.set_param("go", true);
let _ = state.tick(0.1);
assert!(state.is_blending());
let _ = state.tick(0.15); assert!(!state.is_blending());
assert_eq!(state.current_name(), "walk");
}
#[test]
fn anim_state_instant_transition() {
let mut state = AnimState::new();
state.add_node(AnimNode::new("idle", 0));
state.add_node(AnimNode::new("run", 1));
state.add_transition(0, 1, 0.0, "run");
state.set_param("run", true);
let _ = state.tick(0.01);
assert_eq!(state.current_name(), "run");
assert!(!state.is_blending());
}
#[test]
fn anim_state_speed_multiplier() {
let mut state = AnimState::new();
let mut node = AnimNode::new("fast", 0);
node.speed = 2.0;
state.add_node(node);
let playback = state.tick(0.5);
assert!((playback.time - 1.0).abs() < 0.01);
}
#[test]
fn anim_state_params() {
let mut state = AnimState::new();
assert!(!state.get_param("jump"));
state.set_param("jump", true);
assert!(state.get_param("jump"));
state.set_param("jump", false);
assert!(!state.get_param("jump"));
}
#[test]
fn anim_state_trigger_resets_after_transition() {
let mut state = AnimState::new();
state.add_node(AnimNode::new("idle", 0));
state.add_node(AnimNode::new("walk", 1));
state.add_transition(0, 1, 0.1, "go");
state.set_param("go", true);
let _ = state.tick(0.2); assert!(!state.get_param("go"));
}
#[test]
fn anim_state_serde_roundtrip() {
let mut state = AnimState::new();
state.add_node(AnimNode::new("idle", 0));
state.add_node(AnimNode::new("walk", 1));
state.add_transition(0, 1, 0.3, "go");
let json = serde_json::to_string(&state).unwrap();
let decoded: AnimState = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.node_count(), 2);
assert_eq!(decoded.transitions.len(), 1);
}
#[test]
fn anim_node_once() {
let node = AnimNode::once("attack", 3);
assert!(!node.looping);
assert_eq!(node.clip_index, 3);
}
#[test]
fn anim_state_no_nodes() {
let mut state = AnimState::new();
let playback = state.tick(0.1);
assert_eq!(playback.clip_index, 0);
assert_eq!(state.current_name(), "");
}
#[test]
fn anim_state_chain_transitions() {
let mut state = AnimState::new();
state.add_node(AnimNode::new("idle", 0));
state.add_node(AnimNode::new("walk", 1));
state.add_node(AnimNode::new("run", 2));
state.add_transition(0, 1, 0.1, "walk");
state.add_transition(1, 2, 0.1, "run");
state.set_param("walk", true);
let _ = state.tick(0.2);
assert_eq!(state.current_name(), "walk");
state.set_param("run", true);
let _ = state.tick(0.2);
assert_eq!(state.current_name(), "run");
}
}