Skip to main content

veloren_rtsim/data/
actor.rs

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