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