1use crate::{
2 RtState, Rule, RuleError,
3 data::{Npc, npc::SimulationMode},
4 event::{EventCtx, OnDeath, 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_tick);
26 rtstate.bind(on_mount_volume);
27
28 Ok(Self)
29 }
30}
31
32fn on_mount_volume(ctx: EventCtx<SimulateNpcs, OnMountVolume>) {
33 let data = &mut *ctx.state.data_mut();
34
35 if let VolumePos {
37 kind: Volume::Entity(vehicle),
38 ..
39 } = ctx.event.pos
40 && let Some(link) = data.npcs.mounts.get_steerer_link(vehicle)
41 && let Actor::Npc(driver) = link.rider
42 && let Some(driver) = data.npcs.get_mut(driver)
43 {
44 driver.controller.actions.push(NpcAction::Say(
45 Some(ctx.event.actor),
46 comp::Content::localized("npc-speech-welcome-aboard"),
47 ))
48 }
49}
50
51fn on_death(ctx: EventCtx<SimulateNpcs, OnDeath>) {
52 let data = &mut *ctx.state.data_mut();
53
54 if let Actor::Npc(npc_id) = ctx.event.actor {
55 if let Some(npc) = data.npcs.get(npc_id) {
56 let mut rng = ChaChaRng::from_seed(thread_rng().gen::<[u8; 32]>());
57
58 let details = match npc.body {
60 Body::Humanoid(_) => {
61 if let Some((site_id, site)) = data
62 .sites
63 .iter()
64 .filter(|(id, site)| {
65 Some(*id) != npc.home
67 && site.world_site.is_some_and(|s| {
68 matches!(
69 ctx.index.sites.get(s).kind,
70 SiteKind::Refactor(_)
71 | SiteKind::CliffTown(_)
72 | SiteKind::SavannahTown(_)
73 | SiteKind::CoastalTown(_)
74 | SiteKind::DesertCity(_)
75 )
76 })
77 })
78 .min_by_key(|(_, site)| site.population.len())
79 {
80 let rand_wpos = |rng: &mut ChaChaRng| {
81 let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10));
82 wpos2d
83 .map(|e| e as f32 + 0.5)
84 .with_z(ctx.world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
85 };
86 let random_humanoid = |rng: &mut ChaChaRng| {
87 let species = comp::humanoid::ALL_SPECIES.choose(&mut *rng).unwrap();
88 Body::Humanoid(comp::humanoid::Body::random_with(rng, species))
89 };
90 let npc_id = data.spawn_npc(
91 Npc::new(
92 rng.gen(),
93 rand_wpos(&mut rng),
94 random_humanoid(&mut rng),
95 npc.role.clone(),
96 )
97 .with_personality(Personality::random(&mut rng))
98 .with_home(site_id)
99 .with_faction(npc.faction),
100 );
101 Some((npc_id, Some(site_id)))
102 } else {
103 warn!("No site found for respawning humanoid");
104 None
105 }
106 },
107 body => {
108 let home = npc.home.and_then(|_| {
109 data.sites
110 .iter()
111 .filter(|(id, site)| {
112 Some(*id) != npc.home
113 && site.world_site.is_some_and(|s| {
114 matches!(
115 ctx.index.sites.get(s).kind,
116 SiteKind::Terracotta(_)
117 | SiteKind::Haniwa(_)
118 | SiteKind::Myrmidon(_)
119 | SiteKind::Adlet(_)
120 | SiteKind::DwarvenMine(_)
121 | SiteKind::ChapelSite(_)
122 | SiteKind::Cultist(_)
123 | SiteKind::Gnarling(_)
124 | SiteKind::Sahagin(_)
125 | SiteKind::VampireCastle(_),
126 )
127 })
128 })
129 .min_by_key(|(_, site)| site.population.len())
130 });
131
132 let wpos = if let Some((_, home)) = home {
133 let wpos2d = home.wpos.map(|e| e + rng.gen_range(-10..10));
134 wpos2d
135 .map(|e| e as f32 + 0.5)
136 .with_z(ctx.world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
137 } else {
138 let is_gigas = matches!(body, Body::BipedLarge(body) if body.species == comp::body::biped_large::Species::Gigasfrost);
139
140 let pos = (0..(if is_gigas {
141 100
143 } else {
144 10
145 }))
146 .map(|_| {
147 ctx.world
148 .sim()
149 .get_size()
150 .map(|sz| rng.gen_range(0..sz as i32))
151 })
152 .find(|pos| {
153 ctx.world.sim().get(*pos).is_some_and(|c| {
154 !c.is_underwater() && (!is_gigas || c.temp < CONFIG.snow_temp)
155 })
156 })
157 .unwrap_or(ctx.world.sim().get_size().as_() / 2);
158 let wpos2d = pos.cpos_to_wpos_center();
159 wpos2d
160 .map(|e| e as f32 + 0.5)
161 .with_z(ctx.world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
162 };
163
164 let home = home.map(|(site_id, _)| site_id);
165
166 let npc_id = data.npcs.create_npc(
167 Npc::new(rng.gen(), wpos, body, npc.role.clone()).with_home(home),
168 );
169 Some((npc_id, home))
170 },
171 };
172
173 if let Some((npc_id, Some(home_site))) = details {
175 if let Some(home) = data.sites.get_mut(home_site) {
176 home.population.insert(npc_id);
177 }
178 }
179 } else {
180 error!("Trying to respawn non-existent NPC");
181 }
182 }
183}
184
185fn on_tick(ctx: EventCtx<SimulateNpcs, OnTick>) {
186 let data = &mut *ctx.state.data_mut();
187
188 let ids = data.npcs.mounts.ids().collect::<Vec<_>>();
190 let mut mount_activity = SecondaryMap::new();
191 for link_id in ids {
192 if let Some(link) = data.npcs.mounts.get(link_id) {
193 if let Some(mount) = data
194 .npcs
195 .npcs
196 .get(link.mount)
197 .filter(|mount| !mount.is_dead())
198 {
199 let wpos = mount.wpos;
200 if let Actor::Npc(rider) = link.rider {
201 if let Some(rider) = data
202 .npcs
203 .npcs
204 .get_mut(rider)
205 .filter(|rider| !rider.is_dead())
206 {
207 rider.wpos = wpos;
208 mount_activity.insert(link.mount, rider.controller.activity);
209 } else {
210 data.npcs.mounts.dismount(link.rider)
211 }
212 }
213 } else {
214 data.npcs.mounts.remove_mount(link.mount)
215 }
216 }
217 }
218
219 for (npc_id, npc) in data.npcs.npcs.iter_mut().filter(|(_, npc)| !npc.is_dead()) {
220 if matches!(npc.mode, SimulationMode::Simulated) {
221 for action in std::mem::take(&mut npc.controller.actions) {
223 match action {
224 NpcAction::Say(_, _) => {}, NpcAction::Attack(_) => {}, NpcAction::Dialogue(_, _) => {},
227 }
228 }
229
230 let activity = if data.npcs.mounts.get_mount_link(npc_id).is_some() {
231 continue;
233 } else if let Some(activity) = mount_activity.get(npc_id) {
234 *activity
235 } else {
236 npc.controller.activity
237 };
238
239 match activity {
240 Some(NpcActivity::Goto(target, speed_factor)) => {
242 let diff = target - npc.wpos;
243 let dist2 = diff.magnitude_squared();
244
245 if dist2 > 0.5f32.powi(2) {
246 let offset = diff
247 * (npc.body.max_speed_approx() * speed_factor * ctx.event.dt
248 / dist2.sqrt())
249 .min(1.0);
250 let new_wpos = npc.wpos + offset;
251
252 let is_valid = match npc.body {
253 Body::Ship(comp::ship::Body::SailBoat | comp::ship::Body::Galleon)
255 | Body::FishMedium(_)
256 | Body::FishSmall(_) => {
257 let chunk_pos = new_wpos.xy().as_().wpos_to_cpos();
258 ctx.world
259 .sim()
260 .get(chunk_pos)
261 .is_none_or(|f| f.river.river_kind.is_some())
262 },
263 Body::Ship(comp::ship::Body::DefaultAirship) => false,
264 _ => true,
265 };
266
267 if is_valid {
268 npc.wpos = new_wpos;
269 }
270
271 npc.dir = (target.xy() - npc.wpos.xy())
272 .try_normalized()
273 .unwrap_or(npc.dir);
274 }
275 },
276 Some(NpcActivity::GotoFlying(target, speed_factor, _, dir, _)) => {
278 let diff = target - npc.wpos;
279 let dist2 = diff.magnitude_squared();
280
281 if dist2 > 0.5f32.powi(2) {
282 match npc.body {
283 Body::Ship(comp::ship::Body::DefaultAirship) => {
284 let offset = (diff - npc.body.mount_offset())
296 * (npc.body.max_speed_approx() * speed_factor * ctx.event.dt
297 / dist2.sqrt());
298 npc.wpos += offset;
299 },
300 _ => {
301 let offset = diff
302 * (npc.body.max_speed_approx() * speed_factor * ctx.event.dt
303 / dist2.sqrt())
304 .min(1.0);
305 let new_wpos = npc.wpos + offset;
306
307 let is_valid = match npc.body {
308 Body::Ship(
310 comp::ship::Body::SailBoat | comp::ship::Body::Galleon,
311 )
312 | Body::FishMedium(_)
313 | Body::FishSmall(_) => {
314 let chunk_pos = new_wpos.xy().as_().wpos_to_cpos();
315 ctx.world
316 .sim()
317 .get(chunk_pos)
318 .is_none_or(|f| f.river.river_kind.is_some())
319 },
320 _ => true,
321 };
322
323 if is_valid {
324 npc.wpos = new_wpos;
325 }
326 },
327 }
328
329 if let Some(dir_override) = dir {
330 npc.dir = dir_override.xy().try_normalized().unwrap_or(npc.dir);
331 } else {
332 npc.dir = (target.xy() - npc.wpos.xy())
333 .try_normalized()
334 .unwrap_or(npc.dir);
335 }
336 }
337 },
338 Some(
339 NpcActivity::Gather(_)
340 | NpcActivity::HuntAnimals
341 | NpcActivity::Dance(_)
342 | NpcActivity::Cheer(_)
343 | NpcActivity::Sit(..)
344 | NpcActivity::Talk(..),
345 ) => {
346 },
349 None => {},
350 }
351
352 let clamped_wpos = npc.wpos.xy().clamped(
354 Vec2::zero(),
355 (ctx.world.sim().get_size() * TerrainChunkSize::RECT_SIZE).as_(),
356 );
357 match npc.body {
358 Body::Ship(comp::ship::Body::DefaultAirship | comp::ship::Body::AirBalloon) => {
359 npc.wpos = clamped_wpos.with_z(
360 ctx.world
361 .sim()
362 .get_surface_alt_approx(clamped_wpos.as_())
363 .max(npc.wpos.z),
364 );
365 },
366 _ => {
367 npc.wpos = clamped_wpos.with_z(
368 ctx.world.sim().get_surface_alt_approx(clamped_wpos.as_())
369 + npc.body.flying_height(),
370 );
371 },
372 }
373 }
374
375 if let Some(new_home) = npc.controller.new_home.take() {
377 if let Some(old_home) = npc.home {
379 if let Some(old_home) = data.sites.get_mut(old_home) {
380 old_home.population.remove(&npc_id);
381 }
382 }
383 if let Some(new_home) = data.sites.get_mut(new_home) {
385 new_home.population.insert(npc_id);
386 }
387 npc.home = Some(new_home);
388 }
389
390 if let Some(hiring) = npc.controller.hiring.take() {
392 npc.hiring = hiring;
393 }
394 }
395}