veloren_rtsim/rule/npc_ai/
mod.rs

1//! This rule is by far the most significant rule in rtsim to date and governs
2//! the behaviour of rtsim NPCs. It uses a novel combinator-based API to express
3//! long-running NPC actions in a manner that's halfway between [async/coroutine programming](https://en.wikipedia.org/wiki/Coroutine) and traditional
4//! [AI decision trees](https://en.wikipedia.org/wiki/Decision_tree).
5//!
6//! It may feel unintuitive when you first work with it, but trust us:
7//! expressing your AI behaviour in this way brings radical advantages and will
8//! simplify your code and make debugging exponentially easier.
9//!
10//! The fundamental abstraction is that of [`Action`]s. [`Action`]s, somewhat
11//! like [`core::future::Future`], represent a long-running behaviour performed
12//! by an NPC. See [`Action`] for a deeper explanation of actions and the
13//! methods that can be used to combine them together.
14//!
15//! NPC actions act upon the NPC's [`crate::data::npc::Controller`]. This type
16//! represent the immediate behavioural intentions of the NPC during simulation,
17//! such as by specifying a location to walk to, an action to perform, speech to
18//! say, or some persistent state to change (like the NPC's home site).
19//!
20//! After brain simulation has occurred, the resulting controller state is
21//! passed to either rtsim's internal NPC simulation rule
22//! ([`crate::rule::simulate_npcs`]) or, if the chunk the NPC is loaded, are
23//! passed to the Veloren server's agent system which attempts to act in
24//! accordance with it.
25
26mod airship_ai;
27pub mod dialogue;
28pub mod movement;
29pub mod util;
30
31use std::{collections::VecDeque, hash::BuildHasherDefault, sync::Arc};
32
33use crate::{
34    RtState, Rule, RuleError,
35    ai::{
36        Action, NpcCtx, State, casual, choose, finish, important, just, now,
37        predicate::{Chance, EveryRange, Predicate, every_range, timeout},
38        seq, until,
39    },
40    data::{
41        ReportKind, Sentiment, Sites,
42        npc::{Brain, DialogueSession, PathData, SimulationMode},
43    },
44    event::OnTick,
45};
46use common::{
47    assets::AssetExt,
48    astar::{Astar, PathResult},
49    comp::{
50        self, Content, bird_large,
51        compass::{Direction, Distance},
52        item::ItemDef,
53    },
54    path::Path,
55    rtsim::{
56        Actor, ChunkResource, DialogueKind, NpcInput, PersonalityTrait, Profession, Response, Role,
57        SiteId,
58    },
59    spiral::Spiral2d,
60    store::Id,
61    terrain::{CoordinateConversions, TerrainChunkSize, sprite},
62    time::DayPeriod,
63    util::Dir,
64};
65use core::ops::ControlFlow;
66use fxhash::FxHasher64;
67use itertools::{Either, Itertools};
68use rand::prelude::*;
69use rand_chacha::ChaChaRng;
70use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
71use vek::*;
72use world::{
73    IndexRef, World,
74    civ::{self, Track},
75    site::{
76        self, PlotKind, Site as WorldSite, SiteKind, TileKind,
77        plot::{PlotKindMeta, tavern},
78    },
79    util::NEIGHBORS,
80};
81
82use self::{
83    movement::{
84        follow_actor, goto, goto_2d, goto_2d_flying, goto_actor, travel_to_point, travel_to_site,
85    },
86    util::do_dialogue,
87};
88
89/// How many ticks should pass between running NPC AI.
90/// Note that this only applies to simulated NPCs: loaded NPCs have their AI
91/// code run every tick. This means that AI code should be broadly
92/// DT-independent.
93const SIMULATED_TICK_SKIP: u64 = 10;
94
95pub struct NpcAi;
96
97#[derive(Clone)]
98struct DefaultState {
99    socialize_timer: EveryRange,
100    move_home_timer: Chance<EveryRange>,
101}
102
103impl Rule for NpcAi {
104    fn start(rtstate: &mut RtState) -> Result<Self, RuleError> {
105        // Keep track of the last `SIMULATED_TICK_SKIP` ticks, to know the deltatime
106        // since the last tick we ran the npc.
107        let mut last_ticks: VecDeque<_> = [1.0 / 30.0; SIMULATED_TICK_SKIP as usize]
108            .into_iter()
109            .collect();
110
111        rtstate.bind::<Self, OnTick>(move |ctx| {
112            last_ticks.push_front(ctx.event.dt);
113            if last_ticks.len() >= SIMULATED_TICK_SKIP as usize {
114                last_ticks.pop_back();
115            }
116            // Temporarily take the brains of NPCs out of their heads to appease the borrow
117            // checker
118            let mut npc_data = {
119                let mut data = ctx.state.data_mut();
120                data.npcs
121                    .iter_mut()
122                    // Don't run AI for dead NPCs
123                    .filter(|(_, npc)| !npc.is_dead() && !matches!(npc.role, Role::Vehicle))
124                    // Don't run AI for simulated NPCs every tick
125                    .filter(|(_, npc)| matches!(npc.mode, SimulationMode::Loaded) || (npc.seed as u64 + ctx.event.tick) % SIMULATED_TICK_SKIP == 0)
126                    .map(|(npc_id, npc)| {
127                        let controller = std::mem::take(&mut npc.controller);
128                        let inbox = std::mem::take(&mut npc.inbox);
129                        let sentiments = std::mem::take(&mut npc.sentiments);
130                        let known_reports = std::mem::take(&mut npc.known_reports);
131                        let brain = npc.brain.take().unwrap_or_else(|| Brain {
132                            action: Box::new(think().repeat().with_state(DefaultState {
133                                socialize_timer: every_range(15.0..30.0),
134                                move_home_timer: every_range(400.0..2000.0).chance(0.5),
135                            })),
136                        });
137                        (npc_id, controller, inbox, sentiments, known_reports, brain)
138                    })
139                    .collect::<Vec<_>>()
140            };
141
142            // The sum of the last `SIMULATED_TICK_SKIP` tick deltatimes is the deltatime since
143            // simulated npcs ran this tick had their ai ran.
144            let simulated_dt = last_ticks.iter().sum::<f32>();
145
146            // Do a little thinking
147            {
148                let data = &*ctx.state.data();
149
150                npc_data
151                    .par_iter_mut()
152                    .for_each(|(npc_id, controller, inbox, sentiments, known_reports, brain)| {
153                        let npc = &data.npcs[*npc_id];
154
155                        controller.reset();
156
157                        brain.action.tick(&mut NpcCtx {
158                            state: ctx.state,
159                            world: ctx.world,
160                            index: ctx.index,
161                            time_of_day: ctx.event.time_of_day,
162                            time: ctx.event.time,
163                            npc,
164                            npc_id: *npc_id,
165                            controller,
166                            inbox,
167                            known_reports,
168                            sentiments,
169                            dt: if matches!(npc.mode, SimulationMode::Loaded) {
170                                ctx.event.dt
171                            } else {
172                                simulated_dt
173                            },
174                            rng: ChaChaRng::from_seed(thread_rng().gen::<[u8; 32]>()),
175                            system_data: &*ctx.system_data,
176                        }, &mut ());
177
178                        // If an input wasn't processed by the brain, we no longer have a use for it
179                        inbox.clear();
180                    });
181            }
182
183            // Reinsert NPC brains
184            let mut data = ctx.state.data_mut();
185            for (npc_id, controller, inbox, sentiments, known_reports, brain) in npc_data {
186                data.npcs[npc_id].controller = controller;
187                data.npcs[npc_id].brain = Some(brain);
188                data.npcs[npc_id].inbox = inbox;
189                data.npcs[npc_id].sentiments = sentiments;
190                data.npcs[npc_id].known_reports = known_reports;
191            }
192        });
193
194        Ok(Self)
195    }
196}
197
198fn idle<S: State>() -> impl Action<S> + Clone {
199    just(|ctx, _| ctx.controller.do_idle()).debug(|| "idle")
200}
201
202fn talk_to<S: State>(tgt: Actor) -> impl Action<S> {
203    now(move |ctx, _| {
204        if ctx.sentiments.toward(tgt).is(Sentiment::ENEMY) {
205            just(move |ctx, _| {
206                ctx.controller
207                    .say(tgt, Content::localized("npc-speech-reject_rival"))
208            })
209            .boxed()
210        } else if matches!(tgt, Actor::Character(_)) {
211            do_dialogue(tgt, move |session| dialogue::general(tgt, session)).boxed()
212        } else {
213            smalltalk_to(tgt).boxed()
214        }
215    })
216}
217
218fn tell_site_content(ctx: &NpcCtx, site: SiteId) -> Option<Content> {
219    if let Some(world_site) = ctx.state.data().sites.get(site)
220        && let Some(site_name) = util::site_name(ctx, site)
221    {
222        Some(Content::localized_with_args("npc-speech-tell_site", [
223            ("site", Content::Plain(site_name)),
224            (
225                "dir",
226                Direction::from_dir(world_site.wpos.as_() - ctx.npc.wpos.xy()).localize_npc(),
227            ),
228            (
229                "dist",
230                Distance::from_length(world_site.wpos.as_().distance(ctx.npc.wpos.xy()) as i32)
231                    .localize_npc(),
232            ),
233        ]))
234    } else {
235        None
236    }
237}
238
239fn smalltalk_to<S: State>(tgt: Actor) -> impl Action<S> {
240    now(move |ctx, _| {
241        if matches!(tgt, Actor::Npc(_)) && ctx.rng.gen_bool(0.2) {
242            // Cut off the conversation sometimes to avoid infinite conversations (but only
243            // if the target is an NPC!) TODO: Don't special case this, have
244            // some sort of 'bored of conversation' system
245            idle().boxed()
246        } else {
247            // Mention nearby sites
248            let comment = if ctx.rng.gen_bool(0.3)
249                && let Some(current_site) = ctx.npc.current_site
250                && let Some(current_site) = ctx.state.data().sites.get(current_site)
251                && let Some(mention_site) = current_site.nearby_sites_by_size.choose(&mut ctx.rng)
252                && let Some(content) = tell_site_content(ctx, *mention_site)
253            {
254                content
255            // Mention current site
256            } else if ctx.rng.gen_bool(0.3)
257                && let Some(current_site) = ctx.npc.current_site
258                && let Some(current_site_name) = util::site_name(ctx, current_site)
259            {
260                Content::localized_with_args("npc-speech-site", [(
261                    "site",
262                    Content::Plain(current_site_name),
263                )])
264
265            // Mention nearby monsters
266            } else if ctx.rng.gen_bool(0.3)
267                && let Some(monster) = ctx
268                    .state
269                    .data()
270                    .npcs
271                    .values()
272                    .filter(|other| matches!(&other.role, Role::Monster))
273                    .min_by_key(|other| other.wpos.xy().distance(ctx.npc.wpos.xy()) as i32)
274            {
275                Content::localized_with_args("npc-speech-tell_monster", [
276                    ("body", monster.body.localize_npc()),
277                    (
278                        "dir",
279                        Direction::from_dir(monster.wpos.xy() - ctx.npc.wpos.xy()).localize_npc(),
280                    ),
281                    (
282                        "dist",
283                        Distance::from_length(monster.wpos.xy().distance(ctx.npc.wpos.xy()) as i32)
284                            .localize_npc(),
285                    ),
286                ])
287            // Specific night dialog
288            } else if ctx.rng.gen_bool(0.6) && DayPeriod::from(ctx.time_of_day.0).is_dark() {
289                Content::localized("npc-speech-night")
290            } else {
291                ctx.npc.personality.get_generic_comment(&mut ctx.rng)
292            };
293            // TODO: Don't special-case players
294            let wait = if matches!(tgt, Actor::Character(_)) {
295                0.0
296            } else {
297                1.5
298            };
299            idle()
300                .repeat()
301                .stop_if(timeout(wait))
302                .then(just(move |ctx, _| ctx.controller.say(tgt, comment.clone())))
303                .boxed()
304        }
305    })
306}
307
308fn socialize() -> impl Action<EveryRange> {
309    now(move |ctx, socialize: &mut EveryRange| {
310        // Skip most socialising actions if we're not loaded
311        if matches!(ctx.npc.mode, SimulationMode::Loaded)
312            && socialize.should(ctx)
313            && !ctx.npc.personality.is(PersonalityTrait::Introverted)
314        {
315            // Sometimes dance
316            if ctx.rng.gen_bool(0.15) {
317                return just(|ctx, _| ctx.controller.do_dance(None))
318                    .repeat()
319                    .stop_if(timeout(6.0))
320                    .debug(|| "dancing")
321                    .map(|_, _| ())
322                    .l()
323                    .l();
324            // Talk to nearby NPCs
325            } else if let Some(other) = ctx
326                .state
327                .data()
328                .npcs
329                .nearby(Some(ctx.npc_id), ctx.npc.wpos, 8.0)
330                .choose(&mut ctx.rng)
331            {
332                return smalltalk_to(other)
333                    // After talking, wait for a while
334                    .then(idle().repeat().stop_if(timeout(4.0)))
335                    .map(|_, _| ())
336                    .r().l();
337            }
338        }
339        idle().r()
340    })
341}
342
343fn adventure() -> impl Action<DefaultState> {
344    choose(|ctx, _| {
345        // Choose a random site that's fairly close by
346        if let Some(tgt_site) = ctx
347            .state
348            .data()
349            .sites
350            .iter()
351            .filter(|(site_id, site)| {
352                site.world_site.is_some_and(|ws| ctx.index.sites.get(ws).any_plot(|plot| matches!(plot.meta(), Some(PlotKindMeta::Workshop { .. })))) && (ctx.npc.current_site != Some(*site_id))
353                    && ctx.rng.gen_bool(0.25)
354            })
355            .min_by_key(|(_, site)| site.wpos.as_().distance(ctx.npc.wpos.xy()) as i32)
356            .map(|(site_id, _)| site_id)
357        {
358            let wait_time = if matches!(ctx.npc.profession(), Some(Profession::Merchant)) {
359                60.0 * 15.0
360            } else {
361                60.0 * 3.0
362            };
363            let site_name = util::site_name(ctx, tgt_site).unwrap_or_default();
364            // Travel to the site
365            important(just(move |ctx, _| ctx.controller.say(None, Content::localized_with_args("npc-speech-moving_on", [("site", site_name.clone())])))
366                          .then(travel_to_site(tgt_site, 0.6))
367                          // Stop for a few minutes
368                          .then(villager(tgt_site).repeat().stop_if(timeout(wait_time)))
369                          .map(|_, _| ())
370                          .boxed(),
371            )
372        } else {
373            casual(finish().boxed())
374        }
375    })
376    .debug(move || "adventure")
377}
378
379fn hired<S: State>(tgt: Actor) -> impl Action<S> {
380    follow_actor(tgt, 5.0)
381        // Stop following if we're no longer hired
382        .stop_if(move |ctx: &mut NpcCtx| ctx.npc.hiring.is_none_or(|(a, _)| a != tgt))
383        .debug(move|| format!("hired by {tgt:?}"))
384        .interrupt_with(move |ctx, _| {
385            // End hiring for various reasons
386            if let Some((tgt, expires)) = ctx.npc.hiring {
387                // Hiring period has expired
388                if ctx.time > expires {
389                    ctx.controller.end_hiring();
390                    // If the actor exists, tell them that the hiring is over
391                    if util::actor_exists(ctx, tgt) {
392                        return Some(goto_actor(tgt, 2.0)
393                            .then(do_dialogue(tgt, |session| {
394                                session.say_statement(Content::localized("npc-dialogue-hire_expired"))
395                            }))
396                            .boxed());
397                    }
398                }
399
400                if ctx.sentiments.toward(tgt).is(Sentiment::RIVAL) {
401                    ctx.controller.end_hiring();
402                    // If the actor exists, tell them that the hiring is over
403                    if util::actor_exists(ctx, tgt) {
404                        return Some(goto_actor(tgt, 2.0)
405                            .then(do_dialogue(tgt, |session| {
406                                session.say_statement(Content::localized(
407                                    "npc-dialogue-hire_cancelled_unhappy",
408                                ))
409                            }))
410                            .boxed());
411                    }
412                }
413            }
414
415            None
416        })
417        .map(|_, _| ())
418}
419
420fn gather_ingredients<S: State>() -> impl Action<S> {
421    just(|ctx, _| {
422        ctx.controller.do_gather(
423            &[
424                ChunkResource::Fruit,
425                ChunkResource::Mushroom,
426                ChunkResource::Plant,
427            ][..],
428        )
429    })
430    .debug(|| "gather ingredients")
431}
432
433fn hunt_animals<S: State>() -> impl Action<S> {
434    just(|ctx, _| ctx.controller.do_hunt_animals()).debug(|| "hunt_animals")
435}
436
437fn find_forest(ctx: &mut NpcCtx) -> Option<Vec2<f32>> {
438    let chunk_pos = ctx.npc.wpos.xy().as_().wpos_to_cpos();
439    Spiral2d::new()
440        .skip(ctx.rng.gen_range(1..=64))
441        .take(24)
442        .map(|rpos| chunk_pos + rpos)
443        .find(|cpos| {
444            ctx.world
445                .sim()
446                .get(*cpos)
447                .is_some_and(|c| c.tree_density > 0.75 && c.surface_veg > 0.5)
448        })
449        .map(|chunk| TerrainChunkSize::center_wpos(chunk).as_())
450}
451
452fn find_farm(ctx: &mut NpcCtx, site: SiteId) -> Option<Vec2<f32>> {
453    ctx.state.data().sites.get(site).and_then(|site| {
454        let site = ctx.index.sites.get(site.world_site?);
455        let farm = site
456            .filter_plots(|p| matches!(p.kind(), PlotKind::FarmField(_)))
457            .choose(&mut ctx.rng)?;
458
459        Some(site.tile_center_wpos(farm.root_tile()).as_())
460    })
461}
462
463fn choose_plaza(ctx: &mut NpcCtx, site: SiteId) -> Option<Vec2<f32>> {
464    ctx.state.data().sites.get(site).and_then(|site| {
465        let site = ctx.index.sites.get(site.world_site?);
466        let plaza = &site.plots[site.plazas().choose(&mut ctx.rng)?];
467        let tile = plaza
468            .tiles()
469            .choose(&mut ctx.rng)
470            .unwrap_or_else(|| plaza.root_tile());
471        Some(site.tile_center_wpos(tile).as_())
472    })
473}
474
475const WALKING_SPEED: f32 = 0.35;
476
477fn villager(visiting_site: SiteId) -> impl Action<DefaultState> {
478    choose(move |ctx, state: &mut DefaultState| {
479        // Consider moving home if the home site gets too full
480        if state.move_home_timer.should(ctx)
481            && let Some(home) = ctx.npc.home
482            && Some(home) == ctx.npc.current_site
483            && let Some(home_pop_ratio) = ctx.state.data().sites.get(home)
484                .and_then(|site| Some((site, ctx.index.sites.get(site.world_site?))))
485                .and_then(|(site, world_site)| { let houses = world_site.filter_plots(|p| matches!(p.meta(), Some(PlotKindMeta::House { .. }))).count(); if houses == 0 { return None } Some(site.population.len() as f32 / houses as f32) } )
486                // Only consider moving if the population is more than 1.5x the number of homes
487                .filter(|pop_ratio| *pop_ratio > 1.5)
488            && let Some(new_home) = ctx
489                .state
490                .data()
491                .sites
492                .iter()
493                // Don't try to move to the site that's currently our home
494                .filter(|(site_id, _)| Some(*site_id) != ctx.npc.home)
495                // Only consider towns as potential homes
496                .filter_map(|(site_id, site)| {
497                    let world_site = site.world_site.map(|ws| ctx.index.sites.get(ws))?;
498                    let house_count = world_site.filter_plots(|p| matches!(p.meta(), Some(PlotKindMeta::House { .. }))).count();
499
500                    if house_count == 0 {
501                        return None;
502                    }
503                    Some((site_id, site, house_count))
504                })
505                // Only select sites that are less densely populated than our own
506                .filter(|(_, site, houses)| (site.population.len() as f32 / *houses as f32) < home_pop_ratio)
507                // Find the closest of the candidate sites
508                .min_by_key(|(_, site, _)| site.wpos.as_().distance(ctx.npc.wpos.xy()) as i32)
509                .map(|(site_id, _, _)| site_id)
510        {
511            let site_name = util::site_name(ctx, new_home);
512            return important(just(move |ctx, _| {
513                if let Some(site_name) = &site_name {
514                    ctx.controller.say(None, Content::localized_with_args("npc-speech-migrating", [("site", site_name.clone())]))
515                }
516            })
517                .then(travel_to_site(new_home, 0.5))
518                .then(just(move |ctx, _| ctx.controller.set_new_home(new_home))));
519        }
520        let day_period = DayPeriod::from(ctx.time_of_day.0);
521        let is_weekend = ctx.time_of_day.day() as u64 % 6 == 0;
522        let is_evening = day_period == DayPeriod::Evening;
523
524        let is_free_time = is_weekend || is_evening;
525
526        let is_raining = ctx.system_data.weather_grid.is_raining(ctx.npc.wpos.xy());
527
528        // Go to a house if it's dark
529        if day_period.is_dark()
530            && !matches!(ctx.npc.profession(), Some(Profession::Guard))
531        {
532            return important(
533                now(move |ctx, _| {
534                    if let Some(house_wpos) = ctx
535                        .state
536                        .data()
537                        .sites
538                        .get(visiting_site)
539                        .and_then(|site| Some(ctx.index.sites.get(site.world_site?)))
540                        .and_then(|site| {
541                            // Find a house in the site we're visiting
542                            let house = site
543                                .plots()
544                                .filter(|p| matches!(p.kind().meta(), Some(PlotKindMeta::House { .. })))
545                                .choose(&mut ctx.rng)?;
546                            Some(site.tile_center_wpos(house.root_tile()).as_())
547                        })
548                    {
549                        just(|ctx, _| {
550                            ctx.controller
551                                .say(None, Content::localized("npc-speech-night_time"))
552                        })
553                        .then(travel_to_point(house_wpos, 0.65))
554                        .debug(|| "walk to house")
555                        .then(socialize().repeat().map_state(|state: &mut DefaultState| &mut state.socialize_timer).debug(|| "wait in house"))
556                        .stop_if(|ctx: &mut NpcCtx| DayPeriod::from(ctx.time_of_day.0).is_light())
557                        .then(just(|ctx, _| {
558                            ctx.controller
559                                .say(None, Content::localized("npc-speech-day_time"))
560                        }))
561                        .map(|_, _| ())
562                        .boxed()
563                    } else {
564                        finish().boxed()
565                    }
566                })
567                .debug(|| "find somewhere to sleep"),
568            );
569        }
570        // Go to a house if its raining
571        else if is_raining
572            && !matches!(ctx.npc.profession(), Some(Profession::Guard))
573        {
574            return important(
575                now(move |ctx, _| {
576                    if let Some(house_wpos) = ctx
577                        .state
578                        .data()
579                        .sites
580                        .get(visiting_site)
581                        .and_then(|site| Some(ctx.index.sites.get(site.world_site?)))
582                        .and_then(|site| {
583                            // Find a house in the site we're visiting
584                            let house = site
585                                .plots()
586                                .filter(|p| matches!(p.kind().meta(), Some(PlotKindMeta::House { .. })))
587                                .choose(&mut ctx.rng)?;
588                            Some(site.tile_center_wpos(house.root_tile()).as_())
589                        })
590                    {
591                        just(|ctx, _| {
592                                ctx.controller.say(None, Content::localized("npc-speech-seeking_shelter_rain"))
593                        })
594                        .then(travel_to_point(house_wpos, 0.65))
595                        .debug(|| "walk to house (rain)")
596                        .then(socialize().repeat().map_state(|state: &mut DefaultState| &mut state.socialize_timer).debug(|| "wait in house (rain)"))
597                        .stop_if(|ctx: &mut NpcCtx| {
598                                    let is_raining = ctx.system_data.weather_grid.is_raining(ctx.npc.wpos.xy());
599                                    !is_raining
600                    })
601                        .then(just(|ctx, _| {
602                                ctx.controller.say(None, Content::localized("npc-speech-rain_stopped"))
603                        }))
604                        .map(|_, _| ())
605                        .boxed()
606                        } else {
607                        finish().boxed()
608                    }
609                })
610                .debug(|| "find somewhere to wait (rain)"),
611            );
612        }
613        // Go do something fun on evenings and holidays, or on random days.
614        else if
615            // Ain't no rest for the wicked
616            !matches!(ctx.npc.profession(), Some(Profession::Guard | Profession::Chef))
617            && (matches!(day_period, DayPeriod::Evening) || is_free_time || ctx.rng.gen_bool(0.05)) {
618            let mut fun_activities = Vec::new();
619
620            if let Some(ws_id) = ctx.state.data().sites[visiting_site].world_site {
621                let ws = ctx.index.sites.get(ws_id);
622                if let Some(arena) = ws.plots().find_map(|p| match p.kind() { PlotKind::DesertCityArena(a) => Some(a), _ => None}) {
623                    let wait_time = ctx.rng.gen_range(100.0..300.0);
624                    // We don't use Z coordinates for seats because they are complicated to calculate from the Ramp procedural generation
625                    // and using goto_2d seems to work just fine. However it also means that NPC will never go seat on the stands
626                    // on the first floor of the arena. This is a compromise that was made because in the current arena procedural generation
627                    // there is also no pathways to the stands on the first floor for NPCs.
628                    let arena_center = Vec3::new(arena.center.x, arena.center.y, arena.base).as_::<f32>();
629                    let stand_dist = arena.stand_dist as f32;
630                    let seat_var_width = ctx.rng.gen_range(0..arena.stand_width) as f32;
631                    let seat_var_length = ctx.rng.gen_range(-arena.stand_length..arena.stand_length) as f32;
632                    // Select a seat on one of the 4 arena stands
633                    let seat = match ctx.rng.gen_range(0..4) {
634                        0 => Vec3::new(arena_center.x - stand_dist + seat_var_width, arena_center.y + seat_var_length, arena_center.z),
635                        1 => Vec3::new(arena_center.x + stand_dist - seat_var_width, arena_center.y + seat_var_length, arena_center.z),
636                        2 => Vec3::new(arena_center.x + seat_var_length, arena_center.y - stand_dist + seat_var_width, arena_center.z),
637                        _ => Vec3::new(arena_center.x + seat_var_length, arena_center.y + stand_dist - seat_var_width, arena_center.z),
638                    };
639                    let look_dir = Dir::from_unnormalized(arena_center - seat);
640                    // Walk to an arena seat, cheer, sit and dance
641                    let action = casual(just(move |ctx, _| ctx.controller.say(None, Content::localized("npc-speech-arena")))
642                            .then(goto_2d(seat.xy(), 0.6, 1.0).debug(|| "go to arena"))
643                            // Turn toward the centre of the arena and watch the action!
644                            .then(choose(move |ctx, _| if ctx.rng.gen_bool(0.3) {
645                                casual(just(move |ctx,_| ctx.controller.do_cheer(look_dir)).repeat().stop_if(timeout(5.0)))
646                            } else if ctx.rng.gen_bool(0.15) {
647                                casual(just(move |ctx,_| ctx.controller.do_dance(look_dir)).repeat().stop_if(timeout(5.0)))
648                            } else {
649                                casual(just(move |ctx,_| ctx.controller.do_sit(look_dir, None)).repeat().stop_if(timeout(15.0)))
650                            })
651                                .repeat()
652                                .stop_if(timeout(wait_time)))
653                            .map(|_, _| ())
654                            .boxed());
655                    fun_activities.push(action);
656                }
657                if let Some(tavern) = ws.plots().filter_map(|p| match p.kind() {  PlotKind::Tavern(a) => Some(a), _ => None }).choose(&mut ctx.rng) {
658                    let tavern_name = tavern.name.clone();
659                    let wait_time = ctx.rng.gen_range(100.0..300.0);
660
661                    let (stage_aabr, stage_z) = tavern.rooms.values().flat_map(|room| {
662                        room.details.iter().filter_map(|detail| match detail {
663                            tavern::Detail::Stage { aabr } => Some((*aabr, room.bounds.min.z + 1)),
664                            _ => None,
665                        })
666                    }).choose(&mut ctx.rng).unwrap_or((tavern.bounds, tavern.door_wpos.z));
667
668                    let bar_pos = tavern.rooms.values().flat_map(|room|
669                        room.details.iter().filter_map(|detail| match detail {
670                            tavern::Detail::Bar { aabr } => {
671                                let side = site::util::Dir::from_vec2(room.bounds.center().xy() - aabr.center());
672                                let pos = side.select_aabr_with(*aabr, aabr.center()) + side.to_vec2();
673
674                                Some(pos.with_z(room.bounds.min.z))
675                            }
676                            _ => None,
677                        })
678                    ).choose(&mut ctx.rng).unwrap_or(stage_aabr.center().with_z(stage_z));
679
680                    // Pick a chair that is theirs for the stay
681                    let chair_pos = tavern.rooms.values().flat_map(|room| {
682                        let z = room.bounds.min.z;
683                        room.details.iter().filter_map(move |detail| match detail {
684                            tavern::Detail::Table { pos, chairs } => Some(chairs.into_iter().map(move |dir| pos.with_z(z) + dir.to_vec2())),
685                            _ => None,
686                        })
687                        .flatten()
688                    }
689                    ).choose(&mut ctx.rng)
690                    // This path is possible, but highly unlikely.
691                    .unwrap_or(bar_pos);
692
693                    let stage_aabr = stage_aabr.as_::<f32>();
694                    let stage_z = stage_z as f32;
695
696                    let action = casual(travel_to_point(tavern.door_wpos.xy().as_() + 0.5, 0.8).then(choose(move |ctx, (last_action, _)| {
697                            let action = [0, 1, 2].into_iter().filter(|i| *last_action != Some(*i)).choose(&mut ctx.rng).expect("We have at least 2 elements");
698                            let socialize_repeat = || socialize().map_state(|(_, timer)| timer).repeat();
699                            match action {
700                                // Go and dance on a stage.
701                                0 => {
702                                    casual(
703                                        now(move |ctx, (last_action, _)| {
704                                            *last_action = Some(action);
705                                            goto(stage_aabr.min.map2(stage_aabr.max, |a, b| ctx.rng.gen_range(a..b)).with_z(stage_z), WALKING_SPEED, 1.0)
706                                        })
707                                        .then(just(move |ctx,_| ctx.controller.do_dance(None)).repeat().stop_if(timeout(ctx.rng.gen_range(20.0..30.0))))
708                                        .map(|_, _| ())
709                                        .debug(|| "Dancing on the stage")
710                                    )
711                                },
712                                // Go and sit at a table.
713                                1 => {
714                                    casual(
715                                        now(move |ctx, (last_action, _)| {
716                                            *last_action = Some(action);
717                                            goto(chair_pos.as_() + 0.5, WALKING_SPEED, 1.0)
718                                                .then(just(move |ctx, _| ctx.controller.do_sit(None, Some(chair_pos)))
719                                                    // .then(socialize().map_state(|(_, timer)| timer))
720                                                    .repeat().stop_if(timeout(ctx.rng.gen_range(30.0..60.0)))
721                                                )
722                                                .map(|_, _| ())
723                                        })
724                                        .debug(move || format!("Sitting in a chair at {} {} {}", chair_pos.x, chair_pos.y, chair_pos.z))
725                                    )
726                                },
727                                // Go to the bar.
728                                _ => {
729                                    casual(
730                                        now(move |ctx, (last_action, _)| {
731                                            *last_action = Some(action);
732                                            goto(bar_pos.as_() + 0.5, WALKING_SPEED, 1.0).then(socialize_repeat().stop_if(timeout(ctx.rng.gen_range(10.0..25.0)))).map(|_, _| ())
733                                        }).debug(|| "At the bar")
734                                    )
735                                },
736                            }
737                        })
738                        .with_state((None::<u32>, every_range(5.0..10.0)))
739                        .repeat()
740                        .stop_if(timeout(wait_time)))
741                        .map(|_, _| ())
742                        .debug(move || format!("At the tavern '{}'", tavern_name))
743                        .boxed()
744                    );
745
746                    fun_activities.push(action);
747                }
748            }
749
750
751            if !fun_activities.is_empty() {
752                let i = ctx.rng.gen_range(0..fun_activities.len());
753                return fun_activities.swap_remove(i);
754            }
755        }
756        // Villagers with roles should perform those roles
757        else if matches!(ctx.npc.profession(), Some(Profession::Herbalist)) && ctx.rng.gen_bool(0.8)
758        {
759            if let Some(forest_wpos) = find_forest(ctx) {
760                return casual(
761                    travel_to_point(forest_wpos, 0.5)
762                        .debug(|| "walk to forest")
763                        .then({
764                            let wait_time = ctx.rng.gen_range(10.0..30.0);
765                            gather_ingredients().repeat().stop_if(timeout(wait_time))
766                        })
767                        .map(|_, _| ()),
768                );
769            }
770        } else if matches!(ctx.npc.profession(), Some(Profession::Farmer)) && ctx.rng.gen_bool(0.8)
771        {
772            if let Some(farm_wpos) = find_farm(ctx, visiting_site) {
773                return casual(
774                    travel_to_point(farm_wpos, 0.5)
775                        .debug(|| "walk to farm")
776                        .then({
777                            let wait_time = ctx.rng.gen_range(30.0..120.0);
778                            gather_ingredients().repeat().stop_if(timeout(wait_time))
779                        })
780                        .map(|_, _| ()),
781                );
782            }
783        } else if matches!(ctx.npc.profession(), Some(Profession::Hunter)) && ctx.rng.gen_bool(0.8) {
784            if let Some(forest_wpos) = find_forest(ctx) {
785                return casual(
786                    just(|ctx, _| {
787                        ctx.controller
788                            .say(None, Content::localized("npc-speech-start_hunting"))
789                    })
790                    .then(travel_to_point(forest_wpos, 0.75))
791                    .debug(|| "walk to forest")
792                    .then({
793                        let wait_time = ctx.rng.gen_range(30.0..60.0);
794                        hunt_animals().repeat().stop_if(timeout(wait_time))
795                    })
796                    .map(|_, _| ()),
797                );
798            }
799        } else if matches!(ctx.npc.profession(), Some(Profession::Guard)) && ctx.rng.gen_bool(0.7) {
800            if let Some(plaza_wpos) = choose_plaza(ctx, visiting_site) {
801                return casual(
802                    travel_to_point(plaza_wpos, 0.4)
803                        .debug(|| "patrol")
804                        .interrupt_with(move |ctx, _| {
805                            if ctx.rng.gen_bool(0.0003) {
806                                Some(just(move |ctx, _| {
807                                    ctx.controller
808                                        .say(None, Content::localized("npc-speech-guard_thought"))
809                                }))
810                            } else {
811                                None
812                            }
813                        })
814                        .map(|_, _| ()),
815                );
816            }
817        } else if matches!(ctx.npc.profession(), Some(Profession::Merchant)) && ctx.rng.gen_bool(0.8)
818        {
819            return casual(
820                just(|ctx, _| {
821                    // Try to direct our speech at nearby actors, if there are any
822                    let (target, phrase) = if ctx.rng.gen_bool(0.3) && let Some(other) = ctx
823                        .state
824                        .data()
825                        .npcs
826                        .nearby(Some(ctx.npc_id), ctx.npc.wpos, 8.0)
827                        .choose(&mut ctx.rng)
828                    {
829                        (Some(other), "npc-speech-merchant_sell_directed")
830                    } else {
831                        // Otherwise, resort to generic expressions
832                        (None, "npc-speech-merchant_sell_undirected")
833                    };
834
835                    ctx.controller.say(target, Content::localized(phrase));
836                })
837                .then(idle().repeat().stop_if(timeout(8.0)))
838                .repeat()
839                .stop_if(timeout(60.0))
840                .debug(|| "sell wares")
841                .map(|_, _| ()),
842            );
843        } else if matches!(ctx.npc.profession(), Some(Profession::Chef))
844            && ctx.rng.gen_bool(0.8)
845            && let Some(ws_id) = ctx.state.data().sites[visiting_site].world_site
846            && let Some(tavern) = ctx.index.sites.get(ws_id).plots().filter_map(|p| match p.kind() {  PlotKind::Tavern(a) => Some(a), _ => None }).choose(&mut ctx.rng)
847            && let Some((bar_pos, room_center)) = tavern.rooms.values().flat_map(|room|
848                room.details.iter().filter_map(|detail| match detail {
849                    tavern::Detail::Bar { aabr } => {
850                        let center = aabr.center();
851                        Some((center.with_z(room.bounds.min.z), room.bounds.center().xy()))
852                    }
853                    _ => None,
854                })
855            ).choose(&mut ctx.rng) {
856
857            let face_dir = Dir::from_unnormalized((room_center - bar_pos).as_::<f32>().with_z(0.0)).unwrap_or_else(|| Dir::random_2d(&mut ctx.rng));
858
859            return casual(
860                travel_to_point(tavern.door_wpos.xy().as_(), 0.5)
861                    .then(goto(bar_pos.as_() + Vec2::new(0.5, 0.5), WALKING_SPEED, 2.0))
862                    // TODO: Just dance there for now, in the future do other stuff.
863                    .then(just(move |ctx, _| ctx.controller.do_dance(Some(face_dir))).repeat().stop_if(timeout(60.0)))
864                    .debug(|| "cook food").map(|_, _| ())
865            )
866        }
867
868        // If nothing else needs doing, walk between plazas and socialize
869        casual(now(move |ctx, _| {
870            // Choose a plaza in the site we're visiting to walk to
871            if let Some(plaza_wpos) = choose_plaza(ctx, visiting_site) {
872                // Walk to the plaza...
873                Either::Left(travel_to_point(plaza_wpos, 0.5)
874                    .debug(|| "walk to plaza"))
875            } else {
876                // No plazas? :(
877                Either::Right(finish())
878            }
879                // ...then socialize for some time before moving on
880                .then(socialize()
881                    .repeat()
882                    .map_state(|state: &mut DefaultState| &mut state.socialize_timer)
883                    .stop_if(timeout(ctx.rng.gen_range(30.0..90.0)))
884                    .debug(|| "wait at plaza"))
885                .map(|_, _| ())
886        }))
887    })
888    .debug(move || format!("villager at site {:?}", visiting_site))
889}
890
891fn pilot<S: State>(ship: common::comp::ship::Body) -> impl Action<S> {
892    // Travel between different towns in a straight line
893    now(move |ctx, _| {
894        let data = &*ctx.state.data();
895        let station_wpos = data
896            .sites
897            .iter()
898            .filter(|(id, _)| Some(*id) != ctx.npc.current_site)
899            .filter_map(|(_, site)| Some(ctx.index.sites.get(site.world_site?)))
900            .flat_map(|site| {
901                site.filter_plots(|plot| {
902                    matches!(plot.kind().meta(), Some(PlotKindMeta::AirshipDock { .. }))
903                })
904                .map(|plot| site.tile_center_wpos(plot.root_tile()))
905            })
906            .choose(&mut ctx.rng);
907        if let Some(station_wpos) = station_wpos {
908            Either::Right(
909                goto_2d_flying(
910                    station_wpos.as_(),
911                    1.0,
912                    50.0,
913                    150.0,
914                    110.0,
915                    ship.flying_height(),
916                )
917                .then(goto_2d_flying(
918                    station_wpos.as_(),
919                    1.0,
920                    10.0,
921                    32.0,
922                    16.0,
923                    30.0,
924                )),
925            )
926        } else {
927            Either::Left(finish())
928        }
929    })
930    .repeat()
931    .map(|_, _| ())
932}
933
934fn captain<S: State>() -> impl Action<S> {
935    // For now just randomly travel the sea
936    now(|ctx, _| {
937        let chunk = ctx.npc.wpos.xy().as_().wpos_to_cpos();
938        if let Some(chunk) = NEIGHBORS
939            .into_iter()
940            .map(|neighbor| chunk + neighbor)
941            .filter(|neighbor| {
942                ctx.world
943                    .sim()
944                    .get(*neighbor)
945                    .is_some_and(|c| c.river.river_kind.is_some())
946            })
947            .choose(&mut ctx.rng)
948        {
949            let wpos = TerrainChunkSize::center_wpos(chunk);
950            let wpos = wpos.as_().with_z(
951                ctx.world
952                    .sim()
953                    .get_interpolated(wpos, |chunk| chunk.water_alt)
954                    .unwrap_or(0.0),
955            );
956            goto(wpos, 0.7, 5.0).boxed()
957        } else {
958            idle().boxed()
959        }
960    })
961    .repeat()
962    .map(|_, _| ())
963}
964
965fn check_inbox<S: State>(ctx: &mut NpcCtx) -> Option<impl Action<S> + use<S>> {
966    let mut action = None;
967    ctx.inbox.retain(|input| {
968        match input {
969            NpcInput::Report(report_id) if !ctx.known_reports.contains(report_id) => {
970                let data = ctx.state.data();
971                let Some(report) = data.reports.get(*report_id) else {
972                    return false;
973                };
974
975                const REPORT_RESPONSE_TIME: f64 = 60.0 * 5.0;
976
977                match report.kind {
978                    ReportKind::Death { killer, actor, .. }
979                        if matches!(&ctx.npc.role, Role::Civilised(_)) =>
980                    {
981                        // TODO: Don't report self
982                        let phrase = if let Some(killer) = killer {
983                            // TODO: For now, we don't make sentiment changes if the killer was an
984                            // NPC because NPCs can't hurt one-another.
985                            // This should be changed in the future.
986                            if !matches!(killer, Actor::Npc(_)) {
987                                // TODO: Don't hard-code sentiment change
988                                let change = if ctx.sentiments.toward(actor).is(Sentiment::ENEMY) {
989                                    // Like the killer if we have negative sentiment towards the
990                                    // killed.
991                                    0.25
992                                } else {
993                                    -0.75
994                                };
995                                ctx.sentiments
996                                    .toward_mut(killer)
997                                    .change_by(change, Sentiment::VILLAIN);
998                            }
999
1000                            // This is a murder of a player. Feel bad for the player and stop
1001                            // attacking them.
1002                            if let Actor::Character(_) = actor {
1003                                ctx.sentiments
1004                                    .toward_mut(actor)
1005                                    .limit_below(Sentiment::ENEMY)
1006                            }
1007
1008                            if ctx.sentiments.toward(actor).is(Sentiment::ENEMY) {
1009                                "npc-speech-witness_enemy_murder"
1010                            } else {
1011                                "npc-speech-witness_murder"
1012                            }
1013                        } else {
1014                            "npc-speech-witness_death"
1015                        };
1016                        ctx.known_reports.insert(*report_id);
1017
1018                        if ctx.time_of_day.0 - report.at_tod.0 < REPORT_RESPONSE_TIME {
1019                            action = Some(
1020                                just(move |ctx, _| {
1021                                    ctx.controller.say(killer, Content::localized(phrase))
1022                                })
1023                                .l()
1024                                .l(),
1025                            );
1026                        }
1027                        false
1028                    },
1029                    ReportKind::Theft {
1030                        thief,
1031                        site,
1032                        sprite,
1033                    } => {
1034                        // Check if this happened at home, where we know what belongs to who
1035                        if let Some(site) = site
1036                            && ctx.npc.home == Some(site)
1037                        {
1038                            // TODO: Don't hardcode sentiment change.
1039                            ctx.sentiments
1040                                .toward_mut(thief)
1041                                .change_by(-0.2, Sentiment::ENEMY);
1042                            ctx.known_reports.insert(*report_id);
1043
1044                            let phrase = if matches!(ctx.npc.profession(), Some(Profession::Farmer))
1045                                && matches!(sprite.category(), sprite::Category::Plant)
1046                            {
1047                                "npc-speech-witness_theft_owned"
1048                            } else {
1049                                "npc-speech-witness_theft"
1050                            };
1051
1052                            if ctx.time_of_day.0 - report.at_tod.0 < REPORT_RESPONSE_TIME {
1053                                action = Some(
1054                                    just(move |ctx, _| {
1055                                        ctx.controller.say(thief, Content::localized(phrase))
1056                                    })
1057                                    .r()
1058                                    .l(),
1059                                );
1060                            }
1061                        }
1062                        false
1063                    },
1064                    // We don't care about deaths of non-civilians
1065                    ReportKind::Death { .. } => false,
1066                }
1067            },
1068            NpcInput::Report(_) => false, // Reports we already know of are ignored
1069            NpcInput::Interaction(by) => {
1070                action = Some(talk_to(*by).r());
1071                false
1072            },
1073            // Dialogue inputs get retained because they're handled by specific conversation actions
1074            // later
1075            NpcInput::Dialogue(_, _) => true,
1076        }
1077    });
1078
1079    action
1080}
1081
1082fn check_for_enemies<S: State>(ctx: &mut NpcCtx) -> Option<impl Action<S> + use<S>> {
1083    // TODO: Instead of checking all nearby actors every tick, it would be more
1084    // effective to have the actor grid generate a per-tick diff so that we only
1085    // need to check new actors in the local area. Be careful though:
1086    // implementing this means accounting for changes in sentiment (that could
1087    // suddenly make a nearby actor an enemy) as well as variable NPC tick
1088    // rates!
1089    ctx.state
1090        .data()
1091        .npcs
1092        .nearby(Some(ctx.npc_id), ctx.npc.wpos, 24.0)
1093        .find(|actor| ctx.sentiments.toward(*actor).is(Sentiment::ENEMY))
1094        .map(|enemy| just(move |ctx, _| ctx.controller.attack(enemy)))
1095}
1096
1097fn react_to_events<S: State>(ctx: &mut NpcCtx, _: &mut S) -> Option<impl Action<S> + use<S>> {
1098    check_inbox::<S>(ctx)
1099        .map(|action| action.boxed())
1100        .or_else(|| check_for_enemies(ctx).map(|action| action.boxed()))
1101}
1102
1103fn humanoid() -> impl Action<DefaultState> {
1104    choose(|ctx, _| {
1105        if let Some(riding) = &ctx.state.data().npcs.mounts.get_mount_link(ctx.npc_id) {
1106            if riding.is_steering {
1107                if let Some(vehicle) = ctx.state.data().npcs.get(riding.mount) {
1108                    match vehicle.body {
1109                        comp::Body::Ship(body @ comp::ship::Body::AirBalloon) => {
1110                            important(pilot(body))
1111                        },
1112                        comp::Body::Ship(comp::ship::Body::DefaultAirship) => {
1113                            important(airship_ai::pilot_airship())
1114                        },
1115                        comp::Body::Ship(
1116                            comp::ship::Body::SailBoat | comp::ship::Body::Galleon,
1117                        ) => important(captain()),
1118                        _ => casual(idle()),
1119                    }
1120                } else {
1121                    casual(finish())
1122                }
1123            } else {
1124                important(
1125                    socialize().map_state(|state: &mut DefaultState| &mut state.socialize_timer),
1126                )
1127            }
1128        } else if let Some((tgt, _)) = ctx.npc.hiring
1129            && util::actor_exists(ctx, tgt)
1130        {
1131            important(hired(tgt).interrupt_with(react_to_events))
1132        } else {
1133            let action = if matches!(
1134                ctx.npc.profession(),
1135                Some(Profession::Adventurer(_) | Profession::Merchant)
1136            ) {
1137                adventure().l().l()
1138            } else if let Some(home) = ctx.npc.home {
1139                villager(home).r().l()
1140            } else {
1141                idle().r() // Homeless
1142            };
1143
1144            casual(action.interrupt_with(react_to_events))
1145        }
1146    })
1147}
1148
1149fn bird_large() -> impl Action<DefaultState> {
1150    now(|ctx, bearing: &mut Vec2<f32>| {
1151        *bearing = bearing
1152            .map(|e| e + ctx.rng.gen_range(-0.1..0.1))
1153            .try_normalized()
1154            .unwrap_or_default();
1155        let bearing_dist = 15.0;
1156        let mut pos = ctx.npc.wpos.xy() + *bearing * bearing_dist;
1157        let is_deep_water =
1158            matches!(ctx.npc.body, common::comp::Body::BirdLarge(b) if matches!(b.species, bird_large::Species::SeaWyvern))
1159                || ctx
1160                .world
1161                .sim()
1162                .get(pos.as_().wpos_to_cpos()).is_none_or(|c| {
1163                    c.alt - c.water_alt < -120.0 && (c.river.is_ocean() || c.river.is_lake())
1164                });
1165        if is_deep_water {
1166            *bearing *= -1.0;
1167            pos = ctx.npc.wpos.xy() + *bearing * bearing_dist;
1168        };
1169        // when high tree_density fly high, otherwise fly low-mid
1170        let npc_pos = ctx.npc.wpos.xy();
1171        let trees = ctx
1172            .world
1173            .sim()
1174            .get(npc_pos.as_().wpos_to_cpos()).is_some_and(|c| c.tree_density > 0.1);
1175        let height_factor = if trees {
1176            2.0
1177        } else {
1178            ctx.rng.gen_range(0.4..0.9)
1179        };
1180
1181        let data = ctx.state.data();
1182        // without destination site fly to next waypoint
1183        let mut dest_site = pos;
1184        if let Some(home) = ctx.npc.home {
1185            let is_home = ctx.npc.current_site == Some(home);
1186            if is_home {
1187                if let Some((id, _)) = data
1188                    .sites
1189                    .iter()
1190                    .filter(|(id, site)| {
1191                        *id != home
1192                            && site.world_site.is_some_and(|site| {
1193                            match ctx.npc.body {
1194                                common::comp::Body::BirdLarge(b) => match b.species {
1195                                    bird_large::Species::Phoenix => matches!(ctx.index.sites.get(site).kind,
1196                                    Some(SiteKind::Terracotta
1197                                    | SiteKind::Haniwa
1198                                    | SiteKind::Myrmidon
1199                                    | SiteKind::Adlet
1200                                    | SiteKind::DwarvenMine
1201                                    | SiteKind::ChapelSite
1202                                    | SiteKind::Cultist
1203                                    | SiteKind::Gnarling
1204                                    | SiteKind::Sahagin
1205                                    | SiteKind::VampireCastle)),
1206                                    bird_large::Species::Cockatrice => matches!(ctx.index.sites.get(site).kind,
1207                                    Some(SiteKind::GiantTree)),
1208                                    bird_large::Species::Roc => matches!(ctx.index.sites.get(site).kind,
1209                                    Some(SiteKind::Haniwa
1210                                    | SiteKind::Cultist)),
1211                                    bird_large::Species::FlameWyvern => matches!(ctx.index.sites.get(site).kind,
1212                                    Some(SiteKind::DwarvenMine
1213                                    | SiteKind::Terracotta)),
1214                                    bird_large::Species::CloudWyvern => matches!(ctx.index.sites.get(site).kind,
1215                                    Some(SiteKind::ChapelSite
1216                                    | SiteKind::Sahagin)),
1217                                    bird_large::Species::FrostWyvern => matches!(ctx.index.sites.get(site).kind,
1218                                    Some(SiteKind::Adlet
1219                                    | SiteKind::Myrmidon)),
1220                                    bird_large::Species::SeaWyvern => matches!(ctx.index.sites.get(site).kind,
1221                                    Some(SiteKind::ChapelSite
1222                                    | SiteKind::Sahagin)),
1223                                    bird_large::Species::WealdWyvern => matches!(ctx.index.sites.get(site).kind,
1224                                    Some(SiteKind::GiantTree
1225                                    | SiteKind::Gnarling)),
1226                                },
1227                                _ => matches!(&ctx.index.sites.get(site).kind, Some(SiteKind::GiantTree)),
1228                            }
1229                        })
1230                    })
1231                    /*choose closest destination:
1232                    .min_by_key(|(_, site)| site.wpos.as_().distance(npc_pos) as i32)*/
1233                //choose random destination:
1234                .choose(&mut ctx.rng)
1235                {
1236                    ctx.controller.set_new_home(id)
1237                }
1238            } else if let Some(site) = data.sites.get(home) {
1239                dest_site = site.wpos.as_::<f32>()
1240            }
1241        }
1242        goto_2d_flying(
1243            pos,
1244            0.2,
1245            bearing_dist,
1246            8.0,
1247            8.0,
1248            ctx.npc.body.flying_height() * height_factor,
1249        )
1250            // If we are too far away from our waypoint position we can stop since we aren't going to a specific place.
1251            // If waypoint position is further away from destination site find a new waypoint
1252            .stop_if(move |ctx: &mut NpcCtx| {
1253                ctx.npc.wpos.xy().distance_squared(pos) > (bearing_dist + 5.0).powi(2)
1254                    || dest_site.distance_squared(pos) > dest_site.distance_squared(npc_pos)
1255            })
1256            // If waypoint position wasn't reached within 10 seconds we're probably stuck and need to find a new waypoint.
1257            .stop_if(timeout(10.0))
1258            .debug({
1259                let bearing = *bearing;
1260                move || format!("Moving with a bearing of {:?}", bearing)
1261            })
1262    })
1263        .repeat()
1264        .with_state(Vec2::<f32>::zero())
1265        .map(|_, _| ())
1266}
1267
1268fn monster() -> impl Action<DefaultState> {
1269    now(|ctx, bearing: &mut Vec2<f32>| {
1270        *bearing = bearing
1271            .map(|e| e + ctx.rng.gen_range(-0.1..0.1))
1272            .try_normalized()
1273            .unwrap_or_default();
1274        let bearing_dist = 24.0;
1275        let mut pos = ctx.npc.wpos.xy() + *bearing * bearing_dist;
1276        let is_deep_water = ctx
1277            .world
1278            .sim()
1279            .get(pos.as_().wpos_to_cpos())
1280            .is_none_or(|c| {
1281                c.alt - c.water_alt < -10.0 && (c.river.is_ocean() || c.river.is_lake())
1282            });
1283        if !is_deep_water {
1284            goto_2d(pos, 0.7, 8.0)
1285        } else {
1286            *bearing *= -1.0;
1287
1288            pos = ctx.npc.wpos.xy() + *bearing * 24.0;
1289
1290            goto_2d(pos, 0.7, 8.0)
1291        }
1292        // If we are too far away from our goal position we can stop since we aren't going to a specific place.
1293        .stop_if(move |ctx: &mut NpcCtx| {
1294            ctx.npc.wpos.xy().distance_squared(pos) > (bearing_dist + 5.0).powi(2)
1295        })
1296        .debug({
1297            let bearing = *bearing;
1298            move || format!("Moving with a bearing of {:?}", bearing)
1299        })
1300    })
1301    .repeat()
1302    .with_state(Vec2::<f32>::zero())
1303    .map(|_, _| ())
1304}
1305
1306fn think() -> impl Action<DefaultState> {
1307    now(|ctx, _| match ctx.npc.body {
1308        common::comp::Body::Humanoid(_) => humanoid().l().l().l(),
1309        common::comp::Body::BirdLarge(_) => bird_large().r().l().l(),
1310        _ => match &ctx.npc.role {
1311            Role::Civilised(_) => socialize()
1312                .map_state(|state: &mut DefaultState| &mut state.socialize_timer)
1313                .l()
1314                .r()
1315                .l(),
1316            Role::Monster => monster().r().r().l(),
1317            Role::Wild => idle().r(),
1318            Role::Vehicle => idle().r(),
1319        },
1320    })
1321}