1use crate::{
2 RtState, Rule, RuleError,
3 data::{Npc, Sentiment, npc::SimulationMode},
4 event::{EventCtx, OnDeath, OnHealthChange, OnHelped, OnMountVolume, OnTick},
5};
6use common::{
7 comp::{self, Body},
8 mounting::{Volume, VolumePos},
9 rtsim::{Actor, NpcAction, NpcActivity, Personality},
10 terrain::{CoordinateConversions, TerrainChunkSize},
11 vol::RectVolSize,
12};
13use rand::prelude::*;
14use rand_chacha::ChaChaRng;
15use slotmap::SecondaryMap;
16use tracing::{error, warn};
17use vek::{Clamp, Vec2};
18use world::{CONFIG, site::SiteKind};
19
20pub struct SimulateNpcs;
21
22impl Rule for SimulateNpcs {
23 fn start(rtstate: &mut RtState) -> Result<Self, RuleError> {
24 rtstate.bind(on_death);
25 rtstate.bind(on_helped);
26 rtstate.bind(on_health_changed);
27 rtstate.bind(on_mount_volume);
28 rtstate.bind(on_tick);
29
30 Ok(Self)
31 }
32}
33
34fn on_mount_volume(ctx: EventCtx<SimulateNpcs, OnMountVolume>) {
35 let data = &mut *ctx.state.data_mut();
36
37 if let VolumePos {
39 kind: Volume::Entity(vehicle),
40 ..
41 } = ctx.event.pos
42 && let Some(link) = data.npcs.mounts.get_steerer_link(vehicle)
43 && let Actor::Npc(driver) = link.rider
44 && let Some(driver) = data.npcs.get_mut(driver)
45 {
46 driver.controller.actions.push(NpcAction::Say(
47 Some(ctx.event.actor),
48 comp::Content::localized("npc-speech-welcome-aboard"),
49 ))
50 }
51}
52
53fn on_death(ctx: EventCtx<SimulateNpcs, OnDeath>) {
54 let data = &mut *ctx.state.data_mut();
55
56 if let Actor::Npc(npc_id) = ctx.event.actor {
57 if let Some(npc) = data.npcs.get(npc_id) {
58 let mut rng = ChaChaRng::from_seed(thread_rng().gen::<[u8; 32]>());
59
60 let details = match npc.body {
62 Body::Humanoid(_) => {
63 if let Some((site_id, site)) = data
64 .sites
65 .iter()
66 .filter(|(id, site)| {
67 Some(*id) != npc.home
69 && site.world_site.is_some_and(|s| {
70 matches!(
71 ctx.index.sites.get(s).kind,
72 SiteKind::Refactor(_)
73 | SiteKind::CliffTown(_)
74 | SiteKind::SavannahTown(_)
75 | SiteKind::CoastalTown(_)
76 | SiteKind::DesertCity(_)
77 )
78 })
79 })
80 .min_by_key(|(_, site)| site.population.len())
81 {
82 let rand_wpos = |rng: &mut ChaChaRng| {
83 let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10));
84 wpos2d
85 .map(|e| e as f32 + 0.5)
86 .with_z(ctx.world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
87 };
88 let random_humanoid = |rng: &mut ChaChaRng| {
89 let species = comp::humanoid::ALL_SPECIES.choose(&mut *rng).unwrap();
90 Body::Humanoid(comp::humanoid::Body::random_with(rng, species))
91 };
92 let npc_id = data.spawn_npc(
93 Npc::new(
94 rng.gen(),
95 rand_wpos(&mut rng),
96 random_humanoid(&mut rng),
97 npc.role.clone(),
98 )
99 .with_personality(Personality::random(&mut rng))
100 .with_home(site_id)
101 .with_faction(npc.faction),
102 );
103 Some((npc_id, Some(site_id)))
104 } else {
105 warn!("No site found for respawning humanoid");
106 None
107 }
108 },
109 body => {
110 let home = npc.home.and_then(|_| {
111 data.sites
112 .iter()
113 .filter(|(id, site)| {
114 Some(*id) != npc.home
115 && site.world_site.is_some_and(|s| {
116 matches!(
117 ctx.index.sites.get(s).kind,
118 SiteKind::Terracotta(_)
119 | SiteKind::Haniwa(_)
120 | SiteKind::Myrmidon(_)
121 | SiteKind::Adlet(_)
122 | SiteKind::DwarvenMine(_)
123 | SiteKind::ChapelSite(_)
124 | SiteKind::Cultist(_)
125 | SiteKind::Gnarling(_)
126 | SiteKind::Sahagin(_)
127 | SiteKind::VampireCastle(_),
128 )
129 })
130 })
131 .min_by_key(|(_, site)| site.population.len())
132 });
133
134 let wpos = if let Some((_, home)) = home {
135 let wpos2d = home.wpos.map(|e| e + rng.gen_range(-10..10));
136 wpos2d
137 .map(|e| e as f32 + 0.5)
138 .with_z(ctx.world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
139 } else {
140 let is_gigas = matches!(body, Body::BipedLarge(body) if body.species == comp::body::biped_large::Species::Gigasfrost);
141
142 let pos = (0..(if is_gigas {
143 100
145 } else {
146 10
147 }))
148 .map(|_| {
149 ctx.world
150 .sim()
151 .get_size()
152 .map(|sz| rng.gen_range(0..sz as i32))
153 })
154 .find(|pos| {
155 ctx.world.sim().get(*pos).is_some_and(|c| {
156 !c.is_underwater() && (!is_gigas || c.temp < CONFIG.snow_temp)
157 })
158 })
159 .unwrap_or(ctx.world.sim().get_size().as_() / 2);
160 let wpos2d = pos.cpos_to_wpos_center();
161 wpos2d
162 .map(|e| e as f32 + 0.5)
163 .with_z(ctx.world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
164 };
165
166 let home = home.map(|(site_id, _)| site_id);
167
168 let npc_id = data.npcs.create_npc(
169 Npc::new(rng.gen(), wpos, body, npc.role.clone()).with_home(home),
170 );
171 Some((npc_id, home))
172 },
173 };
174
175 if let Some((npc_id, Some(home_site))) = details {
177 if let Some(home) = data.sites.get_mut(home_site) {
178 home.population.insert(npc_id);
179 }
180 }
181 } else {
182 error!("Trying to respawn non-existent NPC");
183 }
184 }
185}
186
187fn on_health_changed(ctx: EventCtx<SimulateNpcs, OnHealthChange>) {
188 let data = &mut *ctx.state.data_mut();
189
190 if let Some(cause) = ctx.event.cause
191 && let Actor::Npc(npc) = ctx.event.actor
192 && let Some(npc) = data.npcs.get_mut(npc)
193 {
194 if ctx.event.change < 0.0 {
195 npc.sentiments
196 .toward_mut(cause)
197 .change_by(-0.1, Sentiment::ENEMY);
198 } else if ctx.event.change > 0.0 {
199 npc.sentiments
200 .toward_mut(cause)
201 .change_by(0.05, Sentiment::POSITIVE);
202 }
203 }
204}
205
206fn on_helped(ctx: EventCtx<SimulateNpcs, OnHelped>) {
207 let data = &mut *ctx.state.data_mut();
208
209 if let Some(saver) = ctx.event.saver
210 && let Actor::Npc(npc) = ctx.event.actor
211 && let Some(npc) = data.npcs.get_mut(npc)
212 {
213 npc.controller.actions.push(NpcAction::Say(
214 Some(ctx.event.actor),
215 comp::Content::localized("npc-speech-thank_you"),
216 ));
217 npc.sentiments
218 .toward_mut(saver)
219 .change_by(0.3, Sentiment::FRIEND);
220 }
221}
222
223fn on_tick(ctx: EventCtx<SimulateNpcs, OnTick>) {
224 let data = &mut *ctx.state.data_mut();
225
226 let ids = data.npcs.mounts.ids().collect::<Vec<_>>();
228 let mut mount_activity = SecondaryMap::new();
229 for link_id in ids {
230 if let Some(link) = data.npcs.mounts.get(link_id) {
231 if let Some(mount) = data
232 .npcs
233 .npcs
234 .get(link.mount)
235 .filter(|mount| !mount.is_dead())
236 {
237 let wpos = mount.wpos;
238 if let Actor::Npc(rider) = link.rider {
239 if let Some(rider) = data
240 .npcs
241 .npcs
242 .get_mut(rider)
243 .filter(|rider| !rider.is_dead())
244 {
245 rider.wpos = wpos;
246 mount_activity.insert(link.mount, rider.controller.activity);
247 } else {
248 data.npcs.mounts.dismount(link.rider)
249 }
250 }
251 } else {
252 data.npcs.mounts.remove_mount(link.mount)
253 }
254 }
255 }
256
257 for (npc_id, npc) in data.npcs.npcs.iter_mut().filter(|(_, npc)| !npc.is_dead()) {
258 if matches!(npc.mode, SimulationMode::Simulated) {
259 for action in std::mem::take(&mut npc.controller.actions) {
261 match action {
262 NpcAction::Say(_, _) => {}, NpcAction::Attack(_) => {}, NpcAction::Dialogue(_, _) => {},
265 }
266 }
267
268 let activity = if data.npcs.mounts.get_mount_link(npc_id).is_some() {
269 continue;
271 } else if let Some(activity) = mount_activity.get(npc_id) {
272 *activity
273 } else {
274 npc.controller.activity
275 };
276
277 match activity {
278 Some(NpcActivity::Goto(target, speed_factor)) => {
280 let diff = target - npc.wpos;
281 let dist2 = diff.magnitude_squared();
282
283 if dist2 > 0.5f32.powi(2) {
284 let offset = diff
285 * (npc.body.max_speed_approx() * speed_factor * ctx.event.dt
286 / dist2.sqrt())
287 .min(1.0);
288 let new_wpos = npc.wpos + offset;
289
290 let is_valid = match npc.body {
291 Body::Ship(comp::ship::Body::SailBoat | comp::ship::Body::Galleon)
293 | Body::FishMedium(_)
294 | Body::FishSmall(_) => {
295 let chunk_pos = new_wpos.xy().as_().wpos_to_cpos();
296 ctx.world
297 .sim()
298 .get(chunk_pos)
299 .is_none_or(|f| f.river.river_kind.is_some())
300 },
301 Body::Ship(comp::ship::Body::DefaultAirship) => false,
302 _ => true,
303 };
304
305 if is_valid {
306 npc.wpos = new_wpos;
307 }
308
309 npc.dir = (target.xy() - npc.wpos.xy())
310 .try_normalized()
311 .unwrap_or(npc.dir);
312 }
313 },
314 Some(NpcActivity::GotoFlying(target, speed_factor, _, dir, _)) => {
316 let diff = target - npc.wpos;
317 let dist2 = diff.magnitude_squared();
318
319 if dist2 > 0.5f32.powi(2) {
320 match npc.body {
321 Body::Ship(comp::ship::Body::DefaultAirship) => {
322 let offset = (diff - npc.body.mount_offset())
334 * (npc.body.max_speed_approx() * speed_factor * ctx.event.dt
335 / dist2.sqrt());
336 npc.wpos += offset;
337 },
338 _ => {
339 let offset = diff
340 * (npc.body.max_speed_approx() * speed_factor * ctx.event.dt
341 / dist2.sqrt())
342 .min(1.0);
343 let new_wpos = npc.wpos + offset;
344
345 let is_valid = match npc.body {
346 Body::Ship(
348 comp::ship::Body::SailBoat | comp::ship::Body::Galleon,
349 )
350 | Body::FishMedium(_)
351 | Body::FishSmall(_) => {
352 let chunk_pos = new_wpos.xy().as_().wpos_to_cpos();
353 ctx.world
354 .sim()
355 .get(chunk_pos)
356 .is_none_or(|f| f.river.river_kind.is_some())
357 },
358 _ => true,
359 };
360
361 if is_valid {
362 npc.wpos = new_wpos;
363 }
364 },
365 }
366
367 if let Some(dir_override) = dir {
368 npc.dir = dir_override.xy().try_normalized().unwrap_or(npc.dir);
369 } else {
370 npc.dir = (target.xy() - npc.wpos.xy())
371 .try_normalized()
372 .unwrap_or(npc.dir);
373 }
374 }
375 },
376 Some(
377 NpcActivity::Gather(_)
378 | NpcActivity::HuntAnimals
379 | NpcActivity::Dance(_)
380 | NpcActivity::Cheer(_)
381 | NpcActivity::Sit(..)
382 | NpcActivity::Talk(..),
383 ) => {
384 },
387 None => {},
388 }
389
390 let clamped_wpos = npc.wpos.xy().clamped(
392 Vec2::zero(),
393 (ctx.world.sim().get_size() * TerrainChunkSize::RECT_SIZE).as_(),
394 );
395 match npc.body {
396 Body::Ship(comp::ship::Body::DefaultAirship | comp::ship::Body::AirBalloon) => {
397 npc.wpos = clamped_wpos.with_z(
398 ctx.world
399 .sim()
400 .get_surface_alt_approx(clamped_wpos.as_())
401 .max(npc.wpos.z),
402 );
403 },
404 _ => {
405 npc.wpos = clamped_wpos.with_z(
406 ctx.world.sim().get_surface_alt_approx(clamped_wpos.as_())
407 + npc.body.flying_height(),
408 );
409 },
410 }
411 }
412
413 if let Some(new_home) = npc.controller.new_home.take() {
415 if let Some(old_home) = npc.home {
417 if let Some(old_home) = data.sites.get_mut(old_home) {
418 old_home.population.remove(&npc_id);
419 }
420 }
421 if let Some(new_home) = data.sites.get_mut(new_home) {
423 new_home.population.insert(npc_id);
424 }
425 npc.home = Some(new_home);
426 }
427
428 if let Some(hiring) = npc.controller.hiring.take() {
430 npc.hiring = hiring;
431 }
432 }
433}