Skip to main content

shuyi_ui/grid/
walk.rs

1//! How units move on a grid map (game ticket 0440): a unit that can still
2//! act steps on the spot, and a moving unit glides from tile to tile along
3//! its path, its legs going, turned the way it walks. Every timing is
4//! here.
5//!
6//! All of it is a look (game ADR-0038): the rules, the camera and the
7//! sounds go by the tile a walk is on, which changes at each tile's edge.
8//! How fast a walk goes is the look's too: each skin's setting
9//! ([`GridSkin::walk`](super::GridSkin::walk)).
10
11use shuyi_core::Pos;
12
13#[cfg(not(feature = "std"))]
14use crate::float::Float;
15
16/// Which way a unit is turned. A look only: never a rule.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
18pub enum Facing {
19    /// Towards the camera: every unit that isn't walking.
20    #[default]
21    Down,
22    /// To the left.
23    Left,
24    /// To the right.
25    Right,
26    /// Away from the camera.
27    Up,
28}
29
30impl Facing {
31    /// Its name in text for tests: `down`, `left`, `right` or `up`.
32    pub const fn name(self) -> &'static str {
33        match self {
34            Facing::Down => "down",
35            Facing::Left => "left",
36            Facing::Right => "right",
37            Facing::Up => "up",
38        }
39    }
40}
41
42/// The walking frame of a unit standing with its feet together.
43pub const STANDING_FRAME: u8 = 1;
44
45/// How long each frame of a unit stepping on the spot shows, in
46/// milliseconds. *Tunable.*
47pub const IDLE_STEP_MS: u64 = 250;
48
49/// The frames of a unit stepping on the spot, one per [`IDLE_STEP_MS`]:
50/// standing, one foot, standing, the other foot. It starts on the standing
51/// frame.
52pub const IDLE_FRAMES: [u8; 4] = [STANDING_FRAME, 2, STANDING_FRAME, 0];
53
54/// How long each frame of a walking unit shows, in seconds. *Tunable.*
55pub const WALK_FRAME_S: f32 = 0.1;
56
57/// The frames of a walking unit, one per [`WALK_FRAME_S`]: one foot,
58/// standing, the other foot, standing. It starts with a foot forward.
59pub const WALK_FRAMES: [u8; 4] = [0, STANDING_FRAME, 2, STANDING_FRAME];
60
61/// The frame of a unit stepping on the spot at `clock_ms` on the screen's
62/// animation clock. Every such unit steps together.
63pub fn idle_frame(clock_ms: u64) -> u8 {
64    let step = usize::try_from((clock_ms / IDLE_STEP_MS) % 4).unwrap_or(0);
65    IDLE_FRAMES.get(step).copied().unwrap_or(STANDING_FRAME)
66}
67
68/// How many steps of a path of `len` tiles a walk has taken once it has
69/// gone `tiles` tiles: a step is taken as its tile is reached (at most all
70/// of them; none for a negative or NaN distance).
71pub fn steps(len: usize, tiles: f32) -> usize {
72    let all = len.saturating_sub(1);
73    (1..=u16::try_from(all).unwrap_or(u16::MAX))
74        .take_while(|&k| f32::from(k) <= tiles)
75        .count()
76}
77
78/// How a unit between two tiles of its walk looks.
79#[derive(Debug, Clone, Copy, PartialEq)]
80pub struct Gait {
81    /// The way it walks.
82    pub facing: Facing,
83    /// Its walking frame.
84    pub frame: u8,
85    /// How far it is from the tile it left towards the next, in tiles
86    /// across and down.
87    pub offset: (f32, f32),
88}
89
90/// The way a step from `from` to `to` goes: left or right if it goes
91/// across at all, else up or down.
92pub fn facing(from: Pos, to: Pos) -> Facing {
93    if to.x > from.x {
94        Facing::Right
95    } else if to.x < from.x {
96        Facing::Left
97    } else if to.y < from.y {
98        Facing::Up
99    } else {
100        Facing::Down
101    }
102}
103
104/// The look of a unit that has walked `tiles` tiles along `path` (its
105/// start first) at `pace` tiles per second, beside the tile [`steps`] puts
106/// it on; `None` once it has arrived (or has nowhere to go): it then
107/// stands, facing the camera.
108pub fn gait(path: &[Pos], tiles: f32, pace: f32) -> Option<Gait> {
109    let taken = steps(path.len(), tiles);
110    let (from, to) = (*path.get(taken)?, *path.get(taken + 1)?);
111    let tiles = if tiles.is_finite() {
112        tiles.max(0.0)
113    } else {
114        0.0
115    };
116    // `taken` is at most `tiles`, and small: exact in f32.
117    let part = (tiles - f32::from(u16::try_from(taken).unwrap_or(u16::MAX))).clamp(0.0, 1.0);
118    let toward = |a: i32, b: i32| match b.cmp(&a) {
119        core::cmp::Ordering::Less => -part,
120        core::cmp::Ordering::Equal => 0.0,
121        core::cmp::Ordering::Greater => part,
122    };
123    // Whole frames shown so far: far fewer than f32 holds exactly.
124    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
125    let shown = (tiles * frames_per_tile(pace)).floor() as usize;
126    Some(Gait {
127        facing: facing(from, to),
128        frame: WALK_FRAMES[shown % WALK_FRAMES.len()],
129        offset: (toward(from.x, to.x), toward(from.y, to.y)),
130    })
131}
132
133/// How many walking frames show while a unit crosses a tile at `pace`
134/// tiles per second (none at a pace of zero or one that isn't a number).
135fn frames_per_tile(pace: f32) -> f32 {
136    let per_tile = 1.0 / (pace * WALK_FRAME_S);
137    if per_tile.is_finite() { per_tile } else { 0.0 }
138}
139
140#[cfg(test)]
141mod tests {
142    use proptest::prelude::*;
143
144    use super::*;
145
146    use crate::prelude::*;
147
148    fn p(x: i32, y: i32) -> Pos {
149        Pos::new(x, y)
150    }
151
152    /// A pace of 5 tiles a second: two walking frames a tile.
153    const PACE: f32 = 5.0;
154
155    #[test]
156    fn a_facing_has_a_name_and_a_unit_stands_on_frame_1() {
157        let names = [
158            (Facing::Down, "down"),
159            (Facing::Left, "left"),
160            (Facing::Right, "right"),
161            (Facing::Up, "up"),
162        ];
163        for (facing, name) in names {
164            assert_eq!(facing.name(), name);
165        }
166        assert_eq!((Facing::default(), STANDING_FRAME), (Facing::Down, 1));
167    }
168
169    #[test]
170    fn a_unit_steps_on_the_spot_every_quarter_second() {
171        // Standing, a foot, standing, the other foot, and again.
172        let frames: Vec<u8> = (0..9).map(|k| idle_frame(k * 250)).collect();
173        assert_eq!(frames, [1, 2, 1, 0, 1, 2, 1, 0, 1]);
174        assert_eq!(idle_frame(249), 1);
175        assert_eq!(idle_frame(250), 2);
176        assert_eq!(idle_frame(499), 2);
177        assert_eq!(idle_frame(999), 0);
178        assert!(idle_frame(u64::MAX) <= 2);
179        assert_eq!(IDLE_STEP_MS, 250);
180    }
181
182    #[test]
183    fn a_step_is_taken_as_its_tile_is_reached() {
184        assert_eq!(steps(4, 0.0), 0);
185        assert_eq!(steps(4, 0.99), 0);
186        assert_eq!(steps(4, 1.0), 1);
187        assert_eq!(steps(4, 2.5), 2);
188        assert_eq!(steps(4, 3.0), 3);
189        assert_eq!(steps(4, 99.0), 3);
190        assert_eq!(steps(4, -1.0), 0);
191        assert_eq!(steps(4, f32::NAN), 0);
192        assert_eq!(steps(1, 5.0), 0);
193        assert_eq!(steps(0, 5.0), 0);
194    }
195
196    #[test]
197    fn a_walk_changes_frame_every_100_ms_at_any_pace() {
198        assert!((WALK_FRAME_S - 0.1).abs() < 1e-6);
199        let path = [p(1, 1), p(2, 1), p(3, 1)];
200        // Two frames a tile: a foot, standing; the other foot, standing.
201        let frame = |tiles| gait(&path, tiles, PACE).map(|g| g.frame);
202        assert_eq!(frame(0.0), Some(0));
203        assert_eq!(frame(0.25), Some(0));
204        assert_eq!(frame(0.5), Some(1));
205        assert_eq!(frame(0.75), Some(1));
206        assert_eq!(frame(1.0), Some(2));
207        assert_eq!(frame(1.5), Some(1));
208        assert_eq!(frame(2.0), None);
209        // The legs keep their time at any pace: at 10 tiles a second a
210        // frame lasts a whole tile; at 2.5, a quarter of one.
211        let at = |tiles, pace| gait(&path, tiles, pace).map(|g| g.frame);
212        assert_eq!(
213            (at(0.9, 10.0), at(1.0, 10.0), at(1.9, 10.0)),
214            (Some(0), Some(1), Some(1))
215        );
216        assert_eq!(
217            (at(0.2, 2.5), at(0.25, 2.5), at(0.5, 2.5)),
218            (Some(0), Some(1), Some(2))
219        );
220        // A pace that isn't a positive number: the first frame.
221        for bad in [0.0, -3.0, f32::NAN, f32::INFINITY] {
222            assert_eq!(at(1.5, bad), Some(0), "{bad}");
223        }
224        let path: Vec<Pos> = (0..6).map(|x| p(x, 0)).collect();
225        assert_eq!(gait(&path, 2.25, PACE).map(|g| g.frame), Some(0));
226        assert_eq!(gait(&path, 4.75, PACE).map(|g| g.frame), Some(1));
227    }
228
229    #[test]
230    fn a_walker_faces_the_way_it_goes_and_glides_to_the_next_tile() {
231        // Right, up, left, down.
232        let path = [p(1, 1), p(2, 1), p(2, 0), p(1, 0), p(1, 1)];
233        let at = |tiles| gait(&path, tiles, PACE).map(|g| (g.facing, g.offset));
234        assert_eq!(at(0.0), Some((Facing::Right, (0.0, 0.0))));
235        assert_eq!(at(0.25), Some((Facing::Right, (0.25, 0.0))));
236        assert_eq!(at(0.75), Some((Facing::Right, (0.75, 0.0))));
237        assert_eq!(at(1.0), Some((Facing::Up, (0.0, 0.0))));
238        assert_eq!(at(1.5), Some((Facing::Up, (0.0, -0.5))));
239        assert_eq!(at(2.5), Some((Facing::Left, (-0.5, 0.0))));
240        assert_eq!(at(3.25), Some((Facing::Down, (0.0, 0.25))));
241        // Arrived, or nowhere to go: it stands.
242        assert_eq!(at(4.0), None);
243        assert_eq!(at(99.0), None);
244        assert_eq!(gait(&[p(1, 1)], 0.5, PACE), None);
245        assert_eq!(gait(&[], 0.5, PACE), None);
246        // A bad distance counts as none walked.
247        assert_eq!(at(-3.0), at(0.0));
248        assert_eq!(at(f32::NAN), at(0.0));
249        assert_eq!(at(f32::NEG_INFINITY), at(0.0));
250        assert_eq!(at(f32::INFINITY), None);
251        // A step that isn't to a neighbour still goes one way, a tile at
252        // most.
253        assert_eq!(facing(p(0, 0), p(3, -3)), Facing::Right);
254        assert_eq!(facing(p(0, 0), p(-3, 3)), Facing::Left);
255        assert_eq!(facing(p(0, 0), p(0, 0)), Facing::Down);
256        assert_eq!(facing(p(0, 0), p(0, 1)), Facing::Down);
257        let far = gait(&[p(0, 0), p(5, -5)], 0.5, PACE).unwrap();
258        assert_eq!(far.offset, (0.5, -0.5));
259    }
260
261    prop_compose! {
262        /// A path of up to 12 tiles, each next to the one before.
263        fn any_path()(
264            start in (-20..20i32, -20..20i32),
265            turns in prop::collection::vec(0u8..4, 0..12),
266        ) -> Vec<Pos> {
267            let mut path = vec![p(start.0, start.1)];
268            let mut at = (start.0, start.1);
269            for turn in turns {
270                let (dx, dy) = [(1, 0), (-1, 0), (0, 1), (0, -1)][usize::from(turn)];
271                at = (at.0 + dx, at.1 + dy);
272                path.push(p(at.0, at.1));
273            }
274            path
275        }
276    }
277
278    proptest! {
279        /// For any path, at any moment of the walk, the unit is shown
280        /// within one tile of the path tile it is on, towards the next
281        /// one and never past it, with a frame its sheet has; once the
282        /// walk is over it is on the path's last tile with no offset.
283        #[test]
284        fn a_walker_stays_within_a_tile_of_its_path_and_ends_on_its_last_tile(
285            path in any_path(),
286            tiles in -2.0f32..16.0,
287        ) {
288            let taken = steps(path.len(), tiles);
289            prop_assert!(taken < path.len());
290            let on = path[taken];
291            if let Some(g) = gait(&path, tiles, PACE) {
292                let next = path[taken + 1];
293                let (dx, dy) = g.offset;
294                prop_assert!((0.0..1.0).contains(&dx.abs().max(dy.abs())), "{:?}", g);
295                // Towards the next tile, along one axis.
296                prop_assert!(dx == 0.0 || dy == 0.0, "{:?}", g);
297                prop_assert!(dx * f32::from(i16::try_from(next.x - on.x).unwrap_or(0)) >= 0.0);
298                prop_assert!(dy * f32::from(i16::try_from(next.y - on.y).unwrap_or(0)) >= 0.0);
299                prop_assert_eq!(g.facing, facing(on, next));
300                prop_assert!(g.frame <= 2);
301            } else {
302                prop_assert_eq!(Some(&on), path.last());
303            }
304            // Long enough, and it has arrived.
305            let end = f32::from(u8::try_from(path.len()).unwrap_or(u8::MAX));
306            prop_assert_eq!(gait(&path, end, PACE), None);
307            prop_assert_eq!(path.get(steps(path.len(), end)), path.last());
308        }
309    }
310}