1use std::collections::{HashMap, VecDeque};
2use wde_logger::prelude::*;
3
4use bevy::{ecs::system::SystemParam, prelude::*};
5use wde_terrain_grid::prelude::*;
6
7use crate::{TerrainNavigator, navgrid::NavGrid};
8
9pub(crate) mod pathfinding;
10pub(crate) mod walkmap;
11
12pub use pathfinding::AIRLOCK_ROLE;
13use pathfinding::{NavWorld, Waypoint, find_path, formation_spots};
14use walkmap::WalkMap;
15
16const TURN_SPEED: f32 = 10.0;
18const WAYPOINT_REACHED: f32 = 0.05;
20const AIRLOCK_REACHED: f32 = 0.3;
23const STUCK_REPLAN: f32 = 1.0;
25const DEFAULT_AGENT_RADIUS: f32 = 0.14;
27const FORMATION_SPACING: f32 = 1.2;
29const SEPARATION_RATE: f32 = 8.0;
31const WALKER_YIELD: f32 = 0.25;
33const SIDESTEP: f32 = 0.6;
35
36#[derive(Resource, Clone, Debug)]
39pub struct WalkSpeeds {
40 pub base: f32,
42 pub indoor: f32,
44 pub path: f32,
46 pub ground: f32,
48 pub airlock_cycle: f32
50}
51impl Default for WalkSpeeds {
52 fn default() -> Self {
53 Self {
54 base: 1.5,
55 indoor: 1.2,
56 path: 1.0,
57 ground: 0.7,
58 airlock_cycle: 2.5
59 }
60 }
61}
62
63#[derive(Component, Clone, Copy, Debug)]
65pub struct NavAgent {
66 pub radius: f32,
67 pub speed: f32,
69 pub outdoors_only: bool
71}
72impl Default for NavAgent {
73 fn default() -> Self {
74 Self {
75 radius: DEFAULT_AGENT_RADIUS,
76 speed: 1.0,
77 outdoors_only: false
78 }
79 }
80}
81
82#[derive(Component, Clone, Copy, PartialEq, Eq, Debug)]
85pub struct Indoors(pub bool);
86
87pub struct NavigationPlugin;
88impl Plugin for NavigationPlugin {
89 fn build(&self, app: &mut App) {
90 app.init_resource::<WalkSpeeds>()
91 .init_resource::<WalkMap>()
92 .add_systems(
93 Update,
94 (
95 walkmap::rebuild_walk_map.after(crate::navgrid::rebuild_nav_grid),
96 request_paths,
97 replan_on_network_change,
98 follow_paths,
99 separate_agents
100 )
101 .chain()
102 );
103 }
104}
105
106#[derive(Component)]
108pub(crate) struct NavPath {
109 pub waypoints: VecDeque<Waypoint>,
110 pub destination: Vec3,
111 pub waiting: f32,
113 pub stuck: f32
115}
116
117#[derive(SystemParam)]
119struct NavWorldParam<'w, 's> {
120 grid: Res<'w, Grid>,
121 placed: Query<'w, 's, (Entity, &'static GridEntity), With<BuildingConnections>>,
122 walk: Res<'w, WalkMap>,
123 network: Res<'w, NavGrid>,
124 terrain: Query<'w, 's, &'static wde_terrain::prelude::Terrain>
125}
126impl NavWorldParam<'_, '_> {
127 fn world(&self) -> NavWorld<'_> {
128 NavWorld {
129 grid: &self.grid,
130 buildings: self.placed.iter().collect::<HashMap<_, _>>(),
131 walk: &self.walk,
132 network: &self.network,
133 terrain: self.terrain.single().ok()
134 }
135 }
136
137 fn is_changed(&self) -> bool {
138 self.walk.is_changed() || self.network.is_changed()
139 }
140}
141
142fn assign_spots(agents: &[(Entity, Vec3)], spots: &[Vec3]) -> Vec<(Entity, Option<Vec3>)> {
144 let mut pairs: Vec<(f32, usize, usize)> = agents
145 .iter()
146 .enumerate()
147 .flat_map(|(a, (_, pos))| {
148 spots
149 .iter()
150 .enumerate()
151 .map(move |(s, spot)| (pos.xz().distance_squared(spot.xz()), a, s))
152 })
153 .collect();
154 pairs.sort_unstable_by(|x, y| x.0.total_cmp(&y.0));
155 let mut agent_spot = vec![None; agents.len()];
156 let mut taken = vec![false; spots.len()];
157 for (_, a, s) in pairs {
158 if agent_spot[a].is_none() && !taken[s] {
159 agent_spot[a] = Some(spots[s]);
160 taken[s] = true;
161 }
162 }
163 agents.iter().map(|(e, _)| *e).zip(agent_spot).collect()
164}
165
166fn request_paths(
168 mut commands: Commands,
169 mut navigator: ResMut<TerrainNavigator>,
170 transforms: Query<&Transform>,
171 agents: Query<&NavAgent>,
172 nav: NavWorldParam,
173 speeds: Res<WalkSpeeds>
174) {
175 if navigator.new_entities.is_empty()
176 && navigator.groups.is_empty()
177 && navigator.removed_entities.is_empty()
178 {
179 return;
180 }
181 let world = nav.world();
182
183 let mut requests: Vec<(Entity, Vec3)> = navigator.new_entities.drain(..).collect();
185 for (entities, destination) in std::mem::take(&mut navigator.groups) {
186 let members: Vec<(Entity, Vec3)> = entities
187 .iter()
188 .filter_map(|e| transforms.get(*e).ok().map(|t| (*e, t.translation)))
189 .collect();
190 let radius = members
191 .iter()
192 .map(|(e, _)| agents.get(*e).map_or(DEFAULT_AGENT_RADIUS, |a| a.radius))
193 .fold(0.0, f32::max);
194 let spacing = 2.0 * radius * FORMATION_SPACING;
195 let spots = formation_spots(&world, destination, members.len(), spacing);
196 requests.extend(
197 assign_spots(&members, &spots)
198 .into_iter()
199 .map(|(entity, spot)| (entity, spot.unwrap_or(destination)))
200 );
201 }
202
203 for (entity, destination) in requests {
204 let Ok(transform) = transforms.get(entity) else {
205 continue;
206 };
207 let outdoors_only = agents.get(entity).is_ok_and(|a| a.outdoors_only);
208 match find_path(
209 &world,
210 &speeds,
211 transform.translation,
212 destination,
213 outdoors_only
214 ) {
215 Some(waypoints) => {
216 commands.entity(entity).insert(NavPath {
217 waypoints: waypoints.into(),
218 destination,
219 waiting: 0.0,
220 stuck: 0.0
221 });
222 }
223 None => {
224 warn!("No path from {} to {}.", transform.translation, destination);
225 commands.entity(entity).remove::<NavPath>();
226 }
227 }
228 }
229 for entity in navigator.removed_entities.drain(..) {
230 commands.entity(entity).remove::<NavPath>();
231 }
232}
233
234fn replan_on_network_change(
236 nav: NavWorldParam,
237 mut moving: Query<(&Transform, &mut NavPath, Option<&NavAgent>)>,
238 speeds: Res<WalkSpeeds>
239) {
240 if !nav.is_changed() || moving.is_empty() {
241 return;
242 }
243 let world = nav.world();
244 for (transform, mut path, agent) in &mut moving {
245 if path.waiting > 0.0 {
246 continue; }
248 let outdoors_only = agent.is_some_and(|a| a.outdoors_only);
250 if let Some(waypoints) = find_path(
251 &world,
252 &speeds,
253 transform.translation,
254 path.destination,
255 outdoors_only
256 ) {
257 path.waypoints = waypoints.into();
258 }
259 }
260}
261
262fn walk_step(world: &NavWorld, from: Vec3, step: Vec3) -> Option<Vec3> {
268 let Some(here) = world.locate(from.xz()) else {
269 return Some(from + step);
270 };
271 [
272 step,
273 step * Vec3::new(1.0, 1.0, 0.0),
274 step * Vec3::new(0.0, 1.0, 1.0)
275 ]
276 .into_iter()
277 .filter(|s| s.xz().length_squared() > 1e-10)
278 .map(|s| from + s)
279 .find(|to| {
280 world
281 .locate(to.xz())
282 .is_some_and(|there| world.can_cross(&here, &there))
283 })
284}
285
286type Walker<'a> = (
288 Entity,
289 &'a mut Transform,
290 &'a mut NavPath,
291 Option<&'a Indoors>,
292 Option<&'a NavAgent>
293);
294
295fn follow_paths(
296 mut commands: Commands,
297 mut moving: Query<Walker>,
298 nav: NavWorldParam,
299 speeds: Res<WalkSpeeds>,
300 time: Res<Time>
301) {
302 let dt = time.delta_secs();
303 let world = nav.world();
304 for (entity, mut transform, mut path, indoors, agent) in &mut moving {
305 let mut time_left = dt;
306 let mut heading = None;
307 let mut environment = indoors.map(|i| i.0);
308 while time_left > 0.0 {
309 if path.waiting > 0.0 {
311 let waited = path.waiting.min(time_left);
312 path.waiting -= waited;
313 time_left -= waited;
314 continue;
315 }
316 let Some(&waypoint) = path.waypoints.front() else {
317 break;
318 };
319 environment = Some(waypoint.indoor);
321
322 let speed = speeds.base * waypoint.speed * agent.map_or(1.0, |a| a.speed);
323 let to_target = waypoint.position - transform.translation;
324 let distance = to_target.length();
325 if waypoint.wait > 0.0 && distance <= AIRLOCK_REACHED {
326 path.waypoints.pop_front();
327 path.waiting = waypoint.wait;
328 continue;
329 }
330 let reach = speed * time_left;
331 if distance <= WAYPOINT_REACHED.max(reach) {
332 transform.translation = waypoint.position;
333 time_left -= distance / speed.max(1e-3);
334 path.waypoints.pop_front();
335 path.waiting = waypoint.wait;
336 continue;
337 }
338 match walk_step(&world, transform.translation, to_target / distance * reach) {
340 Some(mut next) => {
341 if let Some(here) = world.locate(next.xz())
343 && here.building.is_none()
344 {
345 next.y = here.height;
346 }
347 transform.translation = next;
348 heading = Some(to_target);
349 path.stuck = 0.0;
350 }
351 None => path.stuck += time_left
352 }
353 time_left = 0.0;
354 }
355 if path.stuck > STUCK_REPLAN {
356 path.stuck = 0.0;
357 if let Some(waypoints) = find_path(
358 &world,
359 &speeds,
360 transform.translation,
361 path.destination,
362 agent.is_some_and(|a| a.outdoors_only)
363 ) {
364 path.waypoints = waypoints.into();
365 }
366 }
367
368 if let Some(indoor) = environment
369 && indoors.is_none_or(|i| i.0 != indoor)
370 {
371 commands.entity(entity).insert(Indoors(indoor));
372 }
373
374 if let Some(heading) = heading
376 && heading.xz().length_squared() > 1e-6
377 {
378 let target = Quat::from_rotation_y((-heading.z).atan2(heading.x));
379 let rotation = transform.rotation;
380 transform.rotation = rotation.slerp(target, (TURN_SPEED * dt).min(1.0));
381 }
382
383 if path.waypoints.is_empty() && path.waiting <= 0.0 {
384 commands.entity(entity).remove::<NavPath>();
385 }
386 }
387}
388
389fn separate_agents(
392 mut agents: Query<(&NavAgent, &mut Transform, Option<&NavPath>)>,
393 nav: NavWorldParam,
394 time: Res<Time>
395) {
396 let rate = (time.delta_secs() * SEPARATION_RATE).min(1.0);
397 if rate <= 0.0 {
398 return;
399 }
400
401 let bodies: Vec<(Vec2, f32, f32, Option<Vec2>)> = agents
403 .iter()
404 .map(|(agent, transform, path)| {
405 let position = transform.translation.xz();
406 let (mobility, heading) = match path {
407 Some(path) if path.waiting > 0.0 => (0.0, None),
409 Some(path) => (
410 WALKER_YIELD,
411 path.waypoints
412 .front()
413 .and_then(|w| (w.position.xz() - position).try_normalize())
414 ),
415 None => (1.0, None)
416 };
417 (position, agent.radius, mobility, heading)
418 })
419 .collect();
420 if bodies.len() < 2 {
421 return;
422 }
423
424 let cell = 2.0 * bodies.iter().map(|b| b.1).fold(0.0, f32::max).max(0.01);
426 let key = |p: Vec2| (p / cell).floor().as_ivec2();
427 let mut cells: HashMap<IVec2, Vec<usize>> = HashMap::new();
428 for (i, body) in bodies.iter().enumerate() {
429 cells.entry(key(body.0)).or_default().push(i);
430 }
431
432 let mut pushes = vec![Vec2::ZERO; bodies.len()];
433 for (i, &(pi, ri, mi, hi)) in bodies.iter().enumerate() {
434 let k = key(pi);
435 for dx in -1..=1 {
436 for dy in -1..=1 {
437 for &j in cells.get(&(k + IVec2::new(dx, dy))).into_iter().flatten() {
438 if j <= i {
439 continue;
440 }
441 let (pj, rj, mj, hj) = bodies[j];
442 let overlap = ri + rj - pi.distance(pj);
443 if overlap <= 0.0 || mi + mj <= 0.0 {
444 continue;
445 }
446 let normal = (pi - pj)
448 .try_normalize()
449 .unwrap_or_else(|| Vec2::from_angle(i as f32 * 2.399));
450 pushes[i] += normal * overlap * mi / (mi + mj);
451 pushes[j] -= normal * overlap * mj / (mi + mj);
452 if let Some(h) = hi {
454 pushes[i] += Vec2::new(-h.y, h.x) * overlap * SIDESTEP;
455 }
456 if let Some(h) = hj {
457 pushes[j] += Vec2::new(-h.y, h.x) * overlap * SIDESTEP;
458 }
459 }
460 }
461 }
462 }
463 if pushes.iter().all(|p| *p == Vec2::ZERO) {
464 return;
465 }
466
467 let world = nav.world();
468 for ((_, mut transform, _), push) in agents.iter_mut().zip(pushes) {
469 if push == Vec2::ZERO {
470 continue;
471 }
472 let from = transform.translation.xz();
473 let to = from + push * rate;
474 if let Some(here) = world.locate(from)
476 && let Some(there) = world.locate(to)
477 && world.can_cross(&here, &there)
478 {
479 transform.translation.x = to.x;
480 transform.translation.z = to.y;
481 }
482 }
483}