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) для slab-теста.
7pub 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
15/// Луч против тайловой сетки стен (DDA-обход клеток).
16/// Возвращает дистанцию до стены или None (промах в пределах range).
17pub 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    // старт внутри стены — попадание в упор
44    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    // дистанция вдоль луча на пересечение одной клетки по каждой оси
52    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    // дистанция вдоль луча до первой границы клетки по каждой оси
64    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
112/// Луч против повёрнутого прямоугольника (slab-тест в локальном фрейме OBB).
113/// Возвращает дистанцию до ближней грани или None (промах).
114pub fn ray_vs_box(origin: [f32; 2], dir: [f32; 2], range: f32, b: &Box2) -> Option<f32> {
115    // перевод луча в локальный фрейм бокса (поворот на −angle)
116    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    // slab-тест по каждой оси AABB [-half, +half]
127    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    // сетка 5×5 со стеной (1) в колонке 3; тайл 10 юнитов
164    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        // старт в центре тайла (1,1), луч вправо: стена начинается на x=30
178        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]; // стена — строка 3 (y от 30)
212
213        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        // до y=30 по диагонали: 25/sin(45°) ≈ 35.36
217        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        // бокс 20×2, повёрнутый на 90°: по лучу видна узкая грань
279        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}