1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5use crate::nav::pathfinder::{self, Edge};
6use crate::rng::Rng;
7
8const COEF_GRID_STEP: f32 = 2.0;
10
11#[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 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 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 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 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 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 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 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 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 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 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)); assert!(!nav.is_walkable(-5.0, 25.0)); }
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 assert!(nav.has_obstacle_between([5.0, 5.0], [25.0, 25.0]));
292 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}