Skip to main content

vimp_engine_core/nav/
navigation.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::nav::pathfinder::{self, Edge};
6use crate::rng::Rng;
7
8// коэффициент шага сетки
9const COEF_GRID_STEP: f32 = 2.0;
10
11/// Навигация ботов: сетка проходимости + граф с A*
12/// (порт src/server/modules/bots/NavigationSystem.js).
13#[derive(Clone, Default, Serialize, Deserialize)]
14pub struct NavigationSystem {
15    nav_grid: Vec<Vec<u8>>,
16    grid_step: f32,
17    nodes: Vec<[f32; 2]>,
18    edges: Vec<Vec<Edge>>,
19    node_grid: HashMap<(i32, i32), Vec<usize>>,
20    node_grid_cell_size: f32,
21}
22
23impl NavigationSystem {
24    /// Строит сетку проходимости и навигационный граф из данных карты
25    /// (сетка тайлов + масштабированный step + список статичных тайлов).
26    pub fn generate(grid: &[Vec<i32>], physics_static: &[i32], step: f32) -> Self {
27        let mut nav = Self::default();
28
29        if grid.is_empty() || step <= 0.0 {
30            return nav;
31        }
32
33        nav.grid_step = step;
34        nav.nav_grid = grid
35            .iter()
36            .map(|row| {
37                row.iter()
38                    .map(|tile| u8::from(physics_static.contains(tile)))
39                    .collect()
40            })
41            .collect();
42
43        let node_placement_step = step * COEF_GRID_STEP;
44        let map_width = nav.nav_grid[0].len() as f32 * step;
45        let map_height = nav.nav_grid.len() as f32 * step;
46
47        // расстановка узлов в свободных местах
48        let mut x = node_placement_step / 2.0;
49
50        while x < map_width {
51            let mut y = node_placement_step / 2.0;
52
53            while y < map_height {
54                if nav.is_walkable(x, y) {
55                    nav.nodes.push([x, y]);
56                }
57
58                y += node_placement_step;
59            }
60
61            x += node_placement_step;
62        }
63
64        // соединение ближайших видимых узлов рёбрами
65        let max_connection_dist_sq =
66            node_placement_step * 1.5 * (node_placement_step * 1.5);
67
68        nav.edges = vec![Vec::new(); nav.nodes.len()];
69
70        for i in 0..nav.nodes.len() {
71            for j in (i + 1)..nav.nodes.len() {
72                let dx = nav.nodes[i][0] - nav.nodes[j][0];
73                let dy = nav.nodes[i][1] - nav.nodes[j][1];
74                let dist_sq = dx * dx + dy * dy;
75
76                if dist_sq <= max_connection_dist_sq
77                    && !nav.has_obstacle_between(nav.nodes[i], nav.nodes[j])
78                {
79                    let distance = dist_sq.sqrt();
80
81                    nav.edges[i].push(Edge {
82                        node: j,
83                        weight: distance,
84                    });
85                    nav.edges[j].push(Edge {
86                        node: i,
87                        weight: distance,
88                    });
89                }
90            }
91        }
92
93        // сетка для быстрого поиска ближайших узлов
94        nav.node_grid_cell_size = node_placement_step;
95
96        for (index, node) in nav.nodes.iter().enumerate() {
97            let cx = (node[0] / nav.node_grid_cell_size).floor() as i32;
98            let cy = (node[1] / nav.node_grid_cell_size).floor() as i32;
99
100            nav.node_grid.entry((cx, cy)).or_default().push(index);
101        }
102
103        nav
104    }
105
106    pub fn has_nodes(&self) -> bool {
107        !self.nodes.is_empty()
108    }
109
110    /// Случайный узел графа (цель патрулирования).
111    pub fn random_node(&self, rng: &mut Rng) -> Option<[f32; 2]> {
112        if self.nodes.is_empty() {
113            return None;
114        }
115
116        let index = (rng.next_f32() * self.nodes.len() as f32).floor() as usize;
117
118        self.nodes.get(index).copied()
119    }
120
121    /// Проходима ли точка в мировых координатах.
122    pub fn is_walkable(&self, x: f32, y: f32) -> bool {
123        if self.nav_grid.is_empty() || self.grid_step == 0.0 {
124            return false;
125        }
126
127        let grid_x = (x / self.grid_step).floor();
128        let grid_y = (y / self.grid_step).floor();
129
130        if grid_x < 0.0 || grid_y < 0.0 {
131            return false;
132        }
133
134        self.nav_grid
135            .get(grid_y as usize)
136            .and_then(|row| row.get(grid_x as usize))
137            .is_some_and(|&cell| cell == 0)
138    }
139
140    /// Быстрая линия видимости по сетке (алгоритм Брезенхэма):
141    /// true — на пути есть препятствие.
142    pub fn has_obstacle_between(&self, start: [f32; 2], end: [f32; 2]) -> bool {
143        let mut x0 = (start[0] / self.grid_step).floor() as i64;
144        let mut y0 = (start[1] / self.grid_step).floor() as i64;
145        let x1 = (end[0] / self.grid_step).floor() as i64;
146        let y1 = (end[1] / self.grid_step).floor() as i64;
147
148        let dx = (x1 - x0).abs();
149        let dy = -(y1 - y0).abs();
150        let sx = if x0 < x1 { 1 } else { -1 };
151        let sy = if y0 < y1 { 1 } else { -1 };
152        let mut err = dx + dy;
153
154        loop {
155            let is_wall = y0 >= 0
156                && x0 >= 0
157                && self
158                    .nav_grid
159                    .get(y0 as usize)
160                    .and_then(|row| row.get(x0 as usize))
161                    .is_some_and(|&cell| cell == 1);
162
163            if is_wall {
164                return true;
165            }
166
167            if x0 == x1 && y0 == y1 {
168                break;
169            }
170
171            let e2 = 2 * err;
172
173            if e2 >= dy {
174                err += dy;
175                x0 += sx;
176            }
177
178            if e2 <= dx {
179                err += dx;
180                y0 += sy;
181            }
182        }
183
184        false
185    }
186
187    /// Путь из точки в точку (мировые координаты) или None.
188    pub fn find_path(&self, start: [f32; 2], end: [f32; 2]) -> Option<Vec<[f32; 2]>> {
189        if self.nodes.is_empty() {
190            return None;
191        }
192
193        if !self.has_obstacle_between(start, end) {
194            return Some(vec![end]);
195        }
196
197        let start_node = self.closest_visible_node(start)?;
198        let end_node = self.closest_visible_node(end)?;
199
200        if start_node == end_node {
201            return None;
202        }
203
204        let path_indexes = pathfinder::find_path(start_node, end_node, &self.nodes, &self.edges)?;
205
206        let mut path: Vec<[f32; 2]> = path_indexes
207            .into_iter()
208            .map(|index| self.nodes[index])
209            .collect();
210
211        path.push(end);
212
213        Some(path)
214    }
215
216    /// Ближайший видимый узел к мировой позиции (поиск по 9 ячейкам).
217    fn closest_visible_node(&self, position: [f32; 2]) -> Option<usize> {
218        if self.nodes.is_empty() || self.node_grid_cell_size == 0.0 {
219            return None;
220        }
221
222        let center_cx = (position[0] / self.node_grid_cell_size).floor() as i32;
223        let center_cy = (position[1] / self.node_grid_cell_size).floor() as i32;
224        let mut candidates: Vec<usize> = Vec::new();
225
226        for cy in (center_cy - 1)..=(center_cy + 1) {
227            for cx in (center_cx - 1)..=(center_cx + 1) {
228                if let Some(cell) = self.node_grid.get(&(cx, cy)) {
229                    candidates.extend_from_slice(cell);
230                }
231            }
232        }
233
234        let mut closest: Option<usize> = None;
235        let mut min_distance_sq = f32::INFINITY;
236
237        for index in candidates {
238            let node = self.nodes[index];
239
240            if !self.has_obstacle_between(position, node) {
241                let dx = position[0] - node[0];
242                let dy = position[1] - node[1];
243                let distance_sq = dx * dx + dy * dy;
244
245                if distance_sq < min_distance_sq {
246                    min_distance_sq = distance_sq;
247                    closest = Some(index);
248                }
249            }
250        }
251
252        closest
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    // карта 6×6: стены по периметру
261    fn walled_grid() -> Vec<Vec<i32>> {
262        vec![
263            vec![1, 1, 1, 1, 1, 1],
264            vec![1, 0, 0, 0, 0, 1],
265            vec![1, 0, 0, 0, 0, 1],
266            vec![1, 0, 0, 0, 0, 1],
267            vec![1, 0, 0, 0, 0, 1],
268            vec![1, 1, 1, 1, 1, 1],
269        ]
270    }
271
272    #[test]
273    fn walkable_inside_not_on_walls() {
274        let nav = NavigationSystem::generate(&walled_grid(), &[1], 10.0);
275
276        assert!(nav.is_walkable(25.0, 25.0));
277        assert!(!nav.is_walkable(5.0, 5.0)); // стена
278        assert!(!nav.is_walkable(-5.0, 25.0)); // за пределами
279    }
280
281    #[test]
282    fn line_of_sight_blocked_by_wall() {
283        let grid = vec![
284            vec![0, 0, 0],
285            vec![0, 1, 0],
286            vec![0, 0, 0],
287        ];
288        let nav = NavigationSystem::generate(&grid, &[1], 10.0);
289
290        // через центр (стена)
291        assert!(nav.has_obstacle_between([5.0, 5.0], [25.0, 25.0]));
292        // вдоль свободного края
293        assert!(!nav.has_obstacle_between([5.0, 5.0], [25.0, 5.0]));
294    }
295
296    #[test]
297    fn direct_path_when_visible() {
298        let nav = NavigationSystem::generate(&walled_grid(), &[1], 10.0);
299        let path = nav.find_path([15.0, 15.0], [45.0, 45.0]).unwrap();
300
301        assert_eq!(path, vec![[45.0, 45.0]]);
302    }
303}