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