1use std::cmp::Ordering;
4use std::collections::{BinaryHeap, HashMap};
5
6use crate::grid::{
7 block_circle, cell_center, clear_door_cells, collides_player_at, mark_building_footprint,
8 terrain_cost, world_to_cell, NavWorld, SEGMENT_SAMPLE_M,
9};
10use crate::z_nav::{cell_walkable_for_path, goal_z_for};
11
12#[derive(Clone, Copy, Eq, PartialEq)]
13struct OpenNode {
14 f: u32,
15 g: u32,
16 x: i16,
17 y: i16,
18}
19
20impl Ord for OpenNode {
21 fn cmp(&self, other: &Self) -> Ordering {
22 other.f.cmp(&self.f).then_with(|| other.g.cmp(&self.g))
23 }
24}
25
26impl PartialOrd for OpenNode {
27 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
28 Some(self.cmp(other))
29 }
30}
31
32struct NavGrid {
33 width: i16,
34 height: i16,
35 blocked: Vec<bool>,
36 cost: Vec<u16>,
37}
38
39impl NavGrid {
40 fn idx(&self, x: i16, y: i16) -> usize {
41 (y as usize) * (self.width as usize) + (x as usize)
42 }
43
44 fn in_bounds(&self, x: i16, y: i16) -> bool {
45 x >= 0 && y >= 0 && x < self.width && y < self.height
46 }
47
48 fn is_walkable(&self, x: i16, y: i16) -> bool {
49 self.in_bounds(x, y) && !self.blocked[self.idx(x, y)]
50 }
51
52 fn move_cost(&self, x: i16, y: i16) -> u32 {
53 self.cost[self.idx(x, y)] as u32
54 }
55
56 fn set_blocked(&mut self, x: i16, y: i16, blocked: bool) {
57 if self.in_bounds(x, y) {
58 let idx = self.idx(x, y);
59 self.blocked[idx] = blocked;
60 }
61 }
62}
63
64fn build_grid(world: &NavWorld, player_z: f32, goal_z: f32) -> NavGrid {
65 let width = world.world_width_m.max(1.0).ceil() as i16;
66 let height = world.world_height_m.max(1.0).ceil() as i16;
67 let len = (width as usize) * (height as usize);
68 let mut blocked = vec![false; len];
69 let mut cost = vec![10u16; len];
70
71 for y in 0..height {
72 for x in 0..width {
73 let cx = x as f32 + 0.5;
74 let cy = y as f32 + 0.5;
75 let kind = world.terrain_kind_at(cx, cy);
76 let tc = terrain_cost(kind);
77 let idx = (y as usize) * (width as usize) + (x as usize);
78 cost[idx] = tc;
79 if tc == u16::MAX {
80 blocked[idx] = true;
81 }
82 }
83 }
84
85 let mut grid = NavGrid {
86 width,
87 height,
88 blocked,
89 cost,
90 };
91
92 for circle in &world.circles {
93 block_circle(
94 &mut grid.blocked,
95 grid.width,
96 grid.height,
97 circle.x,
98 circle.y,
99 circle.radius_m,
100 );
101 }
102
103 for building in &world.buildings {
104 mark_building_footprint(&mut grid.blocked, grid.width, grid.height, building);
105 }
106
107 clear_door_cells(&mut grid.blocked, grid.width, grid.height, &world.doors);
108
109 if !world.z_platforms.is_empty() || !world.z_transitions.is_empty() {
111 for y in 0..height {
112 for x in 0..width {
113 let cx = x as f32 + 0.5;
114 let cy = y as f32 + 0.5;
115 if !cell_walkable_for_path(world, cx, cy, player_z, goal_z) {
116 grid.set_blocked(x, y, true);
117 }
118 }
119 }
120 }
121
122 grid
123}
124
125fn heuristic(ax: i16, ay: i16, bx: i16, by: i16) -> u32 {
126 let dx = (ax - bx).unsigned_abs() as u32;
127 let dy = (ay - by).unsigned_abs() as u32;
128 let diag = dx.min(dy);
129 let straight = dx.max(dy) - diag;
130 diag * 14 + straight * 10
131}
132
133fn line_clear(grid: &NavGrid, from: (i16, i16), to: (i16, i16)) -> bool {
134 let (mut x0, mut y0) = from;
135 let (x1, y1) = to;
136 let dx = (x1 - x0).abs();
137 let dy = (y1 - y0).abs();
138 let sx = if x0 < x1 { 1 } else { -1 };
139 let sy = if y0 < y1 { 1 } else { -1 };
140 let mut err = dx - dy;
141 loop {
142 if !grid.is_walkable(x0, y0) {
143 return false;
144 }
145 if x0 == x1 && y0 == y1 {
146 break;
147 }
148 let e2 = err * 2;
149 if e2 > -dy {
150 err -= dy;
151 x0 += sx;
152 }
153 if e2 < dx {
154 err += dx;
155 y0 += sy;
156 }
157 }
158 true
159}
160
161fn simplify_path(
162 grid: &NavGrid,
163 world: &NavWorld,
164 came_from: &HashMap<(i16, i16), (i16, i16)>,
165 start: (i16, i16),
166 goal: (i16, i16),
167 goal_center: (f32, f32),
168) -> Vec<(f32, f32)> {
169 let mut cells = vec![goal];
170 let mut current = goal;
171 while current != start {
172 let Some(&prev) = came_from.get(¤t) else {
173 break;
174 };
175 cells.push(prev);
176 current = prev;
177 }
178 cells.reverse();
179
180 if cells.is_empty() {
181 return vec![goal_center];
182 }
183
184 let mut waypoints: Vec<(i16, i16)> = Vec::new();
185 let mut anchor = 0usize;
186 waypoints.push(cells[0]);
187 for i in 1..cells.len() {
188 if i + 1 < cells.len() {
189 let from = cell_center(cells[anchor].0, cells[anchor].1);
190 let to = if cells[i + 1] == goal {
191 goal_center
192 } else {
193 cell_center(cells[i + 1].0, cells[i + 1].1)
194 };
195 if line_clear(grid, cells[anchor], cells[i + 1])
196 && segment_clear_world(grid, world, from, to)
197 {
198 continue;
199 }
200 }
201 waypoints.push(cells[i]);
202 anchor = i;
203 }
204
205 let mut out: Vec<(f32, f32)> = waypoints.iter().map(|&(x, y)| cell_center(x, y)).collect();
206 if let Some(last) = out.last_mut() {
207 *last = goal_center;
208 }
209
210 if path_segments_clear(grid, world, &out) {
211 return out;
212 }
213
214 let mut fallback: Vec<(f32, f32)> = cells.iter().map(|&(x, y)| cell_center(x, y)).collect();
215 if let Some(last) = fallback.last_mut() {
216 *last = goal_center;
217 }
218 fallback
219}
220
221fn path_segments_clear(grid: &NavGrid, world: &NavWorld, path: &[(f32, f32)]) -> bool {
222 path.windows(2)
223 .all(|w| segment_clear_world(grid, world, w[0], w[1]))
224}
225
226fn segment_clear_world(grid: &NavGrid, world: &NavWorld, from: (f32, f32), to: (f32, f32)) -> bool {
227 let (fx, fy) = from;
228 let (tx, ty) = to;
229 let dist = (tx - fx).hypot(ty - fy);
230 let steps = (dist / SEGMENT_SAMPLE_M).ceil() as u32 + 1;
231 for step in 0..=steps {
232 let t = step as f32 / steps as f32;
233 let x = fx + (tx - fx) * t;
234 let y = fy + (ty - fy) * t;
235 if collides_player_at(x, y, world) {
236 return false;
237 }
238 }
239 let (cx0, cy0) = world_to_cell(fx, fy);
240 let (cx1, cy1) = world_to_cell(tx, ty);
241 line_clear(grid, (cx0, cy0), (cx1, cy1))
242}
243
244pub fn find_path(
246 world: &NavWorld,
247 from_x: f32,
248 from_y: f32,
249 from_z: f32,
250 to_x: f32,
251 to_y: f32,
252) -> Option<Vec<(f32, f32)>> {
253 let to_z = goal_z_for(world, to_x, to_y, from_z);
254 find_path_with_goal_z(world, from_x, from_y, from_z, to_x, to_y, to_z)
255}
256
257pub fn find_path_with_goal_z(
258 world: &NavWorld,
259 from_x: f32,
260 from_y: f32,
261 from_z: f32,
262 to_x: f32,
263 to_y: f32,
264 to_z: f32,
265) -> Option<Vec<(f32, f32)>> {
266 let grid = build_grid(world, from_z, to_z);
267 let (sx, sy) = world_to_cell(from_x, from_y);
268 let (gx, gy) = world_to_cell(to_x, to_y);
269
270 if !grid.in_bounds(sx, sy) || !grid.in_bounds(gx, gy) {
271 return None;
272 }
273
274 let mut goal_x = gx;
275 let mut goal_y = gy;
276 if !grid.is_walkable(goal_x, goal_y) {
277 let mut found = None;
278 'search: for radius in 1..=16i16 {
279 for dy in -radius..=radius {
280 for dx in -radius..=radius {
281 if dx.abs() != radius && dy.abs() != radius {
282 continue;
283 }
284 let x = gx + dx;
285 let y = gy + dy;
286 if grid.is_walkable(x, y) {
287 found = Some((x, y));
288 break 'search;
289 }
290 }
291 }
292 }
293 let (x, y) = found?;
294 goal_x = x;
295 goal_y = y;
296 }
297
298 let goal_key = (goal_x, goal_y);
299
300 let mut start_x = sx;
301 let mut start_y = sy;
302 if !grid.is_walkable(start_x, start_y) {
303 let mut found = None;
304 'start: for radius in 1..=16i16 {
305 for dy in -radius..=radius {
306 for dx in -radius..=radius {
307 if dx.abs() != radius && dy.abs() != radius {
308 continue;
309 }
310 let x = sx + dx;
311 let y = sy + dy;
312 if grid.is_walkable(x, y) {
313 found = Some((x, y));
314 break 'start;
315 }
316 }
317 }
318 }
319 let (x, y) = found?;
320 start_x = x;
321 start_y = y;
322 }
323 let start_key = (start_x, start_y);
324
325 if start_key == goal_key {
326 return Some(vec![cell_center(goal_x, goal_y)]);
327 }
328
329 let mut open = BinaryHeap::new();
330 let mut g_score: HashMap<(i16, i16), u32> = HashMap::new();
331 let mut came_from: HashMap<(i16, i16), (i16, i16)> = HashMap::new();
332
333 g_score.insert(start_key, 0);
334 open.push(OpenNode {
335 f: heuristic(start_x, start_y, goal_x, goal_y),
336 g: 0,
337 x: start_x,
338 y: start_y,
339 });
340
341 const NEIGHBORS: [(i16, i16, u32); 8] = [
342 (1, 0, 10),
343 (-1, 0, 10),
344 (0, 1, 10),
345 (0, -1, 10),
346 (1, 1, 14),
347 (1, -1, 14),
348 (-1, 1, 14),
349 (-1, -1, 14),
350 ];
351
352 while let Some(current) = open.pop() {
353 if (current.x, current.y) == goal_key {
354 return Some(simplify_path(
355 &grid,
356 world,
357 &came_from,
358 start_key,
359 goal_key,
360 cell_center(goal_x, goal_y),
361 ));
362 }
363 let Some(&best_g) = g_score.get(&(current.x, current.y)) else {
364 continue;
365 };
366 if current.g > best_g {
367 continue;
368 }
369
370 for (dx, dy, step_base) in NEIGHBORS {
371 let nx = current.x + dx;
372 let ny = current.y + dy;
373 if !grid.is_walkable(nx, ny) {
374 continue;
375 }
376 if dx != 0 && dy != 0 {
377 if !grid.is_walkable(current.x + dx, current.y)
378 || !grid.is_walkable(current.x, current.y + dy)
379 {
380 continue;
381 }
382 }
383 let from = cell_center(current.x, current.y);
384 let to = cell_center(nx, ny);
385 if !segment_clear_world(&grid, world, from, to) {
386 continue;
387 }
388 let step = step_base * grid.move_cost(nx, ny) / 10;
389 let tentative = best_g + step;
390 let key = (nx, ny);
391 if tentative >= *g_score.get(&key).unwrap_or(&u32::MAX) {
392 continue;
393 }
394 came_from.insert(key, (current.x, current.y));
395 g_score.insert(key, tentative);
396 open.push(OpenNode {
397 f: tentative + heuristic(nx, ny, goal_x, goal_y),
398 g: tentative,
399 x: nx,
400 y: ny,
401 });
402 }
403 }
404
405 None
406}
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411
412 fn open_world() -> NavWorld {
413 NavWorld {
414 world_width_m: 64.0,
415 world_height_m: 64.0,
416 terrain_zones: vec![],
417 z_platforms: vec![],
418 z_transitions: vec![],
419 buildings: vec![],
420 doors: vec![],
421 circles: vec![],
422 }
423 }
424
425 #[test]
426 fn path_on_open_field() {
427 let world = open_world();
428 let path = find_path(&world, 10.0, 10.0, 0.0, 20.0, 15.0).expect("path");
429 assert!(!path.is_empty());
430 let last = *path.last().unwrap();
431 assert!((last.0 - 20.5).abs() < 1.0);
432 assert!((last.1 - 15.5).abs() < 1.0);
433 }
434
435 #[test]
436 fn path_routes_around_blocking_tree() {
437 let mut world = open_world();
438 world.circles.push(crate::grid::NavBlockingCircle {
439 x: 15.0,
440 y: 12.0,
441 radius_m: 0.8,
442 });
443 let path = find_path(&world, 10.0, 12.0, 0.0, 20.0, 12.0).expect("path around tree");
444 for (x, y) in &path {
445 let near_tree = (*x - 15.0).abs() < 1.0 && (*y - 12.0).abs() < 1.0;
446 assert!(!near_tree, "path should not cut through tree at ({x},{y})");
447 }
448 }
449}