1use super::*;
2
3fn path_in_site(start: Vec2<i32>, end: Vec2<i32>, site: &site::Site) -> PathResult<Vec2<i32>> {
4 let heuristic = |tile: &Vec2<i32>| tile.as_::<f32>().distance(end.as_());
5 const ASTAR_ITERS: usize = 1000;
6 let mut astar = Astar::new(
7 ASTAR_ITERS,
8 start,
9 BuildHasherDefault::<FxHasher64>::default(),
10 );
11
12 let transition = |a: Vec2<i32>, b: Vec2<i32>| {
13 let distance = a.as_::<f32>().distance(b.as_());
14 let a_tile = site.tiles.get(a);
15 let b_tile = site.tiles.get(b);
16
17 let terrain = match &b_tile.kind {
18 TileKind::Empty => 3.0,
19 TileKind::Hazard(_) => 50.0,
20 TileKind::Field => 8.0,
21 TileKind::Plaza | TileKind::Road { .. } | TileKind::Path { .. } | TileKind::Bridge => {
22 1.0
23 },
24
25 TileKind::Building
26 | TileKind::Castle
27 | TileKind::Wall(_)
28 | TileKind::Tower(_)
29 | TileKind::Keep(_)
30 | TileKind::Gate
31 | TileKind::AdletStronghold
32 | TileKind::DwarvenMine
33 | TileKind::GnarlingFortification => 5.0,
34 };
35 let is_door_tile =
36 |plot: Id<site::Plot>, tile: Vec2<i32>| site.plot(plot).door_tile() == Some(tile);
37 let building = if a_tile.is_building() && b_tile.is_road() {
38 a_tile
39 .plot
40 .and_then(|plot| is_door_tile(plot, a).then_some(1.0))
41 .unwrap_or(10000.0)
42 } else if b_tile.is_building() && a_tile.is_road() {
43 b_tile
44 .plot
45 .and_then(|plot| is_door_tile(plot, b).then_some(1.0))
46 .unwrap_or(10000.0)
47 } else if (a_tile.is_building() || b_tile.is_building()) && a_tile.plot != b_tile.plot {
48 10000.0
49 } else {
50 1.0
51 };
52
53 distance * terrain + building
54 };
55
56 let neighbors = |tile: &Vec2<i32>| {
57 let tile = *tile;
58
59 const CARDINALS: &[Vec2<i32>] = &[
60 Vec2::new(1, 0),
61 Vec2::new(0, 1),
62 Vec2::new(-1, 0),
63 Vec2::new(0, -1),
64 ];
65
66 CARDINALS.iter().map(move |c| {
67 let n = tile + *c;
68 (n, transition(tile, n))
69 })
70 };
71
72 astar.poll(ASTAR_ITERS, heuristic, neighbors, |tile| *tile == end)
73}
74
75fn path_between_sites(
76 start: SiteId,
77 end: SiteId,
78 sites: &Sites,
79 world: &World,
80) -> PathResult<(Id<Track>, bool)> {
81 let world_site = |site_id: SiteId| {
82 let id = sites.get(site_id).and_then(|site| site.world_site)?;
83 world.civs().sites.recreate_id(id.id())
84 };
85
86 let start = if let Some(start) = world_site(start) {
87 start
88 } else {
89 return PathResult::Pending;
90 };
91 let end = if let Some(end) = world_site(end) {
92 end
93 } else {
94 return PathResult::Pending;
95 };
96
97 let get_site = |site: &Id<civ::Site>| world.civs().sites.get(*site);
98
99 let end_pos = get_site(&end).center.as_::<f32>();
100 let heuristic = |site: &Id<civ::Site>| get_site(site).center.as_().distance(end_pos);
101
102 let mut astar = Astar::new(250, start, BuildHasherDefault::<FxHasher64>::default());
103
104 let transition = |a: Id<civ::Site>, b: Id<civ::Site>| {
105 world
106 .civs()
107 .track_between(a, b)
108 .map(|(id, _)| world.civs().tracks.get(id).cost)
109 .unwrap_or(f32::INFINITY)
110 };
111 let neighbors = |site: &Id<civ::Site>| {
112 let site = *site;
113 world
114 .civs()
115 .neighbors(site)
116 .map(move |n| (n, transition(n, site)))
117 };
118
119 let path = astar.poll(250, heuristic, neighbors, |site| *site == end);
120
121 path.map(|path| {
122 let path = path
123 .into_iter()
124 .tuple_windows::<(_, _)>()
125 .filter_map(|(a, b)| world.civs().track_between(a, b))
127 .collect();
128 Path { nodes: path }
129 })
130}
131
132fn path_site(
133 start: Vec2<f32>,
134 end: Vec2<f32>,
135 site: Id<WorldSite>,
136 index: IndexRef,
137) -> Option<Vec<Vec2<f32>>> {
138 let site = index.sites.get(site);
139 let start = site.wpos_tile_pos(start.as_());
140
141 let end = site.wpos_tile_pos(end.as_());
142
143 let nodes = match path_in_site(start, end, site) {
144 PathResult::Path(p, _c) => p.nodes,
145 PathResult::Exhausted(p) => p.nodes,
146 PathResult::None(_) | PathResult::Pending => return None,
147 };
148
149 Some(
150 nodes
151 .into_iter()
152 .map(|tile| site.tile_center_wpos(tile).as_() + 0.5)
153 .collect(),
154 )
155}
156
157fn path_between_towns(
158 start: SiteId,
159 end: SiteId,
160 sites: &Sites,
161 world: &World,
162) -> Option<PathData<(Id<Track>, bool), SiteId>> {
163 match path_between_sites(start, end, sites, world) {
164 PathResult::Exhausted(p) => Some(PathData {
165 end,
166 path: p.nodes.into(),
167 repoll: true,
168 }),
169 PathResult::Path(p, _c) => Some(PathData {
170 end,
171 path: p.nodes.into(),
172 repoll: false,
173 }),
174 PathResult::Pending | PathResult::None(_) => None,
175 }
176}
177
178pub fn goto<S: State>(wpos: Vec3<f32>, speed_factor: f32, goal_dist: f32) -> impl Action<S> {
182 const WAYPOINT_DIST: f32 = 12.0;
183
184 just(move |ctx, waypoint: &mut Option<Vec3<f32>>| {
185 if waypoint.is_some_and(|waypoint: Vec3<f32>| {
187 ctx.actor.wpos.xy().distance_squared(waypoint.xy()) < WAYPOINT_DIST.powi(2)
188 }) {
189 *waypoint = None;
190 }
191
192 let waypoint = waypoint.get_or_insert_with(|| {
194 wpos.with_z(ctx.world.sim().get_surface_alt_approx(wpos.xy().as_()))
195 });
196
197 ctx.controller.do_goto(*waypoint, speed_factor);
198 })
199 .repeat()
200 .stop_if(move |ctx: &mut NpcCtx| {
201 ctx.actor.wpos.xy().distance_squared(wpos.xy()) < goal_dist.powi(2)
202 })
203 .with_state(None)
204 .debug(move || format!("goto {}, {}, {}", wpos.x, wpos.y, wpos.z))
205 .map(|_, _| {})
206}
207
208pub fn follow_actor<S: State>(actor: ActorId, distance: f32) -> impl Action<S> {
209 just(move |ctx, _| {
211 if let Some(tgt_wpos) = util::locate_actor(ctx, actor)
212 && let dist_sqr = tgt_wpos.xy().distance_squared(ctx.actor.wpos.xy())
213 && dist_sqr > distance.powi(2)
214 {
215 ctx.controller.do_goto(
224 tgt_wpos,
225 ((dist_sqr.sqrt() - distance) * 0.2).clamp(0.25, 1.0),
226 );
227 } else {
228 ctx.controller.do_idle();
229 }
230 })
231 .repeat()
232 .debug(move || format!("Following actor {actor:?}"))
233 .map(|_, _| ())
234}
235
236pub fn goto_actor<S: State>(actor: ActorId, distance: f32) -> impl Action<S> {
237 follow_actor(actor, distance)
238 .stop_if(move |ctx: &mut NpcCtx| {
239 if let Some(wpos) = util::locate_actor(ctx, actor) {
240 wpos.xy().distance_squared(ctx.actor.wpos.xy()) < distance.powi(2)
241 } else {
242 false
243 }
244 })
245 .map(|_, _| ())
246}
247
248fn goto_flying<S: State>(
251 wpos: Vec3<f32>,
252 speed_factor: f32,
253 goal_dist: f32,
254 step_dist: f32,
255 waypoint_dist: f32,
256 height_offset: f32,
257) -> impl Action<S> {
258 just(move |ctx, waypoint: &mut Option<Vec3<f32>>| {
259 if waypoint.is_some_and(|waypoint: Vec3<f32>| {
261 ctx.actor.wpos.distance_squared(waypoint) < waypoint_dist.powi(2)
262 }) {
263 *waypoint = None;
264 }
265
266 let waypoint = waypoint.get_or_insert_with(|| {
268 let rpos = wpos - ctx.actor.wpos;
269 let len = rpos.magnitude();
270 let wpos = ctx.actor.wpos + (rpos / len) * len.min(step_dist);
271
272 wpos.with_z(ctx.world.sim().get_surface_alt_approx(wpos.xy().as_()) + height_offset)
273 });
274
275 ctx.controller.do_goto(*waypoint, speed_factor);
276 })
277 .repeat()
278 .boxed()
279 .with_state(None)
280 .stop_if(move |ctx: &mut NpcCtx| ctx.actor.wpos.distance_squared(wpos) < goal_dist.powi(2))
281 .debug(move || {
282 format!(
283 "goto flying ({}, {}, {}), goal dist {}",
284 wpos.x, wpos.y, wpos.z, goal_dist
285 )
286 })
287 .map(|_, _| {})
288}
289
290pub fn goto_2d<S: State>(wpos2d: Vec2<f32>, speed_factor: f32, goal_dist: f32) -> impl Action<S> {
293 now(move |ctx, _| {
294 let wpos = wpos2d.with_z(ctx.world.sim().get_surface_alt_approx(wpos2d.as_()));
295 goto(wpos, speed_factor, goal_dist).debug(move || {
296 format!(
297 "goto 2d ({}, {}), z {}, goal dist {}",
298 wpos2d.x, wpos2d.y, wpos.z, goal_dist
299 )
300 })
301 })
302}
303
304pub fn goto_2d_flying<S: State>(
307 wpos2d: Vec2<f32>,
308 speed_factor: f32,
309 goal_dist: f32,
310 step_dist: f32,
311 waypoint_dist: f32,
312 height_offset: f32,
313) -> impl Action<S> {
314 now(move |ctx, _| {
315 let wpos =
316 wpos2d.with_z(ctx.world.sim().get_surface_alt_approx(wpos2d.as_()) + height_offset);
317 goto_flying(
318 wpos,
319 speed_factor,
320 goal_dist,
321 step_dist,
322 waypoint_dist,
323 height_offset,
324 )
325 .debug(move || {
326 format!(
327 "goto 2d flying ({}, {}), goal dist {}",
328 wpos2d.x, wpos2d.y, goal_dist
329 )
330 })
331 })
332}
333
334fn traverse_points<S: State, F>(next_point: F, speed_factor: f32) -> impl Action<S>
335where
336 F: FnMut(&mut NpcCtx) -> Option<Vec2<f32>> + Clone + Send + Sync + 'static,
337{
338 until(move |ctx, next_point: &mut F| {
339 let Some(wpos) = next_point(ctx) else {
341 return ControlFlow::Break(());
342 };
343
344 let wpos_site = |wpos: Vec2<f32>| {
345 ctx.world
346 .sim()
347 .get_wpos(wpos.as_())
348 .and_then(|chunk| chunk.sites.first().copied())
349 };
350
351 let wpos_sites_contain = |wpos: Vec2<f32>, site: Id<world::site::Site>| {
352 ctx.world
353 .sim()
354 .get(wpos.as_().wpos_to_cpos())
355 .map(|chunk| chunk.sites.contains(&site))
356 .unwrap_or(false)
357 };
358
359 let npc_wpos = ctx.actor.wpos;
360
361 if let Some(site) = wpos_site(npc_wpos.xy()) {
363 let mut site_exit = wpos;
364 while let Some(next) = next_point(ctx).filter(|next| wpos_sites_contain(*next, site)) {
365 site_exit = next;
366 }
367
368 if let Some(path) = path_site(npc_wpos.xy(), site_exit, site, ctx.index) {
370 let path_len = path.len();
371 ControlFlow::Continue(Either::Left(
372 seq(path.into_iter().map(move |wpos| goto_2d(wpos, 1.0, 8.0)))
373 .then(goto_2d(site_exit, speed_factor, 8.0))
374 .debug(move || {
375 format!(
376 "in site from ({}, {}) to ({}, {}), path length: {path_len}",
377 npc_wpos.x, npc_wpos.y, site_exit.x, site_exit.y,
378 )
379 }),
380 ))
381 } else {
382 ControlFlow::Continue(Either::Right(
384 goto_2d(site_exit, speed_factor, 8.0)
385 .debug(move || {
386 format!(
387 "direct from {}, {}, ({}) to site exit at {}, {}",
388 npc_wpos.x, npc_wpos.y, npc_wpos.z, site_exit.x, site_exit.y
389 )
390 })
391 .boxed(),
392 ))
393 }
394 } else {
395 ControlFlow::Continue(Either::Right(
397 goto_2d(wpos, speed_factor, 8.0)
398 .debug(move || {
399 format!(
400 "from {}, {}, ({}) to the next waypoint at {}, {}",
401 npc_wpos.x, npc_wpos.y, npc_wpos.z, wpos.x, wpos.y
402 )
403 })
404 .boxed(),
405 ))
406 }
407 })
408 .with_state(next_point)
409 .debug(|| "traverse points")
410}
411
412pub fn travel_to_point<S: State>(wpos: Vec2<f32>, speed_factor: f32) -> impl Action<S> {
414 now(move |ctx, _| {
415 const WAYPOINT: f32 = 48.0;
416 let start = ctx.actor.wpos.xy();
417 let diff = wpos - start;
418 let n = (diff.magnitude() / WAYPOINT).max(1.0);
419 let mut points = (1..n as usize + 1).map(move |i| start + diff * (i as f32 / n));
420 traverse_points(move |_| points.next(), speed_factor)
421 })
422 .debug(move || format!("travel to point {}, {}", wpos.x, wpos.y))
423}
424
425pub fn travel_to_site<S: State>(tgt_site: SiteId, speed_factor: f32) -> impl Action<S> {
427 now(move |ctx, _| {
428 let sites = &ctx.data.sites;
429
430 let site_wpos = sites.get(tgt_site).map(|site| site.wpos.as_());
431
432 if let Some(current_site) = ctx.actor.current_site
435 && let Some(tracks) = path_between_towns(current_site, tgt_site, sites, ctx.world)
436 {
437
438 let mut path_nodes = tracks.path
439 .into_iter()
440 .flat_map(move |(track_id, reversed)| (0..)
441 .map(move |node_idx| (node_idx, track_id, reversed)));
442
443 traverse_points(move |ctx| {
444 let (node_idx, track_id, reversed) = path_nodes.next()?;
445 let nodes = &ctx.world.civs().tracks.get(track_id).path().nodes;
446
447 let idx = if reversed {
449 nodes.len().checked_sub(node_idx + 1)
450 } else {
451 Some(node_idx)
452 };
453
454 if let Some(node) = idx.and_then(|idx| nodes.get(idx)) {
455 let node_chunk_wpos = TerrainChunkSize::center_wpos(*node);
457
458 Some(ctx.world.sim()
460 .get_nearest_path(node_chunk_wpos)
461 .map_or(node_chunk_wpos, |(_, wpos, _, _)| wpos.as_())
462 .as_::<f32>())
463 } else {
464 None
465 }
466 }, speed_factor)
467 .boxed()
468 } else if let Some(site) = sites.get(tgt_site) {
469 travel_to_point(site.wpos.map(|e| e as f32 + 0.5), speed_factor).debug(|| "travel to point fallback").boxed()
471 } else {
472 finish().boxed()
474 }
475 .stop_if(move |ctx: &mut NpcCtx| site_wpos.is_some_and(|site_wpos| ctx.actor.wpos.xy().distance_squared(site_wpos) < 16f32.powi(2)))
477 })
478 .debug(move || format!("travel_to_site {:?}", tgt_site))
479 .map(|_, _| ())
480}