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    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            // Since we get a, b from neighbors, track_between shouldn't return None.
127            .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
179// Actions
180
181/// Try to walk toward a 3D position without caring for obstacles.
182pub 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 we're close to the next waypoint, complete it
187        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        // Get the next waypoint on the route toward the goal
194        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    // const STEP_DIST: f32 = 30.0;
211    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            // // Don't try to path too far in one go
217            // let tgt_wpos = if dist_sqr > STEP_DIST.powi(2) {
218            //     let tgt_wpos_2d = ctx.npc.wpos.xy() + (tgt_wpos -
219            // ctx.npc.wpos).xy().normalized() * STEP_DIST;     tgt_wpos_2d.
220            // with_z(ctx.world.sim().get_surface_alt_approx(tgt_wpos_2d.as_()))
221            // } else {
222            //     tgt_wpos
223            // };
224            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
249/// Try to walk fly a 3D position following the terrain altitude at an offset
250/// without caring for obstacles.
251fn 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 we're close to the next waypoint, complete it
261        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        // Get the next waypoint on the route toward the goal
268        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
293/// Try to walk toward a 2D position on the surface without caring for
294/// obstacles.
295pub 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
307/// Try to fly toward a 2D position following the terrain altitude at an offset
308/// without caring for obstacles.
309pub 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        // Pick next waypoint, return if path ended
343        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 we're traversing within a site, do intra-site pathfinding
365        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            // Navigate through the site to the site exit
372            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                // No intra-site path found, just attempt to move towards the exit node
382                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            // We're in the middle of a road, just go to the next waypoint
395            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
411/// Try to travel to a site. Where practical, paths will be taken.
412pub 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
424/// Try to travel to a site. Where practical, paths will be taken.
425pub 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 we're currently in a site, try to find a path to the target site via
432        // tracks
433        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                // Handle the case where we walk paths backward
447                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                    // Find the centre of the track node's chunk
455                    let node_chunk_wpos = TerrainChunkSize::center_wpos(*node);
456
457                    // Refine the node position a bit more based on local path information
458                    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            // If all else fails, just walk toward the target site in a straight line
469            travel_to_point(site.wpos.map(|e| e as f32 + 0.5), speed_factor).debug(|| "travel to point fallback").boxed()
470        } else {
471            // If we can't find a way to get to the site at all, there's nothing more to be done
472            finish().boxed()
473        }
474            // Stop the NPC early if we're near the site to prevent huddling around the centre
475            .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}