1#[derive(Clone, Copy, Debug)]
9pub struct Box2 {
10 pub x: f32,
11 pub y: f32,
12 pub angle: f32,
13 pub half_w: f32,
14 pub half_h: f32,
15}
16
17pub fn ray_vs_grid(
20 origin: [f32; 2],
21 dir: [f32; 2],
22 range: f32,
23 map: &[Vec<i32>],
24 solid_tiles: &[i32],
25 tile_size: f32,
26) -> Option<f32> {
27 let rows = map.len();
28 let cols = map.first().map(|row| row.len()).unwrap_or(0);
29
30 if rows == 0 || cols == 0 || solid_tiles.is_empty() {
31 return None;
32 }
33
34 let is_solid = |cx: i64, cy: i64| {
35 cy >= 0
36 && (cy as usize) < rows
37 && cx >= 0
38 && (cx as usize) < cols
39 && solid_tiles.contains(&map[cy as usize][cx as usize])
40 };
41
42 let mut cell_x = (origin[0] / tile_size).floor() as i64;
43 let mut cell_y = (origin[1] / tile_size).floor() as i64;
44
45 if is_solid(cell_x, cell_y) {
47 return Some(0.0);
48 }
49
50 let step_x: i64 = if dir[0] > 0.0 { 1 } else { -1 };
51 let step_y: i64 = if dir[1] > 0.0 { 1 } else { -1 };
52
53 let delta_x = if dir[0] != 0.0 {
55 (tile_size / dir[0]).abs()
56 } else {
57 f32::INFINITY
58 };
59 let delta_y = if dir[1] != 0.0 {
60 (tile_size / dir[1]).abs()
61 } else {
62 f32::INFINITY
63 };
64
65 let mut max_x = if dir[0] != 0.0 {
67 let edge = if dir[0] > 0.0 {
68 (cell_x + 1) as f32 * tile_size - origin[0]
69 } else {
70 origin[0] - cell_x as f32 * tile_size
71 };
72
73 edge / dir[0].abs()
74 } else {
75 f32::INFINITY
76 };
77 let mut max_y = if dir[1] != 0.0 {
78 let edge = if dir[1] > 0.0 {
79 (cell_y + 1) as f32 * tile_size - origin[1]
80 } else {
81 origin[1] - cell_y as f32 * tile_size
82 };
83
84 edge / dir[1].abs()
85 } else {
86 f32::INFINITY
87 };
88
89 let mut traveled = 0.0f32;
90
91 while traveled <= range {
92 if max_x < max_y {
93 traveled = max_x;
94 max_x += delta_x;
95 cell_x += step_x;
96 } else {
97 traveled = max_y;
98 max_y += delta_y;
99 cell_y += step_y;
100 }
101
102 if traveled > range {
103 return None;
104 }
105
106 if is_solid(cell_x, cell_y) {
107 return Some(traveled);
108 }
109 }
110
111 None
112}
113
114pub fn ray_vs_box(origin: [f32; 2], dir: [f32; 2], range: f32, b: &Box2) -> Option<f32> {
117 let (sin, cos) = (-b.angle).sin_cos();
119 let rel_x = origin[0] - b.x;
120 let rel_y = origin[1] - b.y;
121
122 let local_origin = [cos * rel_x - sin * rel_y, sin * rel_x + cos * rel_y];
123 let local_dir = [cos * dir[0] - sin * dir[1], sin * dir[0] + cos * dir[1]];
124
125 let mut t_min = 0.0f32;
126 let mut t_max = range;
127
128 let slabs = [
130 (local_origin[0], local_dir[0], b.half_w),
131 (local_origin[1], local_dir[1], b.half_h),
132 ];
133
134 for (o, d, half) in slabs {
135 if d == 0.0 {
136 if o < -half || o > half {
137 return None;
138 }
139
140 continue;
141 }
142
143 let mut t1 = (-half - o) / d;
144 let mut t2 = (half - o) / d;
145
146 if t1 > t2 {
147 std::mem::swap(&mut t1, &mut t2);
148 }
149
150 t_min = t_min.max(t1);
151 t_max = t_max.min(t2);
152
153 if t_min > t_max {
154 return None;
155 }
156 }
157
158 Some(t_min)
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 fn wall_grid() -> Vec<Vec<i32>> {
167 let mut grid = vec![vec![0; 5]; 5];
168
169 for row in &mut grid {
170 row[3] = 1;
171 }
172
173 grid
174 }
175
176 #[test]
177 fn grid_ray_hits_wall_to_the_right() {
178 let grid = wall_grid();
179 let hit = ray_vs_grid([15.0, 15.0], [1.0, 0.0], 100.0, &grid, &[1], 10.0);
181
182 assert_eq!(hit, Some(15.0));
183 }
184
185 #[test]
186 fn grid_ray_misses_within_range() {
187 let grid = wall_grid();
188 let hit = ray_vs_grid([15.0, 15.0], [1.0, 0.0], 10.0, &grid, &[1], 10.0);
189
190 assert_eq!(hit, None);
191 }
192
193 #[test]
194 fn grid_ray_away_from_wall_misses() {
195 let grid = wall_grid();
196 let hit = ray_vs_grid([15.0, 15.0], [-1.0, 0.0], 100.0, &grid, &[1], 10.0);
197
198 assert_eq!(hit, None);
199 }
200
201 #[test]
202 fn grid_start_inside_wall_hits_at_zero() {
203 let grid = wall_grid();
204 let hit = ray_vs_grid([35.0, 15.0], [1.0, 0.0], 100.0, &grid, &[1], 10.0);
205
206 assert_eq!(hit, Some(0.0));
207 }
208
209 #[test]
210 fn grid_diagonal_and_vertical_rays() {
211 let mut grid = vec![vec![0; 5]; 5];
212
213 grid[3] = vec![1; 5]; let dir = [std::f32::consts::FRAC_1_SQRT_2, std::f32::consts::FRAC_1_SQRT_2];
216 let diagonal = ray_vs_grid([5.0, 5.0], dir, 100.0, &grid, &[1], 10.0).unwrap();
217
218 assert!((diagonal - 25.0 * std::f32::consts::SQRT_2).abs() < 0.01);
220
221 let vertical = ray_vs_grid([5.0, 5.0], [0.0, 1.0], 100.0, &grid, &[1], 10.0);
222
223 assert_eq!(vertical, Some(25.0));
224 }
225
226 #[test]
227 fn grid_without_solid_tiles_never_hits() {
228 let grid = wall_grid();
229 let hit = ray_vs_grid([15.0, 15.0], [1.0, 0.0], 100.0, &grid, &[], 10.0);
230
231 assert_eq!(hit, None);
232 }
233
234 #[test]
235 fn box_head_on_hit() {
236 let b = Box2 {
237 x: 50.0,
238 y: 0.0,
239 angle: 0.0,
240 half_w: 10.0,
241 half_h: 5.0,
242 };
243 let hit = ray_vs_box([0.0, 0.0], [1.0, 0.0], 100.0, &b);
244
245 assert_eq!(hit, Some(40.0));
246 }
247
248 #[test]
249 fn box_miss_and_out_of_range() {
250 let b = Box2 {
251 x: 50.0,
252 y: 20.0,
253 angle: 0.0,
254 half_w: 10.0,
255 half_h: 5.0,
256 };
257
258 assert_eq!(ray_vs_box([0.0, 0.0], [1.0, 0.0], 100.0, &b), None);
259
260 let near = Box2 { y: 0.0, ..b };
261
262 assert_eq!(ray_vs_box([0.0, 0.0], [1.0, 0.0], 30.0, &near), None);
263 }
264
265 #[test]
266 fn box_start_inside_hits_at_zero() {
267 let b = Box2 {
268 x: 0.0,
269 y: 0.0,
270 angle: 0.0,
271 half_w: 10.0,
272 half_h: 10.0,
273 };
274
275 assert_eq!(ray_vs_box([0.0, 0.0], [1.0, 0.0], 100.0, &b), Some(0.0));
276 }
277
278 #[test]
279 fn box_rotation_changes_hit_distance() {
280 let b = Box2 {
282 x: 50.0,
283 y: 0.0,
284 angle: std::f32::consts::FRAC_PI_2,
285 half_w: 10.0,
286 half_h: 1.0,
287 };
288 let hit = ray_vs_box([0.0, 0.0], [1.0, 0.0], 100.0, &b).unwrap();
289
290 assert!((hit - 49.0).abs() < 0.001);
291 }
292
293 #[test]
294 fn box_behind_ray_misses() {
295 let b = Box2 {
296 x: -50.0,
297 y: 0.0,
298 angle: 0.0,
299 half_w: 10.0,
300 half_h: 5.0,
301 };
302
303 assert_eq!(ray_vs_box([0.0, 0.0], [1.0, 0.0], 100.0, &b), None);
304 }
305}