Skip to main content

kvas_engine/
timer.rs

1//! Deterministic gameplay timers driven explicitly by simulation deltas.
2//!
3//! Timers never read wall-clock time and own no host resources, so they may be
4//! stored directly in script structs. Hot reload resets unexposed script state.
5//!
6//! ```
7//! use kvas_engine::timer::{Timer, TimerMode};
8//! use std::time::Duration;
9//! let mut timer = Timer::new(Duration::from_millis(100), TimerMode::Repeating);
10//! timer.tick(Duration::from_millis(250));
11//! assert_eq!(timer.times_finished(), 2);
12//! assert_eq!(timer.elapsed(), Duration::from_millis(50));
13//! assert!((timer.fraction() - 0.5).abs() < 1e-6);
14//! ```
15
16use std::time::Duration;
17
18/// What happens when a timer reaches its duration.
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum TimerMode {
21    /// Completes once and remains finished until reset.
22    Once,
23    /// Wraps elapsed time and counts every crossed interval, including catch-up.
24    Repeating,
25}
26
27/// Explicitly ticked timer with pause, reset and exact duration accounting.
28/// A zero-duration timer completes once per non-paused tick, including a zero tick.
29#[derive(Clone, Debug)]
30pub struct Timer {
31    duration: Duration,
32    elapsed: Duration,
33    mode: TimerMode,
34    paused: bool,
35    finished: bool,
36    times_finished: u64,
37}
38
39impl Timer {
40    /// Creates an unpaused timer at zero elapsed time.
41    pub fn new(duration: Duration, mode: TimerMode) -> Self {
42        Self { duration, elapsed: Duration::ZERO, mode, paused: false, finished: false, times_finished: 0 }
43    }
44
45    /// Creates a one-shot timer from finite non-negative seconds.
46    /// Invalid or unrepresentable durations return `None`.
47    pub fn once(seconds: f32) -> Option<Self> {
48        Duration::try_from_secs_f32(seconds).ok().map(|d| Self::new(d, TimerMode::Once))
49    }
50
51    /// Creates a repeating timer from finite non-negative seconds.
52    pub fn repeating(seconds: f32) -> Option<Self> {
53        Duration::try_from_secs_f32(seconds).ok().map(|d| Self::new(d, TimerMode::Repeating))
54    }
55
56    /// Advances time and replaces this tick's completion count.
57    /// Paused ticks clear completion pulses while preserving elapsed time and
58    /// the sticky finished state of one-shot timers.
59    pub fn tick(&mut self, delta: Duration) -> &mut Self {
60        self.times_finished = 0;
61        if self.mode == TimerMode::Repeating { self.finished = false; }
62        if self.paused || (self.mode == TimerMode::Once && self.finished) {
63            return self;
64        }
65        if self.duration.is_zero() {
66            self.times_finished = 1;
67            self.finished = true;
68            return self;
69        }
70        let total = self.elapsed.as_nanos() + delta.as_nanos();
71        let duration = self.duration.as_nanos();
72        if total >= duration {
73            self.finished = true;
74            match self.mode {
75                TimerMode::Once => {
76                    self.elapsed = self.duration;
77                    self.times_finished = 1;
78                }
79                TimerMode::Repeating => {
80                    self.times_finished = (total / duration).min(u64::MAX as u128) as u64;
81                    self.elapsed = from_nanos(total % duration);
82                }
83            }
84        } else {
85            self.elapsed = from_nanos(total);
86            self.finished = false;
87        }
88        self
89    }
90
91    /// Ticks using a script/frame delta in seconds. Invalid deltas are ignored
92    /// and clear completion edges, so a bad frame cannot poison timer state.
93    pub fn tick_secs(&mut self, seconds: f32) -> &mut Self {
94        match Duration::try_from_secs_f32(seconds) {
95            Ok(dt) => self.tick(dt),
96            Err(_) => {
97                self.times_finished = 0;
98                if self.mode == TimerMode::Repeating { self.finished = false; }
99                self
100            }
101        }
102    }
103
104    /// Configured interval.
105    pub fn duration(&self) -> Duration {
106        self.duration
107    }
108    /// Elapsed part of the interval (clamped for one-shot, wrapped for repeating).
109    pub fn elapsed(&self) -> Duration {
110        self.elapsed
111    }
112    /// Time remaining until the next completion. A finished one-shot returns zero.
113    pub fn remaining(&self) -> Duration {
114        self.duration.saturating_sub(self.elapsed)
115    }
116    /// Progress in `[0, 1]`; zero-duration timers return one after their first tick.
117    pub fn fraction(&self) -> f32 {
118        if self.duration.is_zero() {
119            return if self.finished { 1.0 } else { 0.0 };
120        }
121        (self.elapsed.as_secs_f64() / self.duration.as_secs_f64()).clamp(0.0, 1.0) as f32
122    }
123    /// Whether a one-shot is complete, or a repeating timer completed this tick.
124    pub fn finished(&self) -> bool {
125        self.finished
126    }
127    /// Whether the most recent tick crossed at least one interval.
128    pub fn just_finished(&self) -> bool {
129        self.times_finished != 0
130    }
131    /// Number of crossed intervals in the last tick, saturating at `u64::MAX`.
132    pub fn times_finished(&self) -> u64 {
133        self.times_finished
134    }
135    /// Current repetition policy.
136    pub fn mode(&self) -> TimerMode {
137        self.mode
138    }
139    /// Whether ticks are suspended.
140    pub fn is_paused(&self) -> bool {
141        self.paused
142    }
143    /// Suspends or resumes the timer without resetting it.
144    pub fn set_paused(&mut self, paused: bool) {
145        self.paused = paused;
146    }
147    /// Clears elapsed/completion state while preserving duration, mode and pause.
148    pub fn reset(&mut self) {
149        self.elapsed = Duration::ZERO;
150        self.finished = false;
151        self.times_finished = 0;
152    }
153    /// Replaces the interval and resets progress, preserving mode and pause.
154    pub fn set_duration(&mut self, duration: Duration) {
155        self.duration = duration;
156        self.reset();
157    }
158    /// Replaces the repetition policy and resets progress.
159    pub fn set_mode(&mut self, mode: TimerMode) {
160        self.mode = mode;
161        self.reset();
162    }
163}
164
165fn from_nanos(value: u128) -> Duration {
166    Duration::new((value / 1_000_000_000) as u64, (value % 1_000_000_000) as u32)
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172    #[test]
173    fn once_edges_and_pause() {
174        let mut t = Timer::once(1.0).unwrap();
175        t.tick_secs(0.5);
176        assert_eq!(t.fraction(), 0.5);
177        t.set_paused(true);
178        t.tick_secs(2.0);
179        assert!(!t.finished());
180        t.set_paused(false);
181        t.tick_secs(2.0);
182        assert!(t.just_finished());
183        assert_eq!(t.remaining(), Duration::ZERO);
184        t.tick_secs(1.0);
185        assert!(t.finished());
186        assert!(!t.just_finished());
187        t.reset();
188        assert!(!t.finished());
189        assert_eq!(t.elapsed(), Duration::ZERO);
190    }
191    #[test]
192    fn repeating_counts_catchup_exactly_and_handles_invalid_deltas() {
193        let mut t = Timer::new(Duration::from_millis(100), TimerMode::Repeating);
194        t.tick(Duration::from_millis(250));
195        assert_eq!(t.times_finished(), 2);
196        assert_eq!(t.elapsed(), Duration::from_millis(50));
197        t.tick_secs(f32::NAN);
198        assert!(!t.just_finished());
199        assert_eq!(t.elapsed(), Duration::from_millis(50));
200        t.tick(Duration::from_millis(50));
201        assert_eq!(t.times_finished(), 1);
202        assert_eq!(t.elapsed(), Duration::ZERO);
203        assert!(Timer::once(-1.0).is_none());
204        assert!(Timer::repeating(f32::INFINITY).is_none());
205    }
206    #[test]
207    fn zero_and_max_durations_are_bounded() {
208        let mut t = Timer::new(Duration::ZERO, TimerMode::Repeating);
209        t.tick(Duration::MAX);
210        assert_eq!(t.times_finished(), 1);
211        t.set_duration(Duration::from_nanos(1));
212        t.tick(Duration::MAX);
213        assert_eq!(t.times_finished(), u64::MAX);
214        t.set_duration(Duration::MAX);
215        t.tick(Duration::MAX);
216        assert_eq!(t.times_finished(), 1);
217    }
218}