Skip to main content

engine/
animation.rs

1/**--------------------------------------------------------------------------------
2*!  Engine-level, asset-agnostic animation runtime.
3*?  This module contains the generic animation definition (`AnimationDef`) and
4*?  the runtime/state machine (`AnimationState`) that drive frame timing and
5*?  animation switching. It is deliberately asset-agnostic so the `game` crate
6*?  can keep its `AssetKey` enum and map frames->textures locally.
7*--------------------------------------------------------------------------------**/
8#[derive(Clone, Debug)]
9pub struct AnimationDef {
10    pub name: String,
11    pub start_frame: usize,
12    pub frame_count: usize,
13    pub frame_duration: f32,
14    pub looping: bool,
15}
16
17impl AnimationDef {
18    pub fn new(
19        name: impl Into<String>,
20        start_frame: usize,
21        frame_count: usize,
22        frame_duration: f32,
23        looping: bool,
24    ) -> Self {
25        Self {
26            name: name.into(),
27            start_frame,
28            frame_count,
29            frame_duration,
30            looping,
31        }
32    }
33}
34
35//? Animation state machine. Does not know about textures or asset keys.
36pub struct AnimationState {
37    pub current_anim: String,
38    pub frame_index: usize,
39    pub timer: f32,
40    animations: Vec<AnimationDef>,
41    current_index: usize,
42}
43
44//? Create a new runtime state from a list of `AnimationDef`s and a default name.
45impl AnimationState {
46    pub fn new(animations: Vec<AnimationDef>, default_anim: &str) -> Self {
47        let current_index = animations
48            .iter()
49            .position(|a| a.name == default_anim)
50            .unwrap_or(0);
51        let current_anim = animations
52            .get(current_index)
53            .map(|a| a.name.clone())
54            .unwrap_or_else(|| default_anim.to_string());
55        Self {
56            current_anim,
57            frame_index: 0,
58            timer: 0.0,
59            animations,
60            current_index,
61        }
62    }
63
64    //? Advance the timer and update the current frame index accordingly.
65    pub fn update(&mut self, dt: f32) {
66        let anim = match self.animations.get(self.current_index) {
67            Some(a) => a,
68            None => return,
69        };
70
71        self.timer += dt;
72
73        if self.timer >= anim.frame_duration {
74            self.timer -= anim.frame_duration;
75            self.frame_index += 1;
76
77            if self.frame_index >= anim.frame_count {
78                if anim.looping {
79                    self.frame_index = 0;
80                } else {
81                    self.frame_index = anim.frame_count.saturating_sub(1);
82                }
83            }
84        }
85    }
86
87    //? Return the current animation definition and the active frame index.
88    pub fn current(&self) -> Option<(&AnimationDef, usize)> {
89        let anim = self.animations.get(self.current_index)?;
90        Some((anim, self.frame_index))
91    }
92
93    //? Switch to a different animation by name (resets frame/timer).
94    pub fn play(&mut self, anim_name: &str) {
95        if self.current_anim != anim_name
96            && let Some(idx) = self.animations.iter().position(|a| a.name == anim_name)
97        {
98            self.current_index = idx;
99            self.current_anim = anim_name.to_string();
100            self.frame_index = 0;
101            self.timer = 0.0;
102        }
103    }
104
105    //? Returns true if the current anim is non-looping and has reached its last frame.
106    pub fn is_finished(&self) -> bool {
107        let anim = match self.animations.get(self.current_index) {
108            Some(a) => a,
109            None => return true,
110        };
111
112        !anim.looping && self.frame_index >= anim.frame_count.saturating_sub(1)
113    }
114
115    //? Name of the currently playing animation.
116    pub fn current_animation_name(&self) -> Option<&str> {
117        Some(&self.current_anim)
118    }
119
120    //? Progress through the current animation as a value in [0.0, 1.0].
121    pub fn get_progress(&self) -> f32 {
122        if let Some(anim) = self.animations.get(self.current_index) {
123            if anim.frame_count == 0 {
124                return 0.0;
125            }
126            let total_frames = anim.frame_count;
127            let progress_per_frame = 1.0 / total_frames as f32;
128            let frame_progress = self.timer / anim.frame_duration;
129            (self.frame_index as f32 + frame_progress) * progress_per_frame
130        } else {
131            0.0
132        }
133    }
134}