veloren_rtsim/data/
npc.rs

1use crate::{
2    ai::Action,
3    data::{Reports, Sentiments, quest::Quest},
4    generate::name,
5};
6pub use common::rtsim::{NpcId, Profession};
7use common::{
8    character::CharacterId,
9    comp::{self, agent::FlightMode, item::ItemDef},
10    grid::Grid,
11    map::Marker,
12    resources::Time,
13    rtsim::{
14        Actor, Dialogue, DialogueId, DialogueKind, FactionId, NpcAction, NpcActivity, NpcInput,
15        Personality, QuestId, ReportId, Response, Role, SiteId, TerrainResource,
16    },
17    store::Id,
18    terrain::CoordinateConversions,
19    util::Dir,
20};
21use hashbrown::{HashMap, HashSet};
22use rand::prelude::*;
23use serde::{Deserialize, Serialize};
24use slotmap::HopSlotMap;
25use std::{
26    collections::VecDeque,
27    ops::{Deref, DerefMut},
28    sync::{
29        Arc,
30        atomic::{AtomicU32, Ordering},
31    },
32};
33use tracing::error;
34use vek::*;
35use world::{
36    civ::Track,
37    site::Site as WorldSite,
38    util::{LOCALITY, RandomPerm},
39};
40
41#[derive(Copy, Clone, Debug, Default)]
42pub enum SimulationMode {
43    /// The NPC is unloaded and is being simulated via rtsim.
44    #[default]
45    Simulated,
46    /// The NPC has been loaded into the game world as an ECS entity.
47    Loaded,
48}
49
50#[derive(Clone)]
51pub struct PathData<P, N> {
52    pub end: N,
53    pub path: VecDeque<P>,
54    pub repoll: bool,
55}
56
57#[derive(Clone, Default)]
58pub struct PathingMemory {
59    pub intrasite_path: Option<(PathData<Vec2<i32>, Vec2<i32>>, Id<WorldSite>)>,
60    pub intersite_path: Option<(PathData<(Id<Track>, bool), SiteId>, usize)>,
61}
62
63#[derive(Default)]
64pub struct Controller {
65    pub npc_actions: Vec<(NpcId, Box<dyn Action<(), ()>>)>,
66    pub actions: Vec<NpcAction>,
67    pub activity: Option<NpcActivity>,
68    pub new_home: Option<Option<SiteId>>,
69    pub look_dir: Option<Dir>,
70    pub job: Option<Job>,
71    pub quests_to_create: Vec<(QuestId, Quest)>,
72}
73
74impl Controller {
75    /// Reset the controller to a neutral state before the start of the next
76    /// brain tick.
77    pub fn reset(&mut self, npc: &Npc) {
78        self.activity = None;
79        self.look_dir = None;
80        self.job = npc.job.clone();
81    }
82
83    pub fn do_idle(&mut self) { self.activity = None; }
84
85    pub fn do_talk(&mut self, tgt: Actor) { self.activity = Some(NpcActivity::Talk(tgt)); }
86
87    pub fn do_goto(&mut self, wpos: Vec3<f32>, speed_factor: f32) {
88        self.activity = Some(NpcActivity::Goto(wpos, speed_factor));
89    }
90
91    /// go to with height above terrain and direction
92    pub fn do_goto_with_height_and_dir(
93        &mut self,
94        wpos: Vec3<f32>,
95        speed_factor: f32,
96        height: Option<f32>,
97        dir: Option<Dir>,
98        flight_mode: FlightMode,
99    ) {
100        self.activity = Some(NpcActivity::GotoFlying(
101            wpos,
102            speed_factor,
103            height,
104            dir,
105            flight_mode,
106        ));
107    }
108
109    pub fn do_gather(&mut self, resources: &'static [TerrainResource]) {
110        self.activity = Some(NpcActivity::Gather(resources));
111    }
112
113    pub fn do_hunt_animals(&mut self) { self.activity = Some(NpcActivity::HuntAnimals); }
114
115    pub fn do_dance(&mut self, dir: Option<Dir>) { self.activity = Some(NpcActivity::Dance(dir)); }
116
117    pub fn do_cheer(&mut self, dir: Option<Dir>) { self.activity = Some(NpcActivity::Cheer(dir)); }
118
119    pub fn do_sit(&mut self, dir: Option<Dir>, pos: Option<Vec3<i32>>) {
120        self.activity = Some(NpcActivity::Sit(dir, pos));
121    }
122
123    pub fn say(&mut self, target: impl Into<Option<Actor>>, content: comp::Content) {
124        self.actions.push(NpcAction::Say(target.into(), content));
125    }
126
127    pub fn npc_dialogue(
128        &mut self,
129        target: NpcId,
130        content: impl Into<Option<comp::Content>>,
131        response: impl Action<(), ()>,
132    ) {
133        if let Some(content) = content.into() {
134            self.say(Actor::Npc(target), content);
135        }
136        self.npc_action(target, response);
137    }
138
139    pub fn npc_action(&mut self, target: NpcId, response: impl Action<(), ()>) {
140        self.npc_actions.push((target, Box::new(response)));
141    }
142
143    pub fn attack(&mut self, target: impl Into<Actor>) {
144        self.actions.push(NpcAction::Attack(target.into()));
145    }
146
147    pub fn set_new_home(&mut self, new_home: impl Into<Option<SiteId>>) {
148        self.new_home = Some(new_home.into());
149    }
150
151    pub fn set_newly_hired(&mut self, actor: Actor, expires: Time) {
152        self.job = Some(Job::Hired(actor, expires));
153    }
154
155    pub fn end_hiring(&mut self) {
156        if matches!(self.job, Some(Job::Hired(..))) {
157            self.job = None;
158        }
159    }
160
161    pub fn end_quest(&mut self) {
162        if matches!(self.job, Some(Job::Quest(..))) {
163            self.job = None;
164        }
165    }
166
167    /// Start a new dialogue.
168    pub fn dialogue_start(&mut self, target: impl Into<Actor>) -> DialogueSession {
169        let target = target.into();
170
171        let session = DialogueSession {
172            target,
173            id: DialogueId(rand::rng().random()),
174        };
175
176        self.actions.push(NpcAction::Dialogue(target, Dialogue {
177            id: session.id,
178            kind: DialogueKind::Start,
179        }));
180
181        session
182    }
183
184    /// End an existing dialogue.
185    pub fn dialogue_end(&mut self, session: DialogueSession) {
186        self.actions
187            .push(NpcAction::Dialogue(session.target, Dialogue {
188                id: session.id,
189                kind: DialogueKind::End,
190            }));
191    }
192
193    pub fn dialogue_response(
194        &mut self,
195        session: DialogueSession,
196        tag: u32,
197        response: &(u16, Response),
198    ) {
199        self.actions
200            .push(NpcAction::Dialogue(session.target, Dialogue {
201                id: session.id,
202                kind: DialogueKind::Response {
203                    tag,
204                    response: response.1.clone(),
205                    response_id: response.0,
206                },
207            }));
208    }
209
210    fn new_dialogue_tag(&self) -> u32 {
211        static TAG_COUNTER: AtomicU32 = AtomicU32::new(0);
212        TAG_COUNTER.fetch_add(1, Ordering::Relaxed)
213    }
214
215    /// Ask a question, with various possible answers. Returns the dialogue tag,
216    /// used for identifying the answer.
217    pub fn dialogue_question(
218        &mut self,
219        session: DialogueSession,
220        msg: comp::Content,
221        responses: impl IntoIterator<Item = (u16, Response)>,
222    ) -> u32 {
223        let tag = self.new_dialogue_tag();
224
225        self.actions
226            .push(NpcAction::Dialogue(session.target, Dialogue {
227                id: session.id,
228                kind: DialogueKind::Question {
229                    tag,
230                    msg,
231                    responses: responses.into_iter().collect(),
232                },
233            }));
234
235        tag
236    }
237
238    /// Provide a statement as part of a dialogue. Returns the dialogue tag,
239    /// used for identifying acknowledgements.
240    pub fn dialogue_statement(
241        &mut self,
242        session: DialogueSession,
243        msg: comp::Content,
244        given_item: Option<(Arc<ItemDef>, u32)>,
245    ) -> u32 {
246        let tag = self.new_dialogue_tag();
247
248        self.actions
249            .push(NpcAction::Dialogue(session.target, Dialogue {
250                id: session.id,
251                kind: DialogueKind::Statement {
252                    msg,
253                    given_item,
254                    tag,
255                },
256            }));
257
258        tag
259    }
260
261    /// Provide a location marker as part of a dialogue.
262    pub fn dialogue_marker(&mut self, session: DialogueSession, marker: Marker) {
263        self.actions
264            .push(NpcAction::Dialogue(session.target, Dialogue {
265                id: session.id,
266                kind: DialogueKind::Marker(marker),
267            }));
268    }
269}
270
271// Represents an ongoing dialogue with another actor.
272#[derive(Copy, Clone)]
273pub struct DialogueSession {
274    pub target: Actor,
275    pub id: DialogueId,
276}
277
278pub struct Brain {
279    pub action: Box<dyn Action<(), !>>,
280}
281
282#[derive(Serialize, Deserialize)]
283pub struct Npc {
284    pub uid: u64,
285    // Persisted state
286    pub seed: u32,
287    /// Represents the location of the NPC.
288    pub wpos: Vec3<f32>,
289    pub dir: Vec2<f32>,
290
291    pub body: comp::Body,
292    pub role: Role,
293    pub home: Option<SiteId>,
294    pub faction: Option<FactionId>,
295    /// The current health of the NPC, < 0.0 is dead and 1.0 is max.
296    pub health_fraction: f32,
297
298    /// The [`crate::data::Report`]s that the NPC is aware of.
299    pub known_reports: HashSet<ReportId>,
300
301    #[serde(default)]
302    pub personality: Personality,
303    #[serde(default)]
304    pub sentiments: Sentiments,
305
306    #[serde(default)]
307    pub job: Option<Job>,
308
309    // Unpersisted state
310    #[serde(skip)]
311    pub chunk_pos: Option<Vec2<i32>>,
312    #[serde(skip)]
313    pub current_site: Option<SiteId>,
314
315    #[serde(skip)]
316    pub controller: Controller,
317    #[serde(skip)]
318    pub inbox: VecDeque<NpcInput>,
319
320    /// Whether the NPC is in simulated or loaded mode (when rtsim is run on the
321    /// server, loaded corresponds to being within a loaded chunk). When in
322    /// loaded mode, the interactions of the NPC should not be simulated but
323    /// should instead be derived from the game.
324    #[serde(skip)]
325    pub mode: SimulationMode,
326
327    #[serde(skip)]
328    pub brain: Option<Brain>,
329
330    #[serde(skip)]
331    pub npc_dialogue: VecDeque<(NpcId, Box<dyn Action<(), ()>>)>,
332}
333
334/// A job is a long-running, persistent, non-stackable occupation that an NPC
335/// must persistently attend to, but may be temporarily interrupted from. NPCs
336/// will recurrently attempt to perform tasks that relate to their job.
337#[derive(Clone, Serialize, Deserialize, PartialEq)]
338pub enum Job {
339    /// An NPC can temporarily become a hired hand (`(hiring_actor,
340    /// termination_time)`).
341    Hired(Actor, Time),
342    /// NPC is helping to perform a quest
343    Quest(QuestId),
344}
345
346impl Clone for Npc {
347    fn clone(&self) -> Self {
348        Self {
349            uid: self.uid,
350            seed: self.seed,
351            wpos: self.wpos,
352            dir: self.dir,
353            role: self.role.clone(),
354            home: self.home,
355            faction: self.faction,
356            health_fraction: self.health_fraction,
357            known_reports: self.known_reports.clone(),
358            body: self.body,
359            personality: self.personality,
360            sentiments: self.sentiments.clone(),
361            job: self.job.clone(),
362            // Not persisted
363            chunk_pos: None,
364            current_site: Default::default(),
365            controller: Default::default(),
366            inbox: Default::default(),
367            mode: Default::default(),
368            brain: Default::default(),
369            npc_dialogue: Default::default(),
370        }
371    }
372}
373
374impl Npc {
375    pub const PERM_ENTITY_CONFIG: u32 = 1;
376    const PERM_NAME: u32 = 0;
377
378    pub fn new(seed: u32, wpos: Vec3<f32>, body: comp::Body, role: Role) -> Self {
379        Self {
380            // To be assigned later
381            uid: 0,
382            seed,
383            wpos,
384            dir: Vec2::unit_x(),
385            body,
386            personality: Default::default(),
387            sentiments: Default::default(),
388            job: None,
389            role,
390            home: None,
391            faction: None,
392            health_fraction: 1.0,
393            known_reports: Default::default(),
394            chunk_pos: None,
395            current_site: None,
396            controller: Default::default(),
397            inbox: Default::default(),
398            mode: SimulationMode::Simulated,
399            brain: None,
400            npc_dialogue: Default::default(),
401        }
402    }
403
404    pub fn is_dead(&self) -> bool { self.health_fraction <= 0.0 }
405
406    // TODO: have a dedicated `NpcBuilder` type for this.
407    pub fn with_personality(mut self, personality: Personality) -> Self {
408        self.personality = personality;
409        self
410    }
411
412    // // TODO: have a dedicated `NpcBuilder` type for this.
413    // pub fn with_profession(mut self, profession: impl Into<Option<Profession>>)
414    // -> Self {     if let Role::Humanoid(p) = &mut self.role {
415    //         *p = profession.into();
416    //     } else {
417    //         panic!("Tried to assign profession {:?} to NPC, but has role {:?},
418    // which cannot have a profession", profession.into(), self.role);     }
419    //     self
420    // }
421
422    // TODO: have a dedicated `NpcBuilder` type for this.
423    pub fn with_home(mut self, home: impl Into<Option<SiteId>>) -> Self {
424        self.home = home.into();
425        self
426    }
427
428    // TODO: have a dedicated `NpcBuilder` type for this.
429    pub fn with_faction(mut self, faction: impl Into<Option<FactionId>>) -> Self {
430        self.faction = faction.into();
431        self
432    }
433
434    pub fn rng(&self, perm: u32) -> impl Rng + use<> {
435        RandomPerm::new(self.seed.wrapping_add(perm))
436    }
437
438    // TODO: Don't make this depend on deterministic RNG, actually persist names
439    // once we've decided that we want to
440    pub fn get_name(&self) -> Option<String> {
441        if let comp::Body::Humanoid(_) = &self.body {
442            Some(name::generate_npc(&mut self.rng(Self::PERM_NAME)))
443        } else {
444            None
445        }
446    }
447
448    pub fn profession(&self) -> Option<Profession> {
449        match &self.role {
450            Role::Civilised(profession) => *profession,
451            Role::Monster | Role::Wild | Role::Vehicle => None,
452        }
453    }
454
455    pub fn hired(&self) -> Option<(Actor, Time)> {
456        if let Some(Job::Hired(actor, time)) = self.job {
457            Some((actor, time))
458        } else {
459            None
460        }
461    }
462
463    pub fn cleanup(&mut self, reports: &Reports) {
464        // Clear old or superfluous sentiments
465        // TODO: It might be worth giving more important NPCs a higher sentiment
466        // 'budget' than less important ones.
467        self.sentiments
468            .cleanup(crate::data::sentiment::NPC_MAX_SENTIMENTS);
469        // Clear reports that have been forgotten
470        self.known_reports
471            .retain(|report| reports.contains_key(*report));
472        // TODO: Limit number of reports
473        // TODO: Clear old inbox items
474    }
475}
476
477#[derive(Default, Clone, Serialize, Deserialize)]
478pub struct GridCell {
479    pub npcs: Vec<NpcId>,
480}
481
482#[derive(Clone, Serialize, Deserialize, Debug)]
483pub struct NpcLink {
484    pub mount: NpcId,
485    pub rider: Actor,
486    pub is_steering: bool,
487}
488
489#[derive(Clone, Default, Serialize, Deserialize)]
490struct Riders {
491    steerer: Option<MountId>,
492    riders: Vec<MountId>,
493}
494
495#[derive(Clone, Default, Serialize, Deserialize)]
496#[serde(
497    from = "HopSlotMap<MountId, NpcLink>",
498    into = "HopSlotMap<MountId, NpcLink>"
499)]
500pub struct NpcLinks {
501    links: HopSlotMap<MountId, NpcLink>,
502    mount_map: slotmap::SecondaryMap<NpcId, Riders>,
503    rider_map: HashMap<Actor, MountId>,
504}
505
506impl NpcLinks {
507    pub fn remove_mount(&mut self, mount: NpcId) {
508        if let Some(riders) = self.mount_map.remove(mount) {
509            for link in riders
510                .riders
511                .into_iter()
512                .chain(riders.steerer)
513                .filter_map(|link_id| self.links.get(link_id))
514            {
515                self.rider_map.remove(&link.rider);
516            }
517        }
518    }
519
520    /// Internal function, only removes from `mount_map`.
521    fn remove_rider(&mut self, id: MountId, link: &NpcLink) {
522        if let Some(riders) = self.mount_map.get_mut(link.mount) {
523            if link.is_steering && riders.steerer == Some(id) {
524                riders.steerer = None;
525            } else if let Some((i, _)) = riders.riders.iter().enumerate().find(|(_, i)| **i == id) {
526                riders.riders.remove(i);
527            }
528
529            if riders.steerer.is_none() && riders.riders.is_empty() {
530                self.mount_map.remove(link.mount);
531            }
532        }
533    }
534
535    pub fn remove_link(&mut self, link_id: MountId) {
536        if let Some(link) = self.links.remove(link_id) {
537            self.rider_map.remove(&link.rider);
538            self.remove_rider(link_id, &link);
539        }
540    }
541
542    pub fn dismount(&mut self, rider: impl Into<Actor>) {
543        if let Some(id) = self.rider_map.remove(&rider.into())
544            && let Some(link) = self.links.remove(id)
545        {
546            self.remove_rider(id, &link);
547        }
548    }
549
550    // This is the only function to actually add a mount link.
551    // And it ensures that there isn't link chaining
552    pub fn add_mounting(
553        &mut self,
554        mount: NpcId,
555        rider: impl Into<Actor>,
556        steering: bool,
557    ) -> Result<MountId, MountingError> {
558        let rider = rider.into();
559        if Actor::Npc(mount) == rider {
560            return Err(MountingError::MountSelf);
561        }
562        if let Actor::Npc(rider) = rider
563            && self.mount_map.contains_key(rider)
564        {
565            return Err(MountingError::RiderIsMounted);
566        }
567        if self.rider_map.contains_key(&Actor::Npc(mount)) {
568            return Err(MountingError::MountIsRiding);
569        }
570        if let Some(mount_entry) = self.mount_map.entry(mount) {
571            if let hashbrown::hash_map::Entry::Vacant(rider_entry) = self.rider_map.entry(rider) {
572                let riders = mount_entry.or_insert(Riders::default());
573
574                if steering {
575                    if riders.steerer.is_none() {
576                        let id = self.links.insert(NpcLink {
577                            mount,
578                            rider,
579                            is_steering: true,
580                        });
581                        riders.steerer = Some(id);
582                        rider_entry.insert(id);
583                        Ok(id)
584                    } else {
585                        Err(MountingError::HasSteerer)
586                    }
587                } else {
588                    // TODO: Maybe have some limit on the number of riders depending on the mount?
589                    let id = self.links.insert(NpcLink {
590                        mount,
591                        rider,
592                        is_steering: false,
593                    });
594                    riders.riders.push(id);
595                    rider_entry.insert(id);
596                    Ok(id)
597                }
598            } else {
599                Err(MountingError::AlreadyRiding)
600            }
601        } else {
602            Err(MountingError::MountDead)
603        }
604    }
605
606    pub fn steer(
607        &mut self,
608        mount: NpcId,
609        rider: impl Into<Actor>,
610    ) -> Result<MountId, MountingError> {
611        self.add_mounting(mount, rider, true)
612    }
613
614    pub fn ride(
615        &mut self,
616        mount: NpcId,
617        rider: impl Into<Actor>,
618    ) -> Result<MountId, MountingError> {
619        self.add_mounting(mount, rider, false)
620    }
621
622    pub fn get_mount_link(&self, rider: impl Into<Actor>) -> Option<&NpcLink> {
623        self.rider_map
624            .get(&rider.into())
625            .and_then(|link| self.links.get(*link))
626    }
627
628    pub fn get_steerer_link(&self, mount: NpcId) -> Option<&NpcLink> {
629        self.mount_map
630            .get(mount)
631            .and_then(|mount| self.links.get(mount.steerer?))
632    }
633
634    pub fn get(&self, id: MountId) -> Option<&NpcLink> { self.links.get(id) }
635
636    pub fn ids(&self) -> impl Iterator<Item = MountId> + '_ { self.links.keys() }
637
638    pub fn iter(&self) -> impl Iterator<Item = &NpcLink> + '_ { self.links.values() }
639
640    pub fn iter_mounts(&self) -> impl Iterator<Item = NpcId> + '_ { self.mount_map.keys() }
641}
642
643impl From<HopSlotMap<MountId, NpcLink>> for NpcLinks {
644    fn from(mut value: HopSlotMap<MountId, NpcLink>) -> Self {
645        let mut from_map = slotmap::SecondaryMap::new();
646        let mut to_map = HashMap::with_capacity(value.len());
647        let mut delete = Vec::new();
648        for (id, link) in value.iter() {
649            if let Some(entry) = from_map.entry(link.mount) {
650                let riders = entry.or_insert(Riders::default());
651                if link.is_steering {
652                    if let Some(old) = riders.steerer.replace(id) {
653                        error!("Replaced steerer {old:?} with {id:?}");
654                    }
655                } else {
656                    riders.riders.push(id);
657                }
658            } else {
659                delete.push(id);
660            }
661            to_map.insert(link.rider, id);
662        }
663        for id in delete {
664            value.remove(id);
665        }
666        Self {
667            links: value,
668            mount_map: from_map,
669            rider_map: to_map,
670        }
671    }
672}
673
674impl From<NpcLinks> for HopSlotMap<MountId, NpcLink> {
675    fn from(other: NpcLinks) -> Self { other.links }
676}
677slotmap::new_key_type! {
678    pub struct MountId;
679}
680
681#[derive(Clone, Serialize, Deserialize)]
682pub struct MountData {
683    is_steering: bool,
684}
685
686#[derive(Clone, Serialize, Deserialize)]
687pub struct Npcs {
688    pub uid_counter: u64,
689    pub npcs: HopSlotMap<NpcId, Npc>,
690    pub mounts: NpcLinks,
691    // TODO: This feels like it should be its own rtsim resource
692    // TODO: Consider switching to `common::util::SpatialGrid` instead
693    #[serde(skip, default = "construct_npc_grid")]
694    pub npc_grid: Grid<GridCell>,
695    #[serde(skip)]
696    pub character_map: HashMap<Vec2<i32>, Vec<(CharacterId, Vec3<f32>)>>,
697}
698
699impl Default for Npcs {
700    fn default() -> Self {
701        Self {
702            uid_counter: 0,
703            npcs: Default::default(),
704            mounts: Default::default(),
705            npc_grid: construct_npc_grid(),
706            character_map: Default::default(),
707        }
708    }
709}
710
711fn construct_npc_grid() -> Grid<GridCell> { Grid::new(Vec2::zero(), Default::default()) }
712
713#[derive(Debug)]
714pub enum MountingError {
715    MountDead,
716    RiderDead,
717    HasSteerer,
718    AlreadyRiding,
719    MountIsRiding,
720    RiderIsMounted,
721    MountSelf,
722}
723
724impl Npcs {
725    pub fn create_npc(&mut self, mut npc: Npc) -> NpcId {
726        npc.uid = self.uid_counter;
727        self.uid_counter += 1;
728        self.npcs.insert(npc)
729    }
730
731    /// Queries nearby npcs, not garantueed to work if radius > 32.0
732    // TODO: Find a more efficient way to implement this, it's currently
733    // (theoretically) O(n^2).
734    pub fn nearby(
735        &self,
736        this_npc: Option<NpcId>,
737        wpos: Vec3<f32>,
738        radius: f32,
739    ) -> impl Iterator<Item = Actor> + '_ {
740        let chunk_pos = wpos.xy().as_().wpos_to_cpos();
741        let r_sqr = radius * radius;
742        LOCALITY
743            .into_iter()
744            .flat_map(move |neighbor| {
745                self.npc_grid.get(chunk_pos + neighbor).map(move |cell| {
746                    cell.npcs
747                        .iter()
748                        .copied()
749                        .filter(move |npc| {
750                            self.npcs
751                                .get(*npc)
752                                .is_some_and(|npc| npc.wpos.distance_squared(wpos) < r_sqr)
753                                && Some(*npc) != this_npc
754                        })
755                        .map(Actor::Npc)
756                })
757            })
758            .flatten()
759            .chain(
760                self.character_map
761                    .get(&chunk_pos)
762                    .map(|characters| {
763                        characters.iter().filter_map(move |(character, c_wpos)| {
764                            if c_wpos.distance_squared(wpos) < r_sqr {
765                                Some(Actor::Character(*character))
766                            } else {
767                                None
768                            }
769                        })
770                    })
771                    .into_iter()
772                    .flatten(),
773            )
774    }
775}
776
777impl Deref for Npcs {
778    type Target = HopSlotMap<NpcId, Npc>;
779
780    fn deref(&self) -> &Self::Target { &self.npcs }
781}
782
783impl DerefMut for Npcs {
784    fn deref_mut(&mut self) -> &mut Self::Target { &mut self.npcs }
785}