veloren_rtsim/rule/
simulate_npcs.rs

1use crate::{
2    RtState, Rule, RuleError,
3    data::{Sentiment, npc::SimulationMode},
4    event::{EventCtx, OnHealthChange, OnHelped, OnMountVolume, OnTick},
5};
6use common::{
7    comp::{self, Body},
8    mounting::{Volume, VolumePos},
9    rtsim::{Actor, NpcAction, NpcActivity},
10    terrain::{CoordinateConversions, TerrainChunkSize},
11    vol::RectVolSize,
12};
13use slotmap::SecondaryMap;
14use vek::{Clamp, Vec2};
15
16pub struct SimulateNpcs;
17
18impl Rule for SimulateNpcs {
19    fn start(rtstate: &mut RtState) -> Result<Self, RuleError> {
20        rtstate.bind(on_helped);
21        rtstate.bind(on_health_changed);
22        rtstate.bind(on_mount_volume);
23        rtstate.bind(on_tick);
24
25        Ok(Self)
26    }
27}
28
29fn on_mount_volume(ctx: EventCtx<SimulateNpcs, OnMountVolume>) {
30    let data = &mut *ctx.state.data_mut();
31
32    // TODO: Add actor to riders.
33    if let VolumePos {
34        kind: Volume::Entity(vehicle),
35        ..
36    } = ctx.event.pos
37        && let Some(link) = data.npcs.mounts.get_steerer_link(vehicle)
38        && let Actor::Npc(driver) = link.rider
39        && let Some(driver) = data.npcs.get_mut(driver)
40    {
41        driver.controller.actions.push(NpcAction::Say(
42            Some(ctx.event.actor),
43            comp::Content::localized("npc-speech-welcome-aboard"),
44        ))
45    }
46}
47
48fn on_health_changed(ctx: EventCtx<SimulateNpcs, OnHealthChange>) {
49    let data = &mut *ctx.state.data_mut();
50
51    if let Some(cause) = ctx.event.cause
52        && let Actor::Npc(npc) = ctx.event.actor
53        && let Some(npc) = data.npcs.get_mut(npc)
54    {
55        if ctx.event.change < 0.0 {
56            npc.sentiments
57                .toward_mut(cause)
58                .change_by(-0.1, Sentiment::ENEMY);
59        } else if ctx.event.change > 0.0 {
60            npc.sentiments
61                .toward_mut(cause)
62                .change_by(0.05, Sentiment::POSITIVE);
63        }
64    }
65}
66
67fn on_helped(ctx: EventCtx<SimulateNpcs, OnHelped>) {
68    let data = &mut *ctx.state.data_mut();
69
70    if let Some(saver) = ctx.event.saver
71        && let Actor::Npc(npc) = ctx.event.actor
72        && let Some(npc) = data.npcs.get_mut(npc)
73    {
74        npc.controller.actions.push(NpcAction::Say(
75            Some(ctx.event.actor),
76            comp::Content::localized("npc-speech-thank_you"),
77        ));
78        npc.sentiments
79            .toward_mut(saver)
80            .change_by(0.3, Sentiment::FRIEND);
81    }
82}
83
84fn on_tick(ctx: EventCtx<SimulateNpcs, OnTick>) {
85    let data = &mut *ctx.state.data_mut();
86
87    // Maintain links
88    let ids = data.npcs.mounts.ids().collect::<Vec<_>>();
89    let mut mount_activity = SecondaryMap::new();
90    for link_id in ids {
91        if let Some(link) = data.npcs.mounts.get(link_id) {
92            if let Some(mount) = data
93                .npcs
94                .npcs
95                .get(link.mount)
96                .filter(|mount| !mount.is_dead())
97            {
98                let wpos = mount.wpos;
99                if let Actor::Npc(rider) = link.rider {
100                    if let Some(rider) = data
101                        .npcs
102                        .npcs
103                        .get_mut(rider)
104                        .filter(|rider| !rider.is_dead())
105                    {
106                        rider.wpos = wpos;
107                        mount_activity.insert(link.mount, rider.controller.activity);
108                    } else {
109                        data.npcs.mounts.dismount(link.rider)
110                    }
111                }
112            } else {
113                data.npcs.mounts.remove_mount(link.mount)
114            }
115        }
116    }
117
118    for (npc_id, npc) in data.npcs.npcs.iter_mut().filter(|(_, npc)| !npc.is_dead()) {
119        if matches!(npc.mode, SimulationMode::Simulated) {
120            // Consume NPC actions
121            for action in std::mem::take(&mut npc.controller.actions) {
122                match action {
123                    NpcAction::Say(_, _) => {}, // Currently, just swallow interactions
124                    NpcAction::Attack(_) => {}, // TODO: Implement simulated combat
125                    NpcAction::Dialogue(_, _) => {},
126                }
127            }
128
129            let activity = if data.npcs.mounts.get_mount_link(npc_id).is_some() {
130                // We are riding, nothing to do.
131                continue;
132            } else if let Some(activity) = mount_activity.get(npc_id) {
133                *activity
134            } else {
135                npc.controller.activity
136            };
137
138            match activity {
139                // Move NPCs if they have a target destination
140                Some(NpcActivity::Goto(target, speed_factor)) => {
141                    let diff = target - npc.wpos;
142                    let dist2 = diff.magnitude_squared();
143
144                    if dist2 > 0.5f32.powi(2) {
145                        let offset = diff
146                            * (npc.body.max_speed_approx() * speed_factor * ctx.event.dt
147                                / dist2.sqrt())
148                            .min(1.0);
149                        let new_wpos = npc.wpos + offset;
150
151                        let is_valid = match npc.body {
152                            // Don't move water bound bodies outside of water.
153                            Body::Ship(comp::ship::Body::SailBoat | comp::ship::Body::Galleon)
154                            | Body::FishMedium(_)
155                            | Body::FishSmall(_) => {
156                                let chunk_pos = new_wpos.xy().as_().wpos_to_cpos();
157                                ctx.world
158                                    .sim()
159                                    .get(chunk_pos)
160                                    .is_none_or(|f| f.river.river_kind.is_some())
161                            },
162                            Body::Ship(comp::ship::Body::DefaultAirship) => false,
163                            _ => true,
164                        };
165
166                        if is_valid {
167                            npc.wpos = new_wpos;
168                        }
169
170                        npc.dir = (target.xy() - npc.wpos.xy())
171                            .try_normalized()
172                            .unwrap_or(npc.dir);
173                    }
174                },
175                // Move Flying NPCs like airships if they have a target destination
176                Some(NpcActivity::GotoFlying(target, speed_factor, _, dir, _)) => {
177                    let diff = target - npc.wpos;
178                    let dist2 = diff.magnitude_squared();
179
180                    if dist2 > 0.5f32.powi(2) {
181                        match npc.body {
182                            Body::Ship(comp::ship::Body::DefaultAirship) => {
183                                // Don't limit airship movement to 1.0 per axis
184                                // SimulationMode::Simulated treats the Npc dimensions differently
185                                // somehow from when the Npc is
186                                // loaded. The calculation below results in a position
187                                // that is roughly the offset from the ship centerline to the
188                                // docking position and is also off
189                                // by the offset from the ship fore/aft centerline to the docking
190                                // position. The result is that if the player spawns in at a dock
191                                // where an airship is docked, the
192                                // airship will bounce around while seeking the docking position
193                                // in loaded mode.
194                                let offset = diff
195                                    * (npc.body.max_speed_approx() * speed_factor * ctx.event.dt
196                                        / dist2.sqrt());
197                                npc.wpos += offset;
198                            },
199                            _ => {
200                                let offset = diff
201                                    * (npc.body.max_speed_approx() * speed_factor * ctx.event.dt
202                                        / dist2.sqrt())
203                                    .min(1.0);
204                                let new_wpos = npc.wpos + offset;
205
206                                let is_valid = match npc.body {
207                                    // Don't move water bound bodies outside of water.
208                                    Body::Ship(
209                                        comp::ship::Body::SailBoat | comp::ship::Body::Galleon,
210                                    )
211                                    | Body::FishMedium(_)
212                                    | Body::FishSmall(_) => {
213                                        let chunk_pos = new_wpos.xy().as_().wpos_to_cpos();
214                                        ctx.world
215                                            .sim()
216                                            .get(chunk_pos)
217                                            .is_none_or(|f| f.river.river_kind.is_some())
218                                    },
219                                    _ => true,
220                                };
221
222                                if is_valid {
223                                    npc.wpos = new_wpos;
224                                }
225                            },
226                        }
227
228                        if let Some(dir_override) = dir {
229                            npc.dir = dir_override.xy().try_normalized().unwrap_or(npc.dir);
230                        } else {
231                            npc.dir = (target.xy() - npc.wpos.xy())
232                                .try_normalized()
233                                .unwrap_or(npc.dir);
234                        }
235                    }
236                },
237                Some(
238                    NpcActivity::Gather(_)
239                    | NpcActivity::HuntAnimals
240                    | NpcActivity::Dance(_)
241                    | NpcActivity::Cheer(_)
242                    | NpcActivity::Sit(..)
243                    | NpcActivity::Talk(..),
244                ) => {
245                    // TODO: Maybe they should walk around randomly
246                    // when gathering resources?
247                },
248                None => {},
249            }
250
251            // Make sure NPCs remain in a valid location
252            let clamped_wpos = npc.wpos.xy().clamped(
253                Vec2::zero(),
254                (ctx.world.sim().get_size() * TerrainChunkSize::RECT_SIZE).as_(),
255            );
256            match npc.body {
257                Body::Ship(comp::ship::Body::DefaultAirship | comp::ship::Body::AirBalloon) => {
258                    npc.wpos = clamped_wpos.with_z(
259                        ctx.world
260                            .sim()
261                            .get_surface_alt_approx(clamped_wpos.as_())
262                            .max(npc.wpos.z),
263                    );
264                },
265                _ => {
266                    npc.wpos = clamped_wpos.with_z(
267                        ctx.world.sim().get_surface_alt_approx(clamped_wpos.as_())
268                            + npc.body.flying_height(),
269                    );
270                },
271            }
272        }
273
274        // Move home if required
275        if let Some(new_home) = npc.controller.new_home.take() {
276            // Remove the NPC from their old home population
277            if let Some(old_home) = npc.home {
278                if let Some(old_home) = data.sites.get_mut(old_home) {
279                    old_home.population.remove(&npc_id);
280                }
281            }
282            // Add the NPC to their new home population
283            if let Some(new_home) = data.sites.get_mut(new_home) {
284                new_home.population.insert(npc_id);
285            }
286            npc.home = Some(new_home);
287        }
288
289        // Set hired status if required (I'm a poet and I didn't know it)
290        if let Some(hiring) = npc.controller.hiring.take() {
291            npc.hiring = hiring;
292        }
293    }
294}