Skip to main content

nightshade/ecs/
timer.rs

1//! Plain-data time accumulators a game owns and ticks itself.
2//!
3//! A [`Timer`] counts toward a fixed duration and reports when it elapses, once
4//! or on a repeating cycle. A [`Stopwatch`] counts up without end. Neither holds
5//! engine state: store one in a component or a resource, advance it with the
6//! frame's delta, and read its progress. A repeating timer carries the overshoot
7//! into the next cycle and reports how many cycles a single tick completed, so a
8//! long frame neither drifts the schedule nor silently drops events. Both derive
9//! serde, so they round-trip through a save file mid-countdown.
10
11use serde::{Deserialize, Serialize};
12
13/// Whether a [`Timer`] stops after it elapses or restarts for another cycle.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
15pub enum TimerMode {
16    /// Elapses once and then stays finished until [`Timer::reset`].
17    #[default]
18    Once,
19    /// Restarts each time it elapses, carrying the remainder into the next cycle.
20    Repeating,
21}
22
23/// A countdown toward a duration. Tick it with the frame delta and read
24/// [`Timer::just_finished`] for the cycle it completes, [`Timer::fraction`] for
25/// progress to drive a bar or a fade, or [`Timer::remaining`] for a clock.
26#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
27pub struct Timer {
28    duration: f32,
29    elapsed: f32,
30    mode: TimerMode,
31    paused: bool,
32    finished: bool,
33    times_finished_this_tick: u32,
34}
35
36impl Timer {
37    /// A timer of `duration` seconds with the given mode.
38    pub fn new(duration: f32, mode: TimerMode) -> Self {
39        Self {
40            duration: duration.max(0.0),
41            mode,
42            ..Default::default()
43        }
44    }
45
46    /// A one-shot timer of `duration` seconds.
47    pub fn once(duration: f32) -> Self {
48        Self::new(duration, TimerMode::Once)
49    }
50
51    /// A repeating timer that elapses every `duration` seconds.
52    pub fn repeating(duration: f32) -> Self {
53        Self::new(duration, TimerMode::Repeating)
54    }
55
56    /// Advances the timer by `delta` seconds, returning it for chaining. A
57    /// repeating timer wraps and counts every cycle the delta crossed; a
58    /// finished one-shot timer ignores further ticks until reset; a paused timer
59    /// ignores the delta entirely.
60    pub fn tick(&mut self, delta: f32) -> &mut Self {
61        if self.paused {
62            self.times_finished_this_tick = 0;
63            return self;
64        }
65
66        if self.mode == TimerMode::Once && self.finished {
67            self.times_finished_this_tick = 0;
68            return self;
69        }
70
71        self.elapsed += delta;
72
73        if self.duration <= 0.0 {
74            self.finished = true;
75            self.times_finished_this_tick = 1;
76            self.elapsed = 0.0;
77            return self;
78        }
79
80        if self.elapsed >= self.duration {
81            self.finished = true;
82            match self.mode {
83                TimerMode::Repeating => {
84                    self.times_finished_this_tick = (self.elapsed / self.duration) as u32;
85                    self.elapsed %= self.duration;
86                }
87                TimerMode::Once => {
88                    self.times_finished_this_tick = 1;
89                    self.elapsed = self.duration;
90                }
91            }
92        } else {
93            self.finished = false;
94            self.times_finished_this_tick = 0;
95        }
96
97        self
98    }
99
100    /// Whether the timer reached its duration. For a repeating timer this is true
101    /// only on a tick that completed a cycle, matching [`Timer::just_finished`].
102    pub fn finished(&self) -> bool {
103        match self.mode {
104            TimerMode::Once => self.finished,
105            TimerMode::Repeating => self.times_finished_this_tick > 0,
106        }
107    }
108
109    /// Whether the most recent tick completed at least one cycle.
110    pub fn just_finished(&self) -> bool {
111        self.times_finished_this_tick > 0
112    }
113
114    /// How many full cycles the most recent tick completed. Always 0 or 1 for a
115    /// one-shot timer; for a repeating timer a long frame can complete several.
116    pub fn times_finished_this_tick(&self) -> u32 {
117        self.times_finished_this_tick
118    }
119
120    /// Seconds elapsed in the current cycle.
121    pub fn elapsed(&self) -> f32 {
122        self.elapsed
123    }
124
125    /// Seconds left in the current cycle.
126    pub fn remaining(&self) -> f32 {
127        (self.duration - self.elapsed).max(0.0)
128    }
129
130    /// The configured duration in seconds.
131    pub fn duration(&self) -> f32 {
132        self.duration
133    }
134
135    /// Progress through the current cycle from 0 at the start to 1 at the end.
136    pub fn fraction(&self) -> f32 {
137        if self.duration <= 0.0 {
138            1.0
139        } else {
140            (self.elapsed / self.duration).clamp(0.0, 1.0)
141        }
142    }
143
144    /// Progress remaining in the current cycle, from 1 at the start to 0 at the
145    /// end.
146    pub fn fraction_remaining(&self) -> f32 {
147        1.0 - self.fraction()
148    }
149
150    /// This timer's mode.
151    pub fn mode(&self) -> TimerMode {
152        self.mode
153    }
154
155    /// Whether the timer is paused.
156    pub fn paused(&self) -> bool {
157        self.paused
158    }
159
160    /// Sets the duration, keeping elapsed time. Negative values clamp to zero.
161    pub fn set_duration(&mut self, duration: f32) {
162        self.duration = duration.max(0.0);
163    }
164
165    /// Sets the elapsed time into the current cycle.
166    pub fn set_elapsed(&mut self, elapsed: f32) {
167        self.elapsed = elapsed.max(0.0);
168    }
169
170    /// Switches between one-shot and repeating.
171    pub fn set_mode(&mut self, mode: TimerMode) {
172        self.mode = mode;
173    }
174
175    /// Pauses or resumes ticking.
176    pub fn set_paused(&mut self, paused: bool) {
177        self.paused = paused;
178    }
179
180    /// Pauses ticking.
181    pub fn pause(&mut self) {
182        self.paused = true;
183    }
184
185    /// Resumes ticking.
186    pub fn unpause(&mut self) {
187        self.paused = false;
188    }
189
190    /// Clears elapsed time and the finished state, leaving duration and mode.
191    pub fn reset(&mut self) {
192        self.elapsed = 0.0;
193        self.finished = false;
194        self.times_finished_this_tick = 0;
195    }
196}
197
198/// An open-ended count-up. Tick it with the frame delta and read
199/// [`Stopwatch::elapsed`]. Use it where there is no fixed deadline, such as time
200/// since the player last moved or how long an input has been held.
201#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
202pub struct Stopwatch {
203    elapsed: f32,
204    paused: bool,
205}
206
207impl Stopwatch {
208    /// A stopwatch at zero.
209    pub fn new() -> Self {
210        Self::default()
211    }
212
213    /// Advances the stopwatch by `delta` seconds, returning it for chaining. A
214    /// paused stopwatch ignores the delta.
215    pub fn tick(&mut self, delta: f32) -> &mut Self {
216        if !self.paused {
217            self.elapsed += delta;
218        }
219        self
220    }
221
222    /// Seconds accumulated since the last reset.
223    pub fn elapsed(&self) -> f32 {
224        self.elapsed
225    }
226
227    /// Whether the stopwatch is paused.
228    pub fn paused(&self) -> bool {
229        self.paused
230    }
231
232    /// Sets the accumulated time.
233    pub fn set_elapsed(&mut self, elapsed: f32) {
234        self.elapsed = elapsed.max(0.0);
235    }
236
237    /// Pauses or resumes ticking.
238    pub fn set_paused(&mut self, paused: bool) {
239        self.paused = paused;
240    }
241
242    /// Pauses ticking.
243    pub fn pause(&mut self) {
244        self.paused = true;
245    }
246
247    /// Resumes ticking.
248    pub fn unpause(&mut self) {
249        self.paused = false;
250    }
251
252    /// Clears the accumulated time.
253    pub fn reset(&mut self) {
254        self.elapsed = 0.0;
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    #[test]
263    fn once_latches_finished_and_clamps_elapsed() {
264        let mut timer = Timer::once(1.0);
265        timer.tick(0.6);
266        assert!(!timer.finished());
267        timer.tick(0.6);
268        assert!(timer.finished());
269        assert!(timer.just_finished());
270        assert_eq!(timer.elapsed(), 1.0);
271        timer.tick(5.0);
272        assert!(timer.finished());
273        assert!(!timer.just_finished());
274        assert_eq!(timer.elapsed(), 1.0);
275    }
276
277    #[test]
278    fn repeating_carries_remainder() {
279        let mut timer = Timer::repeating(1.0);
280        timer.tick(1.5);
281        assert!(timer.just_finished());
282        assert_eq!(timer.times_finished_this_tick(), 1);
283        assert!((timer.elapsed() - 0.5).abs() < f32::EPSILON);
284    }
285
286    #[test]
287    fn repeating_counts_multiple_cycles_in_one_tick() {
288        let mut timer = Timer::repeating(0.5);
289        timer.tick(1.6);
290        assert_eq!(timer.times_finished_this_tick(), 3);
291        assert!((timer.elapsed() - 0.1).abs() < 1e-6);
292    }
293
294    #[test]
295    fn paused_timer_ignores_ticks() {
296        let mut timer = Timer::once(1.0);
297        timer.pause();
298        timer.tick(2.0);
299        assert!(!timer.finished());
300        assert_eq!(timer.elapsed(), 0.0);
301        timer.unpause();
302        timer.tick(2.0);
303        assert!(timer.finished());
304    }
305
306    #[test]
307    fn fraction_reports_progress() {
308        let mut timer = Timer::once(4.0);
309        timer.tick(1.0);
310        assert!((timer.fraction() - 0.25).abs() < f32::EPSILON);
311        assert!((timer.fraction_remaining() - 0.75).abs() < f32::EPSILON);
312        assert!((timer.remaining() - 3.0).abs() < f32::EPSILON);
313    }
314
315    #[test]
316    fn reset_clears_progress() {
317        let mut timer = Timer::once(1.0);
318        timer.tick(1.0);
319        timer.reset();
320        assert!(!timer.finished());
321        assert_eq!(timer.elapsed(), 0.0);
322    }
323
324    #[test]
325    fn stopwatch_accumulates_and_pauses() {
326        let mut stopwatch = Stopwatch::new();
327        stopwatch.tick(0.5);
328        stopwatch.tick(0.5);
329        assert!((stopwatch.elapsed() - 1.0).abs() < f32::EPSILON);
330        stopwatch.pause();
331        stopwatch.tick(1.0);
332        assert!((stopwatch.elapsed() - 1.0).abs() < f32::EPSILON);
333        stopwatch.reset();
334        assert_eq!(stopwatch.elapsed(), 0.0);
335    }
336}