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