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    // ***** отладочный дамп (crate::debug) ***** //
111
112    pub fn node_count(&self) -> usize {
113        self.nodes.len()
114    }
115
116    pub fn edge_count(&self) -> usize {
117        self.edges.iter().map(|edges| edges.len()).sum()
118    }
119
120    pub fn grid_step(&self) -> f32 {
121        self.grid_step
122    }
123
124    /// Случайный узел графа (цель патрулирования).
125    pub fn random_node(&self, rng: &mut Rng) -> Option<[f32; 2]> {
126        if self.nodes.is_empty() {
127            return None;
128        }
129
130        let index = (rng.next_f32() * self.nodes.len() as f32).floor() as usize;
131
132        self.nodes.get(index).copied()
133    }
134
135    /// Проходима ли точка в мировых координатах.
136    pub fn is_walkable(&self, x: f32, y: f32) -> bool {
137        if self.nav_grid.is_empty() || self.grid_step == 0.0 {
138            return false;
139        }
140
141        let grid_x = (x / self.grid_step).floor();
142        let grid_y = (y / self.grid_step).floor();
143
144        if grid_x < 0.0 || grid_y < 0.0 {
145            return false;
146        }
147
148        self.nav_grid
149            .get(grid_y as usize)
150            .and_then(|row| row.get(grid_x as usize))
151            .is_some_and(|&cell| cell == 0)
152    }
153
154    /// Быстрая линия видимости по сетке (алгоритм Брезенхэма):
155    /// true — на пути есть препятствие.
156    pub fn has_obstacle_between(&self, start: [f32; 2], end: [f32; 2]) -> bool {
157        let mut x0 = (start[0] / self.grid_step).floor() as i64;
158        let mut y0 = (start[1] / self.grid_step).floor() as i64;
159        let x1 = (end[0] / self.grid_step).floor() as i64;
160        let y1 = (end[1] / self.grid_step).floor() as i64;
161
162        let dx = (x1 - x0).abs();
163        let dy = -(y1 - y0).abs();
164        let sx = if x0 < x1 { 1 } else { -1 };
165        let sy = if y0 < y1 { 1 } else { -1 };
166        let mut err = dx + dy;
167
168        loop {
169            let is_wall = y0 >= 0
170                && x0 >= 0
171                && self
172                    .nav_grid
173                    .get(y0 as usize)
174                    .and_then(|row| row.get(x0 as usize))
175                    .is_some_and(|&cell| cell == 1);
176
177            if is_wall {
178                return true;
179            }
180
181            if x0 == x1 && y0 == y1 {
182                break;
183            }
184
185            let e2 = 2 * err;
186
187            if e2 >= dy {
188                err += dy;
189                x0 += sx;
190            }
191
192            if e2 <= dx {
193                err += dx;
194                y0 += sy;
195            }
196        }
197
198        false
199    }
200
201    /// Путь из точки в точку (мировые координаты) или None.
202    pub fn find_path(&self, start: [f32; 2], end: [f32; 2]) -> Option<Vec<[f32; 2]>> {
203        if self.nodes.is_empty() {
204            return None;
205        }
206
207        if !self.has_obstacle_between(start, end) {
208            return Some(vec![end]);
209        }
210
211        let start_node = self.closest_visible_node(start)?;
212        let end_node = self.closest_visible_node(end)?;
213
214        if start_node == end_node {
215            return None;
216        }
217
218        let path_indexes = pathfinder::find_path(start_node, end_node, &self.nodes, &self.edges)?;
219
220        let mut path: Vec<[f32; 2]> = path_indexes
221            .into_iter()
222            .map(|index| self.nodes[index])
223            .collect();
224
225        path.push(end);
226
227        Some(path)
228    }
229
230    /// Ближайший видимый узел к мировой позиции (поиск по 9 ячейкам).
231    fn closest_visible_node(&self, position: [f32; 2]) -> Option<usize> {
232        if self.nodes.is_empty() || self.node_grid_cell_size == 0.0 {
233            return None;
234        }
235
236        let center_cx = (position[0] / self.node_grid_cell_size).floor() as i32;
237        let center_cy = (position[1] / self.node_grid_cell_size).floor() as i32;
238        let mut candidates: Vec<usize> = Vec::new();
239
240        for cy in (center_cy - 1)..=(center_cy + 1) {
241            for cx in (center_cx - 1)..=(center_cx + 1) {
242                if let Some(cell) = self.node_grid.get(&(cx, cy)) {
243                    candidates.extend_from_slice(cell);
244                }
245            }
246        }
247
248        let mut closest: Option<usize> = None;
249        let mut min_distance_sq = f32::INFINITY;
250
251        for index in candidates {
252            let node = self.nodes[index];
253
254            if !self.has_obstacle_between(position, node) {
255                let dx = position[0] - node[0];
256                let dy = position[1] - node[1];
257                let distance_sq = dx * dx + dy * dy;
258
259                if distance_sq < min_distance_sq {
260                    min_distance_sq = distance_sq;
261                    closest = Some(index);
262                }
263            }
264        }
265
266        closest
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273
274    // карта 6×6: стены по периметру
275    fn walled_grid() -> Vec<Vec<i32>> {
276        vec![
277            vec![1, 1, 1, 1, 1, 1],
278            vec![1, 0, 0, 0, 0, 1],
279            vec![1, 0, 0, 0, 0, 1],
280            vec![1, 0, 0, 0, 0, 1],
281            vec![1, 0, 0, 0, 0, 1],
282            vec![1, 1, 1, 1, 1, 1],
283        ]
284    }
285
286    #[test]
287    fn walkable_inside_not_on_walls() {
288        let nav = NavigationSystem::generate(&walled_grid(), &[1], 10.0);
289
290        assert!(nav.is_walkable(25.0, 25.0));
291        assert!(!nav.is_walkable(5.0, 5.0)); // стена
292        assert!(!nav.is_walkable(-5.0, 25.0)); // за пределами
293    }
294
295    #[test]
296    fn line_of_sight_blocked_by_wall() {
297        let grid = vec![
298            vec![0, 0, 0],
299            vec![0, 1, 0],
300            vec![0, 0, 0],
301        ];
302        let nav = NavigationSystem::generate(&grid, &[1], 10.0);
303
304        // через центр (стена)
305        assert!(nav.has_obstacle_between([5.0, 5.0], [25.0, 25.0]));
306        // вдоль свободного края
307        assert!(!nav.has_obstacle_between([5.0, 5.0], [25.0, 5.0]));
308    }
309
310    #[test]
311    fn direct_path_when_visible() {
312        let nav = NavigationSystem::generate(&walled_grid(), &[1], 10.0);
313        let path = nav.find_path([15.0, 15.0], [45.0, 45.0]).unwrap();
314
315        assert_eq!(path, vec![[45.0, 45.0]]);
316    }
317}