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