veloren_rtsim/rule/
simulate_npcs.rs1use 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 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 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 for action in std::mem::take(&mut npc.controller.actions) {
122 match action {
123 NpcAction::Say(_, _) => {}, NpcAction::Attack(_) => {}, NpcAction::Dialogue(_, _) => {},
126 }
127 }
128
129 let activity = if data.npcs.mounts.get_mount_link(npc_id).is_some() {
130 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 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 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 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 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 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 },
248 None => {},
249 }
250
251 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 if let Some(new_home) = npc.controller.new_home.take() {
276 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 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 if let Some(hiring) = npc.controller.hiring.take() {
291 npc.hiring = hiring;
292 }
293 }
294}