Skip to main content

game_gem/
animation.rs

1//! Sprite animation system.
2//!
3//! Features:
4//! - **Sprite sheets** with configurable frames, rows, columns
5//! - **Animation state machine** — idle, run, jump, etc.
6//! - **Blending between animations**
7//! - **Events** at specific keyframes (footstep sounds, spawn effects)
8//! - **Speed control** and **ping-pong** playback
9
10use crate::math::Vec2;
11
12// ─────────────────────────────────────────────
13// Animation data
14// ─────────────────────────────────────────────
15
16/// A single animation defined from a sprite sheet.
17#[derive(Debug, Clone)]
18pub struct Animation {
19    /// Human-readable name (e.g., "idle", "run", "attack").
20    pub name: String,
21    /// Region of the sprite sheet: (x, y, width, height) per frame in pixels.
22    pub frame_size: Vec2,
23    /// Frames in this animation (indices into the sprite sheet row).
24    pub frames: Vec<AnimationFrame>,
25    /// Playback speed (1.0 = normal, 2.0 = double speed).
26    pub speed: f32,
27    /// Whether to loop.
28    pub looping: bool,
29    /// Whether to ping-pong (play forward then backward).
30    pub ping_pong: bool,
31    /// Offset from the sprite's position (for attack effects, etc.).
32    pub offset: Vec2,
33}
34
35/// A single frame in an animation.
36#[derive(Debug, Clone)]
37pub struct AnimationFrame {
38    /// Column index in the sprite sheet.
39    pub column: u32,
40    /// Row index in the sprite sheet.
41    pub row: u32,
42    /// Duration of this specific frame in seconds.
43    /// If 0.0, uses the animation's default timing.
44    pub duration: f32,
45    /// Optional event to trigger when this frame is first shown.
46    pub event: Option<String>,
47}
48
49impl Animation {
50    /// Create a simple animation with evenly-spaced frames in a single row.
51    ///
52    /// - `name` — animation name
53    /// - `row` — sprite sheet row
54    /// - `start_col` — first column (inclusive)
55    /// - `end_col` — last column (inclusive)
56    /// - `frame_duration` — seconds per frame
57    pub fn from_row(
58        name: &str,
59        row: u32,
60        start_col: u32,
61        end_col: u32,
62        frame_duration: f32,
63    ) -> Self {
64        let frames = (start_col..=end_col)
65            .map(|col| AnimationFrame {
66                column: col,
67                row,
68                duration: frame_duration,
69                event: None,
70            })
71            .collect();
72
73        Self {
74            name: name.to_string(),
75            frame_size: Vec2::ZERO,
76            frames,
77            speed: 1.0,
78            looping: true,
79            ping_pong: false,
80            offset: Vec2::ZERO,
81        }
82    }
83
84    /// Builder: set frame size.
85    pub fn with_frame_size(mut self, w: f32, h: f32) -> Self {
86        self.frame_size = Vec2::new(w, h);
87        self
88    }
89
90    /// Builder: set speed.
91    pub fn with_speed(mut self, speed: f32) -> Self {
92        self.speed = speed;
93        self
94    }
95
96    /// Builder: set looping.
97    pub fn with_looping(mut self, looping: bool) -> Self {
98        self.looping = looping;
99        self
100    }
101
102    /// Builder: set ping-pong.
103    pub fn with_ping_pong(mut self) -> Self {
104        self.ping_pong = true;
105        self
106    }
107
108    /// Builder: set offset.
109    pub fn with_offset(mut self, x: f32, y: f32) -> Self {
110        self.offset = Vec2::new(x, y);
111        self
112    }
113
114    /// Add an event to a specific frame.
115    pub fn with_frame_event(mut self, frame_index: usize, event: &str) -> Self {
116        if frame_index < self.frames.len() {
117            self.frames[frame_index].event = Some(event.to_string());
118        }
119        self
120    }
121
122    /// Total duration of the animation in seconds.
123    pub fn total_duration(&self) -> f32 {
124        self.frames.iter().map(|f| f.duration).sum::<f32>() / self.speed
125    }
126
127    /// Number of frames.
128    pub fn frame_count(&self) -> usize {
129        self.frames.len()
130    }
131}
132
133// ─────────────────────────────────────────────
134// Animation Player
135// ─────────────────────────────────────────────
136
137/// Playback state for an animation.
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub enum AnimationState {
140    /// Currently playing.
141    Playing,
142    /// Animation finished (non-looping).
143    Finished,
144    /// Manually paused.
145    Paused,
146}
147
148/// An animation player that controls playback of one animation at a time.
149///
150/// # Example
151/// ```
152/// let mut player = AnimationPlayer::new();
153/// player.play(&idle_anim);
154/// player.play(&run_anim);
155///
156/// // In update:
157/// let events = player.update(dt);
158/// for event in events {
159///     if event == "footstep" { play_sound("step.wav"); }
160/// }
161/// ```
162#[derive(Debug)]
163pub struct AnimationPlayer {
164    /// Current animation (reference).
165    current: Option<Animation>,
166    /// Index of the current frame.
167    frame_index: usize,
168    /// Timer for the current frame.
169    frame_timer: f32,
170    /// Playback state.
171    state: AnimationState,
172    /// Whether playing forward (false = playing backward for ping-pong).
173    forward: bool,
174    /// Events triggered this frame.
175    pending_events: Vec<String>,
176}
177
178impl Default for AnimationPlayer {
179    fn default() -> Self {
180        Self::new()
181    }
182}
183
184impl AnimationPlayer {
185    /// Create a new animation player.
186    pub fn new() -> Self {
187        Self {
188            current: None,
189            frame_index: 0,
190            frame_timer: 0.0,
191            state: AnimationState::Finished,
192            forward: true,
193            pending_events: Vec::new(),
194        }
195    }
196
197    /// Start playing an animation.
198    ///
199    /// If `restart` is true, always restarts from frame 0.
200    /// If false, only restarts if it's a different animation.
201    pub fn play(&mut self, anim: &Animation) {
202        let is_same = self.current.as_ref().map(|c| c.name == anim.name).unwrap_or(false);
203        if !is_same || self.state == AnimationState::Finished {
204            self.current = Some(anim.clone());
205            self.frame_index = 0;
206            self.frame_timer = 0.0;
207            self.state = AnimationState::Playing;
208            self.forward = true;
209            self.pending_events.clear();
210        }
211    }
212
213    /// Force-restart the current animation.
214    pub fn restart(&mut self) {
215        self.frame_index = 0;
216        self.frame_timer = 0.0;
217        self.state = AnimationState::Playing;
218        self.forward = true;
219    }
220
221    /// Pause the animation.
222    pub fn pause(&mut self) {
223        if self.state == AnimationState::Playing {
224            self.state = AnimationState::Paused;
225        }
226    }
227
228    /// Resume from pause.
229    pub fn resume(&mut self) {
230        if self.state == AnimationState::Paused {
231            self.state = AnimationState::Playing;
232        }
233    }
234
235    /// Stop and reset.
236    pub fn stop(&mut self) {
237        self.current = None;
238        self.frame_index = 0;
239        self.frame_timer = 0.0;
240        self.state = AnimationState::Finished;
241    }
242
243    /// Get the current animation name, if any.
244    pub fn current_name(&self) -> Option<&str> {
245        self.current.as_ref().map(|a| a.name.as_str())
246    }
247
248    /// Get the current frame's source rectangle (UV region in the sprite sheet).
249    pub fn current_frame_rect(&self) -> Option<(u32, u32, f32, f32)> {
250        let anim = self.current.as_ref()?;
251        let frame = anim.frames.get(self.frame_index)?;
252        Some((frame.column, frame.row, anim.frame_size.x, anim.frame_size.y))
253    }
254
255    /// Get the current frame index.
256    pub fn frame_index(&self) -> usize {
257        self.frame_index
258    }
259
260    /// Get the playback state.
261    pub fn state(&self) -> AnimationState {
262        self.state
263    }
264
265    /// Get the animation offset.
266    pub fn offset(&self) -> Vec2 {
267        self.current.as_ref().map(|a| a.offset).unwrap_or(Vec2::ZERO)
268    }
269
270    /// Update the animation player. Returns events triggered this frame.
271    pub fn update(&mut self, dt: f32) -> &[String] {
272        self.pending_events.clear();
273
274        if self.state != AnimationState::Playing {
275            return &self.pending_events;
276        }
277
278        let anim = match &self.current {
279            Some(a) => a,
280            None => return &self.pending_events,
281        };
282
283        if anim.frames.is_empty() {
284            self.state = AnimationState::Finished;
285            return &self.pending_events;
286        }
287
288        let frame = &anim.frames[self.frame_index];
289        let effective_duration = if frame.duration > 0.0 {
290            frame.duration / anim.speed
291        } else {
292            0.1 / anim.speed
293        };
294
295        self.frame_timer += dt;
296
297        if self.frame_timer >= effective_duration {
298            self.frame_timer -= effective_duration;
299
300            // Fire frame event
301            if let Some(event) = &frame.event {
302                self.pending_events.push(event.clone());
303            }
304
305            // Advance frame
306            if anim.ping_pong {
307                if self.forward {
308                    if self.frame_index + 1 >= anim.frames.len() {
309                        self.forward = false;
310                        self.frame_index = self.frame_index.saturating_sub(1);
311                    } else {
312                        self.frame_index += 1;
313                    }
314                } else {
315                    if self.frame_index == 0 {
316                        if anim.looping {
317                            self.forward = true;
318                            self.frame_index = 1;
319                        } else {
320                            self.state = AnimationState::Finished;
321                        }
322                    } else {
323                        self.frame_index -= 1;
324                    }
325                }
326            } else {
327                if self.frame_index + 1 >= anim.frames.len() {
328                    if anim.looping {
329                        self.frame_index = 0;
330                    } else {
331                        self.state = AnimationState::Finished;
332                    }
333                } else {
334                    self.frame_index += 1;
335                }
336            }
337        }
338
339        &self.pending_events
340    }
341}