1use core::f32::consts::TAU;
7
8use shuyi_core::{Dir, Pos};
9
10#[cfg(not(feature = "std"))]
11use crate::float::Float;
12use crate::input::frame_seconds;
13
14pub const BLINK_PERIOD: f32 = 1.0;
16
17pub const BLINK_MIN: f32 = 0.5;
19
20#[derive(Debug, Clone, Copy, PartialEq)]
22pub struct Cursor {
23 pub pos: Pos,
25 pub blink_t: f32,
27}
28
29impl Cursor {
30 pub const fn new(pos: Pos) -> Self {
32 Self { pos, blink_t: 0.0 }
33 }
34
35 pub fn tick(&mut self, dt: f32) {
38 self.blink_t = (self.blink_t + frame_seconds(dt)) % BLINK_PERIOD;
39 }
40
41 pub fn brightness(&self) -> f32 {
44 pulse(self.blink_t, BLINK_PERIOD)
45 }
46
47 pub fn step(&mut self, dir: Dir, map_w: u16, map_h: u16) -> bool {
51 let (dx, dy) = dir.delta();
52 let clamp = |v: i32, len: u16| v.clamp(0, (i32::from(len) - 1).max(0));
53 let to = Pos::new(clamp(self.pos.x + dx, map_w), clamp(self.pos.y + dy, map_h));
54 if to == self.pos {
55 return false;
56 }
57 self.pos = to;
58 self.blink_t = 0.0;
59 true
60 }
61
62 pub fn jump(&mut self, pos: Pos) {
64 *self = Self::new(pos);
65 }
66}
67
68fn pulse(t: f32, period: f32) -> f32 {
71 let wave = (TAU * t / period).cos();
72 BLINK_MIN + (1.0 - BLINK_MIN) * (1.0 + wave) / 2.0
73}
74
75#[cfg(test)]
76mod tests {
77 use proptest::prelude::*;
78
79 use super::*;
80
81 const MOVES: [Dir; 4] = [Dir::Left, Dir::Right, Dir::Up, Dir::Down];
82
83 #[test]
84 fn steps_one_tile_and_clamps_at_the_edges() {
85 let mut c = Cursor::new(Pos::new(1, 1));
86 c.blink_t = 0.3;
87 assert!(c.step(Dir::Left, 3, 3));
88 assert_eq!((c.pos, c.blink_t), (Pos::new(0, 1), 0.0));
89 c.blink_t = 0.3;
90 assert!(!c.step(Dir::Left, 3, 3));
91 assert_eq!((c.pos, c.blink_t), (Pos::new(0, 1), 0.3));
92 assert!(c.step(Dir::Up, 3, 3));
93 assert!(!c.step(Dir::Up, 3, 3));
94 assert_eq!(c.pos, Pos::new(0, 0));
95 assert!(c.step(Dir::Right, 3, 3));
96 assert!(c.step(Dir::Right, 3, 3));
97 assert!(!c.step(Dir::Right, 3, 3));
98 assert!(c.step(Dir::Down, 3, 3));
99 assert!(c.step(Dir::Down, 3, 3));
100 assert!(!c.step(Dir::Down, 3, 3));
101 assert_eq!(c.pos, Pos::new(2, 2));
102 let mut corner = Cursor::new(Pos::new(0, 0));
104 assert!(!corner.step(Dir::Right, 0, 0));
105 assert_eq!(corner.pos, Pos::new(0, 0));
106 }
107
108 #[test]
109 fn jump_moves_and_restarts_the_pulse() {
110 let mut c = Cursor::new(Pos::new(1, 1));
111 c.tick(0.4);
112 c.jump(Pos::new(5, 6));
113 assert_eq!(c, Cursor::new(Pos::new(5, 6)));
114 }
115
116 #[test]
117 fn pulses_between_full_and_half_brightness() {
118 let mut c = Cursor::new(Pos::new(0, 0));
119 assert!((c.brightness() - 1.0).abs() < 1e-6);
120 c.tick(0.25);
121 assert!((c.blink_t - 0.25).abs() < 1e-6);
122 assert!((c.brightness() - 0.75).abs() < 1e-6);
123 c.tick(0.25);
124 assert!((c.brightness() - 0.5).abs() < 1e-6);
125 c.tick(0.5);
126 assert!(c.blink_t < 1e-6, "wraps after a period: {}", c.blink_t);
127 c.tick(0.25);
128 c.tick(-1.0);
129 c.tick(f32::NAN);
130 c.tick(f32::INFINITY);
131 assert!((c.blink_t - 0.25).abs() < 1e-6, "{}", c.blink_t);
132 }
133
134 #[test]
135 fn pulse_scales_with_its_period() {
136 assert!((pulse(0.0, 2.0) - 1.0).abs() < 1e-6);
137 assert!((pulse(0.5, 2.0) - 0.75).abs() < 1e-6);
138 assert!((pulse(1.0, 2.0) - BLINK_MIN).abs() < 1e-6);
139 assert!((pulse(2.0, 2.0) - 1.0).abs() < 1e-6);
140 }
141
142 proptest! {
143 #[test]
144 fn never_leaves_the_map(
145 w in 1u16..20,
146 h in 1u16..20,
147 steps in prop::collection::vec(0usize..4, 0..60),
148 ) {
149 let mut c = Cursor::new(Pos::new(0, 0));
150 for s in steps {
151 c.step(MOVES[s], w, h);
152 prop_assert!((0..i32::from(w)).contains(&c.pos.x));
153 prop_assert!((0..i32::from(h)).contains(&c.pos.y));
154 }
155 }
156
157 #[test]
158 fn brightness_stays_between_min_and_full(t in -10.0f32..10.0) {
159 let mut c = Cursor::new(Pos::new(0, 0));
160 c.tick(t);
161 let b = c.brightness();
162 prop_assert!((BLINK_MIN - 1e-6..=1.0 + 1e-6).contains(&b), "{b}");
163 }
164 }
165}