shuyi_core/timer.rs
1//! Timers a game's rules set and forget, counted in steps or turns:
2//! [`Countdown`], which fires once and is the cooldown too, and [`Every`],
3//! which repeats. With them, the step rate ([`STEPS_PER_SECOND`]) and the
4//! two conversions a length in milliseconds goes through
5//! ([`steps_from_ms`], [`ms_from_steps`]).
6//!
7//! A timer says when; a tween ([`ease`](crate::ease)) says how far, drawn
8//! from the time alone.
9//!
10//! # Two kinds of time
11//!
12//! **Simulation time** is steps or turns. A tick game steps its rules once
13//! for each of `input.ticks()` and advances its timers by 1 in each step,
14//! so a timer fires on the same step at 30 and at 60 frames a second. A
15//! turn game advances by 1 where its turn or round ends, in its own
16//! [`TurnRules::apply`](crate::TurnRules::apply). Timers live in the state
17//! the rules own, so they save, hash ([`state_hash`](crate::state_hash))
18//! and replay with it. A timer moves only when it is advanced, so a paused
19//! game's timers wait.
20//!
21//! **Presentation time** is `FrameInput::dt()`, `Ctx::clock_s` and
22//! `HurryClock`, in `shuyi-ui`: glides, blinks, text typing out, a clock on
23//! screen. It may stay in floats, and no rule ever reads it.
24//!
25//! ```
26//! use shuyi_core::timer::Every;
27//!
28//! // Something that moves every 8 steps. `ticks` stands in for
29//! // `input.ticks()`, the steps due this frame.
30//! let mut mover = Every::new(8);
31//! let mut x = 0;
32//! for ticks in [1, 1, 2, 4, 1, 0, 2] {
33//! for _ in 0..ticks {
34//! // One step of the rules.
35//! x += mover.advance(1);
36//! }
37//! }
38//! assert_eq!(x, 1);
39//! assert_eq!(mover.until_next(), 5);
40//! ```
41//!
42//! # Many timers, and things that happen later
43//!
44//! Many timers are a `BTreeMap<K, Countdown>` in the state, never a
45//! `HashMap`, whose order changes the hash ([`hash`](crate::hash)). An event
46//! that happens later is a [`Countdown`] kept beside it, which sends it when
47//! it fires.
48//!
49//! # Values read from a file
50//!
51//! Nothing checks a timer read from a hand-edited save. A value out of range
52//! (`left` above `length`, `since` at or above `period`) still behaves by the
53//! formulas on each method, and no method panics on it.
54
55use serde::{Deserialize, Serialize};
56
57/// Fixed simulation steps a second.
58pub const STEPS_PER_SECOND: u32 = 60;
59
60/// Milliseconds in a second.
61const MS_PER_SECOND: u64 = 1000;
62
63/// Whole steps in `ms` milliseconds at [`STEPS_PER_SECOND`], rounded up, so
64/// any time above 0 is at least one step. It turns a length a designer wrote
65/// in milliseconds into steps, once, at load. A timer never counts
66/// milliseconds.
67#[expect(
68 clippy::cast_possible_truncation,
69 reason = "the most it gives is 257 698 038, for `u32::MAX` ms, which fits"
70)]
71pub const fn steps_from_ms(ms: u32) -> u32 {
72 (ms as u64 * STEPS_PER_SECOND as u64).div_ceil(MS_PER_SECOND) as u32
73}
74
75/// The milliseconds `steps` steps last at [`STEPS_PER_SECOND`], rounded down:
76/// 0, 16, 33, 50, … and exactly 1000 at step 60, so it never drifts as a whole
77/// number of milliseconds per step would. [`steps_from_ms`] of it gives
78/// `steps` back while that fits a `u32`.
79///
80/// It is the engine's one rule from steps to milliseconds: data timed in
81/// milliseconds (a clip, a particle's life, a bone clip) is read at
82/// `ms_from_steps(step - start_step)`, by `frame_at`, `particles_at` and
83/// `pose_at` alike.
84pub const fn ms_from_steps(steps: u32) -> u64 {
85 steps as u64 * MS_PER_SECOND / STEPS_PER_SECOND as u64
86}
87
88/// A timer that fires once, after `length` steps or turns, and waits to be
89/// [restarted](Self::restart).
90///
91/// It is the cooldown too. Advance it by 1 each step or turn, and:
92///
93/// ```
94/// use shuyi_core::timer::Countdown;
95///
96/// // Ready for its first use, then two turns' wait.
97/// let mut cooldown = Countdown::new_ready(2);
98/// let mut uses = 0;
99/// for used in [true, true, false, true] {
100/// if used && cooldown.ready() {
101/// uses += 1;
102/// cooldown.restart();
103/// }
104/// cooldown.advance(1);
105/// }
106/// assert_eq!(uses, 2);
107/// ```
108///
109/// The default has a length of 0 and is ready.
110#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
111pub struct Countdown {
112 /// What [`restart`](Self::restart) starts it from.
113 length: u32,
114 /// What is left of it.
115 left: u32,
116}
117
118impl Countdown {
119 /// One started: it fires after `length`.
120 pub const fn new(length: u32) -> Self {
121 Self {
122 length,
123 left: length,
124 }
125 }
126
127 /// One done already, for a cooldown whose first use goes through.
128 pub const fn new_ready(length: u32) -> Self {
129 Self { length, left: 0 }
130 }
131
132 /// Takes `n` off what is left, stopping at 0. `true` only on the call
133 /// that takes it from above 0 to 0, so it fires once however far `n`
134 /// overshoots, and `advance(0)` never fires. A length of 0 is ready at
135 /// once and never fires.
136 pub fn advance(&mut self, n: u32) -> bool {
137 let running = self.left > 0;
138 self.left = self.left.saturating_sub(n);
139 running && self.left == 0
140 }
141
142 /// Whether nothing is left.
143 pub const fn ready(&self) -> bool {
144 self.left == 0
145 }
146
147 /// What is left of it.
148 pub const fn remaining(&self) -> u32 {
149 self.left
150 }
151
152 /// How much of it has passed, for a bar that fills: its length less what
153 /// is left, stopping at 0.
154 pub const fn elapsed(&self) -> u32 {
155 self.length.saturating_sub(self.left)
156 }
157
158 /// The whole of it.
159 pub const fn length(&self) -> u32 {
160 self.length
161 }
162
163 /// Starts it again from its length.
164 pub const fn restart(&mut self) {
165 self.left = self.length;
166 }
167}
168
169/// A timer that fires every `period` steps or turns.
170///
171/// A period of 0 acts as 1, whether given to [`new`](Self::new) or read from
172/// a file.
173#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
174pub struct Every {
175 /// How often it fires. Read through [`period`](Self::period).
176 period: u32,
177 /// How far it is since it last fired.
178 since: u32,
179}
180
181impl Every {
182 /// One that first fires after a whole `period`.
183 pub const fn new(period: u32) -> Self {
184 Self {
185 period: if period == 0 { 1 } else { period },
186 since: 0,
187 }
188 }
189
190 /// Moves it on by `n`, and says how many times it fired: `(since + n) /
191 /// period` times, keeping the remainder. The count saturates at
192 /// `u32::MAX`.
193 pub fn advance(&mut self, n: u32) -> u32 {
194 let period = u64::from(self.period());
195 let total = u64::from(self.since) + u64::from(n);
196 // Less than `period`, so it fits.
197 self.since = u32::try_from(total % period).unwrap_or(0);
198 u32::try_from(total / period).unwrap_or(u32::MAX)
199 }
200
201 /// How often it fires, at least 1.
202 pub fn period(&self) -> u32 {
203 self.period.max(1)
204 }
205
206 /// How far it is to its next firing: the period less how far it has
207 /// come, and 1 if that's out of range, because the next `advance(1)`
208 /// fires then.
209 pub fn until_next(&self) -> u32 {
210 self.period().saturating_sub(self.since).max(1)
211 }
212
213 /// Back to a whole period before its next firing.
214 pub const fn reset(&mut self) {
215 self.since = 0;
216 }
217}
218
219#[cfg(test)]
220mod tests;