1use std::{
14 cmp::Ordering,
15 collections::{BinaryHeap, HashMap}
16};
17
18use bevy::prelude::*;
19use wde_terrain::prelude::Terrain as TerrainMap;
20use wde_terrain_grid::{core::grid::TILE_SIZE, prelude::*};
21
22use crate::{
23 navgrid::NavGrid,
24 navigation::{
25 WalkSpeeds,
26 walkmap::{CELL_SIZE, WalkMap, is_airlock, is_path_piece, is_pressurized}
27 }
28};
29
30pub const AIRLOCK_ROLE: &str = "airlock";
32
33const SEARCH_MARGIN: f32 = 40.0;
35const MAX_EXPANSIONS: usize = 80_000;
37const SNAP_DISTANCE: f32 = 3.0;
39const LINE_STEP: f32 = CELL_SIZE * 0.5;
41const LINE_HALF_WIDTH: f32 = 0.08;
43const MAX_FORMATION_RINGS: usize = 24;
45const MAX_WALK_SLOPE: f32 = 0.9;
47const SLOPE_PROBE: f32 = 0.5;
49
50#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
51enum Node {
52 Tile(IVec2),
54 Cell(IVec2)
56}
57
58#[derive(Clone, Copy, PartialEq, Eq, Debug)]
60pub(crate) enum Terrain {
61 Ground,
63 Path,
65 Indoor,
67 Airlock
69}
70impl Terrain {
71 fn speed(self, speeds: &WalkSpeeds) -> f32 {
72 match self {
73 Terrain::Ground => speeds.ground,
74 Terrain::Path => speeds.path,
75 Terrain::Indoor | Terrain::Airlock => speeds.indoor
76 }
77 }
78
79 pub fn pressurized(self) -> bool {
80 matches!(self, Terrain::Indoor | Terrain::Airlock)
81 }
82}
83
84fn step_speed(a: Terrain, b: Terrain, speeds: &WalkSpeeds) -> f32 {
86 a.speed(speeds).min(b.speed(speeds)).max(0.01)
87}
88
89#[derive(Clone, Copy, Debug)]
91pub(crate) struct Waypoint {
92 pub position: Vec3,
93 pub speed: f32,
95 pub indoor: bool,
97 pub wait: f32
99}
100
101#[derive(Clone, Copy, PartialEq, Debug)]
103pub(crate) struct Location {
104 pub building: Option<Entity>,
106 pub terrain: Terrain,
107 pub height: f32
109}
110
111pub(crate) struct NavWorld<'a> {
113 pub grid: &'a Grid,
114 pub buildings: HashMap<Entity, &'a GridEntity>,
115 pub walk: &'a WalkMap,
116 pub network: &'a NavGrid,
117 pub terrain: Option<&'a TerrainMap>
119}
120
121impl NavWorld<'_> {
122 fn tile_key(pos: Vec2) -> IVec2 {
123 (pos / TILE_SIZE).floor().as_ivec2()
124 }
125
126 fn tile_center(key: IVec2) -> Vec2 {
127 (key.as_vec2() + 0.5) * TILE_SIZE
128 }
129
130 fn occupant(&self, pos: Vec2) -> Option<(Entity, &GridEntity)> {
131 let (chunk, local) = Grid::get_nearest_tile(pos);
132 let entity = self.grid.get_entity(chunk, local)?;
133 self.buildings.get(&entity).map(|b| (entity, *b))
134 }
135
136 fn is_open_tile(&self, pos: Vec2) -> bool {
139 self.occupant(pos).is_none_or(|(_, b)| is_path_piece(b))
140 }
141
142 fn ground(&self, pos: Vec2) -> Option<f32> {
144 let Some(terrain) = self.terrain else {
145 return Some(0.0);
146 };
147 let height = terrain.height_at(pos)?;
148 let steep = [Vec2::X, Vec2::Y].into_iter().any(|axis| {
149 let probe = axis * SLOPE_PROBE;
150 match (
151 terrain.height_at(pos + probe),
152 terrain.height_at(pos - probe)
153 ) {
154 (Some(a), Some(b)) => (a - b).abs() > MAX_WALK_SLOPE * 2.0 * SLOPE_PROBE,
155 _ => true
156 }
157 });
158 (!steep).then_some(height)
159 }
160
161 pub fn locate(&self, pos: Vec2) -> Option<Location> {
163 match self.occupant(pos) {
164 None => Some(Location {
165 building: None,
166 terrain: Terrain::Ground,
167 height: self.ground(pos)?
168 }),
169 Some((_, b)) if is_path_piece(b) => Some(Location {
170 building: None,
171 terrain: Terrain::Path,
172 height: self.ground(pos)?
173 }),
174 Some((entity, b)) => {
175 let cell = self.walk.cells.get(&WalkMap::cell_key(pos))?;
176 (cell.building == entity).then(|| Location {
177 building: Some(entity),
178 terrain: match (is_airlock(b), is_pressurized(b)) {
179 (true, _) => Terrain::Airlock,
180 (false, true) => Terrain::Indoor,
181 (false, false) => Terrain::Ground
182 },
183 height: cell.floor
184 })
185 }
186 }
187 }
188
189 pub fn can_cross(&self, a: &Location, b: &Location) -> bool {
192 if a.building == b.building {
193 return true;
194 }
195 match (a.terrain.pressurized(), b.terrain.pressurized()) {
196 (false, false) => true,
197 (true, true) => match (a.building, b.building) {
198 (Some(x), Some(y)) => self.network.buildings.get(&x).is_some_and(|building| {
199 building
200 .neighbors
201 .iter()
202 .any(|n| n.entity == y && n.kind == LinkKind::Tunnel)
203 }),
204 _ => false
205 },
206 _ => a.terrain == Terrain::Airlock || b.terrain == Terrain::Airlock
207 }
208 }
209
210 fn line_walkable(&self, a: Vec2, b: Vec2) -> bool {
213 let side = (b - a).perp().normalize_or_zero() * LINE_HALF_WIDTH;
214 [Vec2::ZERO, side, -side]
215 .into_iter()
216 .all(|offset| self.strip_walkable(a + offset, b + offset))
217 }
218
219 fn strip_walkable(&self, a: Vec2, b: Vec2) -> bool {
220 let Some(mut previous) = self.locate(a) else {
221 return false;
222 };
223 let steps = (a.distance(b) / LINE_STEP).ceil().max(1.0) as usize;
224 for i in 1..=steps {
225 let Some(here) = self.locate(a.lerp(b, i as f32 / steps as f32)) else {
226 return false;
227 };
228 if here.terrain != previous.terrain || !self.can_cross(&previous, &here) {
229 return false;
230 }
231 previous = here;
232 }
233 true
234 }
235
236 fn nearest_walkable(
238 &self,
239 pos: Vec2,
240 max_distance: f32,
241 filter: impl Fn(&Location) -> bool
242 ) -> Option<(Vec2, Location)> {
243 if let Some(here) = self.locate(pos).filter(&filter) {
244 return Some((pos, here));
245 }
246 let rings = (max_distance / CELL_SIZE).ceil() as i32;
247 let origin = WalkMap::cell_key(pos);
248 let mut best: Option<(f32, Vec2, Location)> = None;
249 for ring in 1..=rings {
250 for dy in -ring..=ring {
251 for dx in -ring..=ring {
252 if dx.abs() != ring && dy.abs() != ring {
253 continue;
254 }
255 let p = WalkMap::cell_center(origin + IVec2::new(dx, dy));
256 let distance = p.distance(pos);
257 if distance > max_distance || best.is_some_and(|b| b.0 <= distance) {
258 continue;
259 }
260 if let Some(here) = self.locate(p).filter(&filter) {
261 best = Some((distance, p, here));
262 }
263 }
264 }
265 if best.is_some_and(|b| b.0 <= ring as f32 * CELL_SIZE) {
267 break;
268 }
269 }
270 best.map(|(_, p, l)| (p, l))
271 }
272
273 fn node_at(&self, pos: Vec2) -> Node {
275 match self.is_open_tile(pos) {
276 true => Node::Tile(Self::tile_key(pos)),
277 false => Node::Cell(WalkMap::cell_key(pos))
278 }
279 }
280
281 fn node_position(node: Node) -> Vec2 {
282 match node {
283 Node::Tile(key) => Self::tile_center(key),
284 Node::Cell(key) => WalkMap::cell_center(key)
285 }
286 }
287
288 fn neighbors(&self, node: Node, from: &Location, out: &mut Vec<(Node, Location)>) {
290 let push = |pos: Vec2, out: &mut Vec<(Node, Location)>| {
291 if let Some(here) = self.locate(pos)
292 && self.can_cross(from, &here)
293 {
294 let next = self.node_at(pos);
295 if next != node && !out.iter().any(|(n, _)| *n == next) {
296 out.push((next, here));
297 }
298 }
299 };
300 match node {
301 Node::Tile(key) => {
302 let center = Self::tile_center(key);
303 for d in DIRECTIONS {
304 let next = Self::tile_center(key + d);
305 let diagonal = d.x != 0 && d.y != 0;
306 if self.is_open_tile(next) {
307 if !diagonal
309 || (self.is_open_tile(Self::tile_center(key + IVec2::new(d.x, 0)))
310 && self.is_open_tile(Self::tile_center(key + IVec2::new(0, d.y))))
311 {
312 push(next, out);
313 }
314 } else if !diagonal {
315 let side = d.as_vec2() * (TILE_SIZE + CELL_SIZE) / 2.0;
317 let along = Vec2::new(d.y as f32, d.x as f32).abs();
318 for i in 0..8 {
319 let offset = (i as f32 - 3.5) * CELL_SIZE;
320 push(center + side + along * offset, out);
321 }
322 }
323 }
324 }
325 Node::Cell(key) => {
326 for d in DIRECTIONS {
327 let diagonal = d.x != 0 && d.y != 0;
329 if diagonal
330 && (self
331 .locate(WalkMap::cell_center(key + IVec2::new(d.x, 0)))
332 .is_none()
333 || self
334 .locate(WalkMap::cell_center(key + IVec2::new(0, d.y)))
335 .is_none())
336 {
337 continue;
338 }
339 push(WalkMap::cell_center(key + d), out);
340 }
341 }
342 }
343 }
344}
345
346const DIRECTIONS: [IVec2; 8] = [
347 IVec2::new(1, 0),
348 IVec2::new(-1, 0),
349 IVec2::new(0, 1),
350 IVec2::new(0, -1),
351 IVec2::new(1, 1),
352 IVec2::new(1, -1),
353 IVec2::new(-1, 1),
354 IVec2::new(-1, -1)
355];
356
357#[derive(PartialEq)]
358struct Open {
359 f: f32,
360 node: Node
361}
362impl Eq for Open {}
363impl PartialOrd for Open {
364 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
365 Some(self.cmp(other))
366 }
367}
368impl Ord for Open {
369 fn cmp(&self, other: &Self) -> Ordering {
371 other.f.total_cmp(&self.f)
372 }
373}
374
375#[derive(Clone, Copy)]
377struct PathPoint {
378 position: Vec3,
379 terrain: Terrain,
380 pressurized: bool,
382 wait: f32
384}
385
386pub(crate) fn find_path(
389 world: &NavWorld,
390 speeds: &WalkSpeeds,
391 start: Vec3,
392 goal: Vec3,
393 outdoors_only: bool
394) -> Option<Vec<Waypoint>> {
395 let allowed = |l: &Location| !outdoors_only || !l.terrain.pressurized();
396 let (start, start_location) = world.nearest_walkable(start.xz(), SNAP_DISTANCE, allowed)?;
397 let (goal, goal_location) = world.nearest_walkable(goal.xz(), SNAP_DISTANCE, allowed)?;
398 let (start_node, goal_node) = (world.node_at(start), world.node_at(goal));
399 let bounds = Rect::from_corners(start, goal).inflate(SEARCH_MARGIN);
400
401 let fastest = speeds.indoor.max(speeds.path).max(speeds.ground).max(0.01);
403 let goal_position = NavWorld::node_position(goal_node);
404 let heuristic = |pos: Vec2| pos.distance(goal_position) / fastest;
405
406 let mut open = BinaryHeap::from([Open {
408 f: heuristic(start),
409 node: start_node
410 }]);
411 let mut came_from: HashMap<Node, Node> = HashMap::new();
412 let mut cost: HashMap<Node, f32> = HashMap::from([(start_node, 0.0)]);
413 let mut locations: HashMap<Node, Location> =
414 HashMap::from([(start_node, start_location), (goal_node, goal_location)]);
415 let mut expansions = 0;
416 let mut neighbors = Vec::with_capacity(16);
417 while let Some(Open { node, .. }) = open.pop() {
418 if node == goal_node {
419 break;
420 }
421 expansions += 1;
422 if expansions > MAX_EXPANSIONS {
423 return None;
424 }
425 let location = locations[&node];
426 let position = NavWorld::node_position(node);
427 neighbors.clear();
428 world.neighbors(node, &location, &mut neighbors);
429 neighbors.retain(|(_, l)| allowed(l));
430 let node_cost = cost[&node];
431 for &(next, next_location) in &neighbors {
432 let next_position = NavWorld::node_position(next);
433 if !bounds.contains(next_position) {
434 continue;
435 }
436 let next_cost = node_cost
437 + position.distance(next_position)
438 / step_speed(location.terrain, next_location.terrain, speeds);
439 if cost.get(&next).is_some_and(|c| *c <= next_cost) {
440 continue;
441 }
442 cost.insert(next, next_cost);
443 came_from.insert(next, node);
444 locations.insert(next, next_location);
445 open.push(Open {
446 f: next_cost + heuristic(next_position),
447 node: next
448 });
449 }
450 }
451 if !came_from.contains_key(&goal_node) && start_node != goal_node {
452 return None;
453 }
454
455 let mut nodes = vec![goal_node];
457 while let Some(previous) = came_from.get(nodes.last().unwrap()) {
458 nodes.push(*previous);
459 }
460 nodes.reverse();
461 let mut points: Vec<(Vec2, Location)> = nodes
462 .iter()
463 .map(|node| (NavWorld::node_position(*node), locations[node]))
464 .collect();
465 points[0] = (start, start_location);
466 if points.len() == 1 {
467 points.push((goal, goal_location));
468 }
469 let last = points.len() - 1;
470 points[last] = (goal, goal_location);
471
472 let points = airlock_stops(world, &points, speeds);
473 let points = smooth(world, &points);
474
475 Some(
477 points
478 .windows(2)
479 .map(|pair| {
480 let (from, to) = (pair[0], pair[1]);
481 Waypoint {
482 position: to.position,
483 speed: step_speed(from.terrain, to.terrain, speeds),
484 indoor: from.pressurized && to.pressurized,
485 wait: to.wait
486 }
487 })
488 .collect()
489 )
490}
491
492fn airlock_stops(
495 world: &NavWorld,
496 points: &[(Vec2, Location)],
497 speeds: &WalkSpeeds
498) -> Vec<PathPoint> {
499 let point = |(p, l): (Vec2, Location), pressurized: bool, wait: f32| PathPoint {
500 position: Vec3::new(p.x, l.height, p.y),
501 terrain: l.terrain,
502 pressurized,
503 wait
504 };
505 let mut result = Vec::with_capacity(points.len() + 2);
506 let mut i = 0;
507 while i < points.len() {
508 if points[i].1.terrain != Terrain::Airlock {
509 result.push(point(points[i], points[i].1.terrain.pressurized(), 0.0));
510 i += 1;
511 continue;
512 }
513 let end = (i..points.len())
515 .find(|&j| points[j].1.terrain != Terrain::Airlock)
516 .unwrap_or(points.len());
517 let before = i.checked_sub(1).map(|j| points[j].1.terrain.pressurized());
518 let after = points.get(end).map(|p| p.1.terrain.pressurized());
519 let side_in = before.or(after).unwrap_or(true);
520 let side_out = after.or(before).unwrap_or(true);
521 let center = (side_in != side_out)
522 .then(|| {
523 let airlock = points[i].1.building?;
524 let center = world.buildings.get(&airlock)?.center();
525 world.nearest_walkable(center, TILE_SIZE, |l| l.building == Some(airlock))
526 })
527 .flatten();
528 match center {
529 Some(center) => {
530 result.push(point(points[i], side_in, 0.0));
531 result.push(point(center, side_in, speeds.airlock_cycle));
532 result.push(point(points[end - 1], side_out, 0.0));
533 }
534 None => result.extend(points[i..end].iter().map(|p| point(*p, side_in, 0.0)))
535 }
536 i = end;
537 }
538 result
539}
540
541fn smooth(world: &NavWorld, points: &[PathPoint]) -> Vec<PathPoint> {
544 let mut result = vec![points[0]];
545 let mut from = 0;
546 while from + 1 < points.len() {
547 let mut to = from + 1;
548 while to + 1 < points.len()
549 && points[to].wait <= 0.0
550 && points[to + 1].pressurized == points[from].pressurized
551 && world.line_walkable(points[from].position.xz(), points[to + 1].position.xz())
552 {
553 to += 1;
554 }
555 result.push(points[to]);
556 from = to;
557 }
558 result
559}
560
561pub(crate) fn formation_spots(
564 world: &NavWorld,
565 center: Vec3,
566 count: usize,
567 spacing: f32
568) -> Vec<Vec3> {
569 let Some((origin, place)) = world.nearest_walkable(center.xz(), SNAP_DISTANCE, |_| true) else {
570 return Vec::new();
571 };
572 let axes: Vec<Vec2> = (0..6)
573 .map(|i| Vec2::from_angle(i as f32 * std::f32::consts::TAU / 6.0) * spacing)
574 .collect();
575 let mut spots = Vec::with_capacity(count);
576 for ring in 0..=MAX_FORMATION_RINGS {
577 let candidates: Vec<Vec2> = match ring {
578 0 => vec![origin],
579 _ => (0..6)
580 .flat_map(|side| {
581 let corner = origin + axes[side] * ring as f32;
582 let step = axes[(side + 2) % 6];
583 (0..ring).map(move |i| corner + step * i as f32)
584 })
585 .collect()
586 };
587 for p in candidates {
588 if let Some(here) = world.locate(p)
589 && here.building == place.building
590 && here.terrain.pressurized() == place.terrain.pressurized()
591 {
592 spots.push(Vec3::new(p.x, here.height, p.y));
593 if spots.len() == count {
594 return spots;
595 }
596 }
597 }
598 }
599 spots
600}