Skip to main content

vimp_engine_core/client/
raycast.rs

1//! 2D raycast-примитивы для клиентского предсказания выстрелов — порт
2//! src/lib/raycast.js (срез 2.6): приближённая реплика авторитетного
3//! world.cast_ray по данным, которые уже есть у клиента (тайловая сетка
4//! стен, прямоугольники динамики карты и танков).
5
6/// Повёрнутый прямоугольник (OBB) — общее представление геометрии для
7/// slab-теста луча и SAT-контактов (`client::collision`).
8#[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
17/// Луч против тайловой сетки стен (DDA-обход клеток).
18/// Возвращает дистанцию до стены или None (промах в пределах range).
19pub 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    // старт внутри стены — попадание в упор
46    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    // дистанция вдоль луча на пересечение одной клетки по каждой оси
54    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    // дистанция вдоль луча до первой границы клетки по каждой оси
66    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
114/// Луч против повёрнутого прямоугольника (slab-тест в локальном фрейме OBB).
115/// Возвращает дистанцию до ближней грани или None (промах).
116pub fn ray_vs_box(origin: [f32; 2], dir: [f32; 2], range: f32, b: &Box2) -> Option<f32> {
117    // перевод луча в локальный фрейм бокса (поворот на −angle)
118    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    // slab-тест по каждой оси AABB [-half, +half]
129    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    // сетка 5×5 со стеной (1) в колонке 3; тайл 10 юнитов
166    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        // старт в центре тайла (1,1), луч вправо: стена начинается на x=30
180        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]; // стена — строка 3 (y от 30)
214
215        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        // до y=30 по диагонали: 25/sin(45°) ≈ 35.36
219        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        // бокс 20×2, повёрнутый на 90°: по лучу видна узкая грань
281        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}