Skip to main content

veloren_rtsim/rule/npc_ai/
movement.rs

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            // Since we get a, b from neighbors, track_between shouldn't return None.
126            .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
178// Actions
179
180/// Try to walk toward a 3D position without caring for obstacles.
181pub 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 we're close to the next waypoint, complete it
186        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        // Get the next waypoint on the route toward the goal
193        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    // const STEP_DIST: f32 = 30.0;
210    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            // // Don't try to path too far in one go
216            // let tgt_wpos = if dist_sqr > STEP_DIST.powi(2) {
217            //     let tgt_wpos_2d = ctx.actor.wpos.xy() + (tgt_wpos -
218            // ctx.actor.wpos).xy().normalized() * STEP_DIST;     tgt_wpos_2d.
219            // with_z(ctx.world.sim().get_surface_alt_approx(tgt_wpos_2d.as_()))
220            // } else {
221            //     tgt_wpos
222            // };
223            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
248/// Try to walk fly a 3D position following the terrain altitude at an offset
249/// without caring for obstacles.
250fn 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 we're close to the next waypoint, complete it
260        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        // Get the next waypoint on the route toward the goal
267        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
290/// Try to walk toward a 2D position on the surface without caring for
291/// obstacles.
292pub 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
304/// Try to fly toward a 2D position following the terrain altitude at an offset
305/// without caring for obstacles.
306pub 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        // Pick next waypoint, return if path ended
340        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 we're traversing within a site, do intra-site pathfinding
362        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            // Navigate through the site to the site exit
369            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                // No intra-site path found, just attempt to move towards the exit node
383                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            // We're in the middle of a road, just go to the next waypoint
396            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
412/// Try to travel to a site. Where practical, paths will be taken.
413pub 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
425/// Try to travel to a site. Where practical, paths will be taken.
426pub 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 we're currently in a site, try to find a path to the target site via
433        // tracks
434        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                // Handle the case where we walk paths backward
448                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                    // Find the centre of the track node's chunk
456                    let node_chunk_wpos = TerrainChunkSize::center_wpos(*node);
457
458                    // Refine the node position a bit more based on local path information
459                    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            // If all else fails, just walk toward the target site in a straight line
470            travel_to_point(site.wpos.map(|e| e as f32 + 0.5), speed_factor).debug(|| "travel to point fallback").boxed()
471        } else {
472            // If we can't find a way to get to the site at all, there's nothing more to be done
473            finish().boxed()
474        }
475            // Stop the NPC early if we're near the site to prevent huddling around the centre
476            .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}