1use shuyi_core::Pos;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub struct Camera {
11 pub origin: Pos,
13}
14
15impl Camera {
16 pub const MARGIN: i32 = 3;
18
19 pub fn centred_on(target: Pos, map_w: u16, map_h: u16, view: (i32, i32)) -> Self {
22 let origin = Pos::new(
23 clamp_axis(target.x - view.0 / 2, map_w, view.0),
24 clamp_axis(target.y - view.1 / 2, map_h, view.1),
25 );
26 Self { origin }
27 }
28
29 pub fn follow(&mut self, target: Pos, map_w: u16, map_h: u16, view: (i32, i32), margin: i32) {
35 self.origin = Pos::new(
36 follow_axis(self.origin.x, target.x, map_w, view.0, margin),
37 follow_axis(self.origin.y, target.y, map_h, view.1, margin),
38 );
39 }
40}
41
42fn follow_axis(origin: i32, target: i32, map_len: u16, view: i32, margin: i32) -> i32 {
44 let margin = margin.clamp(0, (view - 1) / 2);
45 let lowest = target - (view - 1 - margin);
46 let highest = target - margin;
47 clamp_axis(origin.clamp(lowest, highest), map_len, view)
48}
49
50fn clamp_axis(origin: i32, map_len: u16, view: i32) -> i32 {
53 let map_len = i32::from(map_len);
54 if map_len <= view {
55 -((view - map_len) / 2)
56 } else {
57 origin.clamp(0, map_len - view)
58 }
59}
60
61#[cfg(test)]
62mod tests {
63 use proptest::prelude::*;
64
65 use super::*;
66
67 const VIEW: (i32, i32) = (35, 30);
70
71 fn visible(tile: Pos, c: Camera, view: (i32, i32)) -> bool {
73 (c.origin.x..c.origin.x + view.0).contains(&tile.x)
74 && (c.origin.y..c.origin.y + view.1).contains(&tile.y)
75 }
76
77 fn cam(x: i32, y: i32) -> Camera {
78 Camera {
79 origin: Pos::new(x, y),
80 }
81 }
82
83 fn followed(from: Camera, target: Pos, w: u16, h: u16) -> Pos {
84 let mut c = from;
85 c.follow(target, w, h, VIEW, Camera::MARGIN);
86 c.origin
87 }
88
89 #[test]
90 fn small_map_is_centred() {
91 for target in [Pos::new(0, 0), Pos::new(13, 7), Pos::new(6, 3)] {
93 assert_eq!(followed(cam(0, 0), target, 14, 8), Pos::new(-10, -11));
94 assert_eq!(followed(cam(5, 9), target, 14, 8), Pos::new(-10, -11));
95 }
96 assert_eq!(
97 Camera::centred_on(Pos::new(0, 0), 14, 8, VIEW),
98 cam(-10, -11)
99 );
100 assert_eq!(
102 followed(cam(3, 3), Pos::new(34, 29), 35, 30),
103 Pos::new(0, 0)
104 );
105 assert_eq!(
107 followed(cam(0, 0), Pos::new(0, 50), 34, 64),
108 Pos::new(0, 24)
109 );
110 }
111
112 #[test]
113 fn large_map_clamps_at_every_edge() {
114 let (w, h) = (64, 40);
115 assert_eq!(followed(cam(10, 5), Pos::new(0, 0), w, h), Pos::new(0, 0));
117 assert_eq!(
119 followed(cam(0, 0), Pos::new(63, 39), w, h),
120 Pos::new(29, 10)
121 );
122 assert_eq!(followed(cam(0, 5), Pos::new(63, 0), w, h), Pos::new(29, 0));
124 assert_eq!(followed(cam(20, 0), Pos::new(0, 39), w, h), Pos::new(0, 10));
125 assert_eq!(
127 followed(cam(-5, 99), Pos::new(10, 20), w, h),
128 Pos::new(0, 10)
129 );
130 let centred = |x, y| Camera::centred_on(Pos::new(x, y), w, h, VIEW);
131 assert_eq!(centred(63, 39), cam(29, 10));
132 assert_eq!(centred(0, 0), cam(0, 0));
133 assert_eq!(centred(32, 20), cam(15, 5));
134 }
135
136 #[test]
137 fn margin_is_respected() {
138 let (w, h) = (64, 64);
139 let start = cam(10, 10);
140 assert_eq!(followed(start, Pos::new(13, 13), w, h), Pos::new(10, 10));
142 assert_eq!(followed(start, Pos::new(41, 36), w, h), Pos::new(10, 10));
143 assert_eq!(followed(start, Pos::new(12, 12), w, h), Pos::new(9, 9));
145 assert_eq!(followed(start, Pos::new(42, 37), w, h), Pos::new(11, 11));
146 assert_eq!(followed(start, Pos::new(60, 3), w, h), Pos::new(29, 0));
148 let mut c = start;
150 c.follow(Pos::new(10, 10), w, h, VIEW, 0);
151 assert_eq!(c.origin, Pos::new(10, 10));
152 c.follow(Pos::new(30, 30), w, h, VIEW, 99);
153 assert_eq!(c.origin, Pos::new(13, 15));
155 c.follow(Pos::new(30, 30), w, h, VIEW, -4);
156 assert_eq!(c.origin, Pos::new(13, 15));
157 }
158
159 #[test]
160 fn the_view_size_is_the_callers() {
161 let view = (17, 15);
163 let centred = |x, y, w, h| Camera::centred_on(Pos::new(x, y), w, h, view);
164 assert_eq!(centred(32, 20, 64, 40), cam(24, 13));
165 assert_eq!(centred(63, 39, 64, 40), cam(47, 25));
166 assert_eq!(centred(0, 0, 64, 40), cam(0, 0));
167 assert_eq!(centred(3, 5, 14, 8), cam(-1, -3));
170 assert_eq!(
172 Camera::centred_on(Pos::new(32, 20), 64, 40, (10, 4)),
173 cam(27, 18)
174 );
175 let mut c = cam(0, 0);
176 c.follow(Pos::new(20, 20), 64, 40, view, 3);
177 assert_eq!(c.origin, Pos::new(7, 9));
178 c.follow(Pos::new(8, 10), 64, 40, view, 3);
179 assert_eq!(c.origin, Pos::new(5, 7));
180 assert!(visible(Pos::new(8, 10), c, view));
181 assert!(!visible(Pos::new(22, 10), c, view));
182 assert!(!visible(Pos::new(8, 22), c, view));
183 assert!(!visible(Pos::new(4, 10), c, view));
184 assert!(!visible(Pos::new(8, 6), c, view));
185 }
186
187 proptest! {
188 #[test]
189 fn follow_keeps_target_visible_and_map_filling_the_view(
190 ox in -100..100i32, oy in -100..100i32,
191 tx in 0..64i32, ty in 0..64i32,
192 w in 1u16..65, h in 1u16..65, margin in 0..10i32,
193 view in (1..40i32, 1..40i32),
194 ) {
195 let target = Pos::new(tx.min(i32::from(w) - 1), ty.min(i32::from(h) - 1));
196 let mut c = cam(ox, oy);
197 c.follow(target, w, h, view, margin);
198 prop_assert!(visible(target, c, view));
199 for (o, len, view) in [(c.origin.x, w, view.0), (c.origin.y, h, view.1)] {
200 let len = i32::from(len);
201 if len > view {
202 prop_assert!(o >= 0 && o + view <= len);
203 } else {
204 prop_assert_eq!(o, -((view - len) / 2));
205 }
206 }
207 }
208 }
209}