Skip to main content

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;