Skip to main content

galeon_engine/
virtual_time.rs

1// SPDX-License-Identifier: AGPL-3.0-only OR Commercial
2
3/// Virtual time controls: pause, speed scaling, and max-delta clamping.
4///
5/// Insert this resource to gain time control. If absent, the game loop
6/// uses raw host elapsed (backward compatible).
7///
8/// # Example
9///
10/// ```rust
11/// use galeon_engine::VirtualTime;
12///
13/// let mut vt = VirtualTime::new();
14/// assert!((vt.effective_elapsed(0.5) - 0.25).abs() < f64::EPSILON); // clamped to max_delta
15///
16/// vt.paused = true;
17/// assert_eq!(vt.effective_elapsed(1.0), 0.0);
18///
19/// vt.paused = false;
20/// vt.scale = 2.0;
21/// assert!((vt.effective_elapsed(0.1) - 0.2).abs() < f64::EPSILON);
22/// ```
23pub struct VirtualTime {
24    /// When true, `effective_elapsed()` always returns 0.
25    pub paused: bool,
26    /// Speed multiplier. Clamped to `[0.0, 8.0]`.
27    /// - 1.0 = normal
28    /// - 0.5 = half speed
29    /// - 2.0 = double speed
30    pub scale: f64,
31    /// Raw elapsed is clamped to this value before scaling.
32    /// Prevents death spirals when the host delivers a huge frame delta
33    /// (e.g., tab was backgrounded). Default: 0.25 s.
34    pub max_delta: f64,
35    /// Total virtual time elapsed since engine start.
36    /// Accumulated by `game_loop::tick()` each frame.
37    pub elapsed: f64,
38}
39
40impl VirtualTime {
41    /// Create with default settings: unpaused, scale 1.0, max_delta 0.25 s.
42    pub fn new() -> Self {
43        Self {
44            paused: false,
45            scale: 1.0,
46            max_delta: 0.25,
47            elapsed: 0.0,
48        }
49    }
50
51    /// Transform raw host elapsed into virtual elapsed.
52    ///
53    /// Returns 0.0 when paused. Otherwise clamps `raw` to `max_delta`,
54    /// then multiplies by `scale` (clamped to `[0.0, 8.0]`).
55    pub fn effective_elapsed(&self, raw: f64) -> f64 {
56        if self.paused {
57            return 0.0;
58        }
59        let clamped = raw.min(self.max_delta).max(0.0);
60        let scale = self.scale.clamp(0.0, 8.0);
61        clamped * scale
62    }
63}
64
65impl Default for VirtualTime {
66    fn default() -> Self {
67        Self::new()
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn default_values() {
77        let vt = VirtualTime::new();
78        assert!(!vt.paused);
79        assert!((vt.scale - 1.0).abs() < f64::EPSILON);
80        assert!((vt.max_delta - 0.25).abs() < f64::EPSILON);
81        assert!((vt.elapsed - 0.0).abs() < f64::EPSILON);
82    }
83
84    #[test]
85    fn effective_elapsed_normal() {
86        let vt = VirtualTime::new();
87        let e = vt.effective_elapsed(0.1);
88        assert!((e - 0.1).abs() < f64::EPSILON);
89    }
90
91    #[test]
92    fn effective_elapsed_paused() {
93        let mut vt = VirtualTime::new();
94        vt.paused = true;
95        assert_eq!(vt.effective_elapsed(1.0), 0.0);
96        assert_eq!(vt.effective_elapsed(0.0), 0.0);
97    }
98
99    #[test]
100    fn effective_elapsed_scaled() {
101        let mut vt = VirtualTime::new();
102        vt.scale = 2.0;
103        let e = vt.effective_elapsed(0.1);
104        assert!((e - 0.2).abs() < f64::EPSILON);
105
106        vt.scale = 0.5;
107        let e = vt.effective_elapsed(0.1);
108        assert!((e - 0.05).abs() < f64::EPSILON);
109    }
110
111    #[test]
112    fn effective_elapsed_max_delta_clamp() {
113        let vt = VirtualTime::new(); // max_delta = 0.25
114        let e = vt.effective_elapsed(2.0); // clamped to 0.25
115        assert!((e - 0.25).abs() < f64::EPSILON);
116    }
117
118    #[test]
119    fn effective_elapsed_scale_and_clamp_interact() {
120        let mut vt = VirtualTime::new(); // max_delta = 0.25
121        vt.scale = 4.0;
122        let e = vt.effective_elapsed(2.0); // clamped to 0.25, then * 4.0 = 1.0
123        assert!((e - 1.0).abs() < f64::EPSILON);
124    }
125
126    #[test]
127    fn scale_clamped_to_bounds() {
128        let mut vt = VirtualTime::new();
129        vt.scale = -1.0;
130        assert_eq!(vt.effective_elapsed(0.1), 0.0); // scale clamped to 0.0
131
132        vt.scale = 100.0;
133        let e = vt.effective_elapsed(0.1);
134        assert!((e - 0.8).abs() < f64::EPSILON); // scale clamped to 8.0, 0.1 * 8.0
135    }
136
137    #[test]
138    fn negative_raw_clamped_to_zero() {
139        let vt = VirtualTime::new();
140        assert_eq!(vt.effective_elapsed(-0.5), 0.0);
141    }
142}