Skip to main content

repame_sim/
timer.rs

1//! Fixed-step countdown timer: the alarm pattern step-based games run on.
2//! Counts down in sim steps, fires exactly on the landing step, re-arms for
3//! repeats; a disarmed timer never fires.
4//!
5//! Timebase is seconds like [`SimTime`](crate::SimTime): construct from
6//! seconds (`15.0 / 30.0` for 15 steps at 30 Hz), tick with the step's
7//! `delta_secs`. [`StepTimer::just_finished`] is true only on the completing
8//! step, because branching on that single-step edge is the whole point.
9
10use bevy_ecs::prelude::*;
11use serde::{Deserialize, Serialize};
12
13/// Once (clamp at duration) vs repeating (wrap elapsed) expiry.
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
15pub enum TimerMode {
16    Once,
17    Repeating,
18}
19
20/// Countdown/up timer over plain seconds, ticked once per fixed step.
21#[derive(Component, Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
22pub struct StepTimer {
23    elapsed: f32,
24    dur: f32,
25    mode: TimerMode,
26    just_finished: bool,
27}
28
29impl Default for StepTimer {
30    /// Disarmed (finished from birth): the "no timer" state.
31    fn default() -> Self {
32        Self::disarmed()
33    }
34}
35
36impl StepTimer {
37    pub fn from_seconds(dur: f32, mode: TimerMode) -> Self {
38        Self {
39            elapsed: 0.0,
40            dur: dur.max(0.0),
41            mode,
42            just_finished: false,
43        }
44    }
45
46    /// Disarmed timer (finished from birth).
47    pub fn disarmed() -> Self {
48        Self {
49            elapsed: 0.0,
50            dur: 0.0,
51            mode: TimerMode::Once,
52            just_finished: false,
53        }
54    }
55
56    /// Advance by `dt` (read `just_finished()` / `finished()` afterwards; a
57    /// finished `Once` never re-fires, so a repeat comes from re-arming).
58    pub fn tick(&mut self, dt: f32) {
59        self.just_finished = false;
60        if self.dur <= 0.0 {
61            return;
62        }
63        match self.mode {
64            TimerMode::Once => {
65                let was = self.finished();
66                self.elapsed = (self.elapsed + dt).min(self.dur);
67                self.just_finished = !was && self.finished();
68            }
69            TimerMode::Repeating => {
70                self.elapsed += dt;
71                if self.elapsed >= self.dur {
72                    self.elapsed %= self.dur;
73                    self.just_finished = true;
74                }
75            }
76        }
77    }
78
79    /// True only on the step the timer completed (cleared next step).
80    pub fn just_finished(&self) -> bool {
81        self.just_finished
82    }
83
84    /// Latching finished state (stays true for `Once`).
85    pub fn finished(&self) -> bool {
86        self.elapsed >= self.dur
87    }
88
89    /// Alias for call sites that read `is_finished`.
90    pub fn is_finished(&self) -> bool {
91        self.finished()
92    }
93
94    pub fn duration(&self) -> f32 {
95        self.dur
96    }
97
98    pub fn elapsed_secs(&self) -> f32 {
99        self.elapsed
100    }
101
102    pub fn remaining_secs(&self) -> f32 {
103        (self.dur - self.elapsed).max(0.0)
104    }
105
106    /// 0..1 progress (1.0 for zero-duration timers).
107    pub fn fraction(&self) -> f32 {
108        if self.dur <= 0.0 {
109            return 1.0;
110        }
111        (self.elapsed / self.dur).clamp(0.0, 1.0)
112    }
113
114    /// Remaining seconds for countdown users.
115    pub fn remaining(&self) -> f32 {
116        self.remaining_secs()
117    }
118
119    /// Re-arm to full duration.
120    pub fn reset(&mut self) {
121        self.elapsed = 0.0;
122        self.just_finished = false;
123    }
124
125    /// Re-arm with a new duration (elapsed preserved).
126    pub fn set_duration(&mut self, dur: f32) {
127        self.dur = dur.max(0.0);
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    const DT: f32 = 1.0 / 30.0;
136
137    #[test]
138    fn once_fires_exactly_once_and_stays_finished() {
139        let mut t = StepTimer::from_seconds(3.0 * DT, TimerMode::Once);
140        let mut fires = 0;
141        for _ in 0..2 {
142            t.tick(DT);
143            assert!(!t.just_finished());
144        }
145        for _ in 0..7 {
146            t.tick(DT);
147            fires += t.just_finished() as u32;
148        }
149        assert_eq!(fires, 1);
150        assert!(t.finished());
151    }
152
153    #[test]
154    fn disarmed_and_zero_duration_never_fire() {
155        let mut d = StepTimer::disarmed();
156        d.tick(DT);
157        assert!(!d.just_finished());
158        assert!(d.finished());
159        assert_eq!(d.fraction(), 1.0);
160
161        let mut z = StepTimer::from_seconds(0.0, TimerMode::Once);
162        for _ in 0..10 {
163            z.tick(DT);
164            assert!(!z.just_finished());
165        }
166    }
167
168    #[test]
169    fn repeating_waits_the_period_then_fires() {
170        let mut t = StepTimer::from_seconds(4.0 * DT, TimerMode::Repeating);
171        let mut fires = 0;
172        for i in 0..6 {
173            t.tick(DT);
174            if i < 3 {
175                assert!(!t.just_finished());
176            }
177            fires += t.just_finished() as u32;
178        }
179        assert_eq!(fires, 1);
180    }
181
182    #[test]
183    fn fraction_and_remaining_track_progress() {
184        let mut t = StepTimer::from_seconds(4.0 * DT, TimerMode::Once);
185        t.tick(DT);
186        assert!((t.fraction() - 0.25).abs() < 1e-6);
187        assert!((t.remaining_secs() - 3.0 * DT).abs() < 1e-6);
188        t.tick(10.0 * DT);
189        assert_eq!(t.fraction(), 1.0);
190        assert_eq!(t.remaining_secs(), 0.0);
191    }
192
193    #[test]
194    fn reset_rearms() {
195        let mut t = StepTimer::from_seconds(2.0 * DT, TimerMode::Once);
196        for _ in 0..4 {
197            t.tick(DT);
198        }
199        assert!(t.finished());
200        t.reset();
201        assert!(!t.finished());
202        let mut fires = 0;
203        for _ in 0..4 {
204            t.tick(DT);
205            fires += t.just_finished() as u32;
206        }
207        assert_eq!(fires, 1);
208    }
209}