1use shuyi_core::Pos;
12
13#[cfg(not(feature = "std"))]
14use crate::float::Float;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
18pub enum Facing {
19 #[default]
21 Down,
22 Left,
24 Right,
26 Up,
28}
29
30impl Facing {
31 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
42pub const STANDING_FRAME: u8 = 1;
44
45pub const IDLE_STEP_MS: u64 = 250;
48
49pub const IDLE_FRAMES: [u8; 4] = [STANDING_FRAME, 2, STANDING_FRAME, 0];
53
54pub const WALK_FRAME_S: f32 = 0.1;
56
57pub const WALK_FRAMES: [u8; 4] = [0, STANDING_FRAME, 2, STANDING_FRAME];
60
61pub 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
68pub 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#[derive(Debug, Clone, Copy, PartialEq)]
80pub struct Gait {
81 pub facing: Facing,
83 pub frame: u8,
85 pub offset: (f32, f32),
88}
89
90pub 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
104pub 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 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 #[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
133fn 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 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 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 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 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 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 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 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 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 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 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 #[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 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 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}