1use common::{
2 comp::{self, Body},
3 resources::TimeOfDay,
4 rtsim::{Actor, Personality, Profession, Role},
5 terrain::CoordinateConversions,
6};
7use rand::{
8 Rng,
9 seq::{IteratorRandom, SliceRandom},
10 thread_rng,
11};
12use world::{CONFIG, IndexRef, World, sim::SimChunk, site::SiteKind};
13
14use crate::{
15 Data, EventCtx, OnTick, RtState,
16 data::{
17 Npc,
18 architect::{Death, TrackedPopulation},
19 },
20 event::OnDeath,
21};
22
23use super::{Rule, RuleError};
24
25const ARCHITECT_TICK_SKIP: u64 = 32;
29const MIN_SPAWN_DELAY: f64 = 60.0 * 60.0 * 24.0;
31const RESPAWN_ATTEMPTS: usize = 30;
34
35pub struct Architect;
36
37impl Rule for Architect {
38 fn start(rtstate: &mut RtState) -> Result<Self, RuleError> {
39 rtstate.bind(on_death);
40 rtstate.bind(architect_tick);
41
42 Ok(Self)
43 }
44}
45
46fn on_death(ctx: EventCtx<Architect, OnDeath>) {
47 let data = &mut *ctx.state.data_mut();
48
49 if let Actor::Npc(npc_id) = ctx.event.actor
50 && let Some(npc) = data.npcs.get(npc_id)
51 {
52 data.architect.on_death(npc, data.time_of_day);
53 }
54}
55
56fn architect_tick(ctx: EventCtx<Architect, OnTick>) {
57 if ctx.event.tick % ARCHITECT_TICK_SKIP != 0 {
58 return;
59 }
60
61 let tod = ctx.event.time_of_day;
62
63 let data = &mut *ctx.state.data_mut();
64
65 let mut rng = thread_rng();
66 let mut count_to_spawn = rng.gen_range(1..20);
67
68 let pop = data.architect.population.clone();
69 'outer: for (pop, count) in pop
70 .iter()
71 .zip(data.architect.wanted_population.iter())
72 .filter(|((_, current), (_, wanted))| current < wanted)
73 .map(|((pop, current), (_, wanted))| (pop, wanted - current))
74 {
75 for _ in 0..count {
76 let (body, role) = match pop {
77 TrackedPopulation::Adventurers => (
78 Body::Humanoid(comp::humanoid::Body::random()),
79 Role::Civilised(Some(Profession::Adventurer(rng.gen_range(0..=3)))),
80 ),
81 TrackedPopulation::Merchants => (
82 Body::Humanoid(comp::humanoid::Body::random()),
83 Role::Civilised(Some(Profession::Merchant)),
84 ),
85 TrackedPopulation::Guards => (
86 Body::Humanoid(comp::humanoid::Body::random()),
87 Role::Civilised(Some(Profession::Guard)),
88 ),
89 TrackedPopulation::Captains => (
90 Body::Humanoid(comp::humanoid::Body::random()),
91 Role::Civilised(Some(Profession::Captain)),
92 ),
93 TrackedPopulation::OtherTownNpcs => (
94 Body::Humanoid(comp::humanoid::Body::random()),
95 Role::Civilised(Some(match rng.gen_range(0..10) {
96 0 => Profession::Hunter,
97 1 => Profession::Blacksmith,
98 2 => Profession::Chef,
99 3 => Profession::Alchemist,
100 4..=5 => Profession::Herbalist,
101 _ => Profession::Farmer,
102 })),
103 ),
104 TrackedPopulation::Pirates => (
105 Body::Humanoid(comp::humanoid::Body::random()),
106 Role::Civilised(Some(Profession::Pirate(false))),
107 ),
108 TrackedPopulation::PirateCaptains => (
109 Body::Humanoid(comp::humanoid::Body::random()),
110 Role::Civilised(Some(Profession::Pirate(true))),
111 ),
112 TrackedPopulation::Cultists => (
113 Body::Humanoid(comp::humanoid::Body::random()),
114 Role::Civilised(Some(Profession::Cultist)),
115 ),
116 TrackedPopulation::GigasFrost => (
117 Body::BipedLarge(comp::biped_large::Body::random_with(
118 &mut rng,
119 &comp::biped_large::Species::Gigasfrost,
120 )),
121 Role::Monster,
122 ),
123 TrackedPopulation::GigasFire => (
124 Body::BipedLarge(comp::biped_large::Body::random_with(
125 &mut rng,
126 &comp::biped_large::Species::Gigasfire,
127 )),
128 Role::Monster,
129 ),
130 TrackedPopulation::OtherMonsters => {
131 let species = [
132 comp::biped_large::Species::Ogre,
133 comp::biped_large::Species::Cyclops,
134 comp::biped_large::Species::Wendigo,
135 comp::biped_large::Species::Cavetroll,
136 comp::biped_large::Species::Mountaintroll,
137 comp::biped_large::Species::Swamptroll,
138 comp::biped_large::Species::Blueoni,
139 comp::biped_large::Species::Redoni,
140 comp::biped_large::Species::Tursus,
141 ]
142 .choose(&mut rng)
143 .unwrap();
144
145 (
146 Body::BipedLarge(comp::biped_large::Body::random_with(&mut rng, species)),
147 Role::Monster,
148 )
149 },
150 TrackedPopulation::CloudWyvern => (
151 Body::BirdLarge(comp::bird_large::Body::random_with(
152 &mut rng,
153 &comp::bird_large::Species::CloudWyvern,
154 )),
155 Role::Wild,
156 ),
157 TrackedPopulation::FrostWyvern => (
158 Body::BirdLarge(comp::bird_large::Body::random_with(
159 &mut rng,
160 &comp::bird_large::Species::FrostWyvern,
161 )),
162 Role::Wild,
163 ),
164 TrackedPopulation::SeaWyvern => (
165 Body::BirdLarge(comp::bird_large::Body::random_with(
166 &mut rng,
167 &comp::bird_large::Species::SeaWyvern,
168 )),
169 Role::Wild,
170 ),
171 TrackedPopulation::FlameWyvern => (
172 Body::BirdLarge(comp::bird_large::Body::random_with(
173 &mut rng,
174 &comp::bird_large::Species::FlameWyvern,
175 )),
176 Role::Wild,
177 ),
178 TrackedPopulation::WealdWyvern => (
179 Body::BirdLarge(comp::bird_large::Body::random_with(
180 &mut rng,
181 &comp::bird_large::Species::WealdWyvern,
182 )),
183 Role::Wild,
184 ),
185 TrackedPopulation::Phoenix => (
186 Body::BirdLarge(comp::bird_large::Body::random_with(
187 &mut rng,
188 &comp::bird_large::Species::Phoenix,
189 )),
190 Role::Wild,
191 ),
192 TrackedPopulation::Roc => (
193 Body::BirdLarge(comp::bird_large::Body::random_with(
194 &mut rng,
195 &comp::bird_large::Species::Roc,
196 )),
197 Role::Wild,
198 ),
199 TrackedPopulation::Cockatrice => (
200 Body::BirdLarge(comp::bird_large::Body::random_with(
201 &mut rng,
202 &comp::bird_large::Species::Cockatrice,
203 )),
204 Role::Wild,
205 ),
206 TrackedPopulation::Other => continue 'outer,
207 };
208
209 let fake_death = Death {
210 time: TimeOfDay(tod.0 - MIN_SPAWN_DELAY),
211 body,
212 role,
213 faction: None,
214 };
215
216 data.architect.population.on_spawn(&fake_death);
217
218 data.architect.deaths.push_front(fake_death);
219 }
220
221 count_to_spawn += count;
222 }
223
224 let mut failed_spawn = Vec::new();
226
227 while count_to_spawn > 0
228 && let Some(death) = data.architect.deaths.pop_front()
229 {
230 if data.architect.population.of_death(&death)
231 > data.architect.wanted_population.of_death(&death)
232 {
233 data.architect.population.on_death(&death);
234 continue;
236 }
237
238 if death.time.0 + MIN_SPAWN_DELAY > tod.0 {
239 data.architect.deaths.push_front(death);
240 break;
241 }
242
243 if spawn_npc(data, ctx.world, ctx.index, &death) {
244 count_to_spawn -= 1;
245 } else {
246 failed_spawn.push(death);
247 }
248 }
249
250 for death in failed_spawn.into_iter().rev() {
251 data.architect.deaths.push_front(death);
252 }
253}
254
255fn randomize_body(body: Body, rng: &mut impl Rng) -> Body {
256 let mut random_humanoid = || {
257 let species = comp::humanoid::ALL_SPECIES.choose(rng).unwrap();
258 Body::Humanoid(comp::humanoid::Body::random_with(rng, species))
259 };
260 match body {
261 Body::Humanoid(_) => random_humanoid(),
262 body => body,
263 }
264}
265
266fn role_personality(rng: &mut impl Rng, role: &Role) -> Personality {
267 match role {
268 Role::Civilised(profession) => match profession {
269 Some(Profession::Guard | Profession::Merchant | Profession::Captain) => {
270 Personality::random_good(rng)
271 },
272 Some(Profession::Cultist | Profession::Pirate(_)) => Personality::random_evil(rng),
273 None
274 | Some(
275 Profession::Farmer
276 | Profession::Chef
277 | Profession::Hunter
278 | Profession::Blacksmith
279 | Profession::Alchemist
280 | Profession::Herbalist
281 | Profession::Adventurer(_),
282 ) => Personality::random(rng),
283 },
284 Role::Wild => Personality::random(rng),
285 Role::Monster => Personality::random_evil(rng),
286 Role::Vehicle => Personality::default(),
287 }
288}
289
290fn spawn_anywhere(
291 data: &mut Data,
292 world: &World,
293 death: &Death,
294 rng: &mut impl Rng,
295 body: Body,
296 personality: Personality,
297) {
298 let mut attempt = |check: bool| {
299 let cpos = world
300 .sim()
301 .map_size_lg()
302 .chunks()
303 .map(|s| rng.gen_range(0..s as i32));
304
305 if let Some(chunk) = world.sim().get(cpos)
308 && (!check || !chunk.is_underwater())
309 {
310 let wpos = cpos.cpos_to_wpos_center();
311 let wpos = wpos.as_().with_z(world.sim().get_surface_alt_approx(wpos));
312
313 data.spawn_npc(
314 Npc::new(rng.gen(), wpos, body, death.role.clone()).with_personality(personality),
315 );
316 return true;
317 }
318
319 false
320 };
321 for _ in 0..RESPAWN_ATTEMPTS {
322 if attempt(true) {
323 return;
324 }
325 }
326 attempt(false);
327}
328
329fn spawn_at_plot(
330 data: &mut Data,
331 world: &World,
332 index: IndexRef,
333 death: &Death,
334 rng: &mut impl Rng,
335 body: Body,
336 personality: Personality,
337 match_plot: impl Fn(&Data, common::rtsim::SiteId, &world::site::Plot) -> bool,
338) -> bool {
339 let sites = &index.sites;
340 let data_ref = &*data;
341 let match_plot = &match_plot;
342 if let Some((id, site, plot)) = data
343 .sites
344 .iter()
345 .filter(|(_, site)| !site.is_loaded())
346 .filter_map(|(id, site)| Some((id, site.world_site?)))
347 .flat_map(|(id, world_site)| {
348 let world_site = sites.get(world_site);
349 world_site
350 .filter_plots(move |plot| match_plot(data_ref, id, plot))
351 .map(move |plot| (id, world_site, plot))
352 })
353 .choose(rng)
354 {
355 let wpos = site.tile_center_wpos(plot.root_tile());
356 let wpos = wpos
357 .as_()
358 .with_z(world.sim().get_alt_approx(wpos).unwrap_or(0.0));
359 let mut npc = Npc::new(rng.gen(), wpos, body, death.role.clone())
360 .with_personality(personality)
361 .with_home(id);
362 if let Some(faction) = data.sites[id].faction {
363 npc = npc.with_faction(faction);
364 }
365 data.spawn_npc(npc);
366
367 true
368 } else {
369 false
370 }
371}
372
373fn spawn_profession(
374 data: &mut Data,
375 world: &World,
376 index: IndexRef,
377 death: &Death,
378 rng: &mut impl Rng,
379 body: Body,
380 personality: Personality,
381 profession: Option<Profession>,
382) -> bool {
383 match profession {
384 Some(Profession::Pirate(captain)) => {
385 spawn_at_plot(
386 data,
387 world,
388 index,
389 death,
390 rng,
391 body,
392 personality,
393 |data, s, p| {
394 if captain
396 && data.sites[s].population.iter().any(|npc| {
397 data.npcs.get(*npc).is_some_and(|npc| {
398 matches!(npc.profession(), Some(Profession::Pirate(true)))
399 })
400 })
401 {
402 return false;
403 }
404 matches!(p.kind(), world::site::PlotKind::PirateHideout(_))
405 },
406 )
407 },
408 _ => spawn_at_plot(
409 data,
410 world,
411 index,
412 death,
413 rng,
414 body,
415 personality,
416 |_, _, p| {
417 matches!(
418 p.kind().meta(),
419 Some(world::site::plot::PlotKindMeta::House { .. })
420 )
421 },
422 ),
423 }
424}
425
426fn spawn_npc(data: &mut Data, world: &World, index: IndexRef, death: &Death) -> bool {
427 let mut rng = thread_rng();
428 let body = randomize_body(death.body, &mut rng);
429 let personality = role_personality(&mut rng, &death.role);
430 let did_spawn = if let Some(faction_id) = death.faction
432 && data.factions.get(faction_id).is_some()
433 {
434 if let Some((id, site)) = data
435 .sites
436 .iter()
437 .filter(|(_, site)| site.faction == Some(faction_id) && !site.is_loaded())
438 .choose(&mut rng)
439 {
440 let wpos = site.wpos;
441 let wpos = wpos
442 .as_()
443 .with_z(world.sim().get_alt_approx(wpos).unwrap_or(0.0));
444 data.spawn_npc(
445 Npc::new(rng.gen(), wpos, body, death.role.clone())
446 .with_personality(personality)
447 .with_home(id)
448 .with_faction(faction_id),
449 );
450
451 true
452 } else {
453 false
454 }
455 } else {
456 match &death.role {
457 Role::Civilised(profession) => spawn_profession(
458 data,
459 world,
460 index,
461 death,
462 &mut rng,
463 body,
464 personality,
465 *profession,
466 ),
467 Role::Wild => {
468 let site_filter: fn(&SiteKind) -> bool = match body {
469 Body::BirdLarge(body) => match body.species {
470 comp::bird_large::Species::Phoenix => {
471 |site| matches!(site, SiteKind::DwarvenMine)
472 },
473 comp::bird_large::Species::Cockatrice => {
474 |site| matches!(site, SiteKind::Myrmidon)
475 },
476 comp::bird_large::Species::Roc => |site| matches!(site, SiteKind::Haniwa),
477 comp::bird_large::Species::FlameWyvern => {
478 |site| matches!(site, SiteKind::Terracotta)
479 },
480 comp::bird_large::Species::CloudWyvern => {
481 |site| matches!(site, SiteKind::Sahagin)
482 },
483 comp::bird_large::Species::FrostWyvern => {
484 |site| matches!(site, SiteKind::Adlet)
485 },
486 comp::bird_large::Species::SeaWyvern => {
487 |site| matches!(site, SiteKind::ChapelSite)
488 },
489 comp::bird_large::Species::WealdWyvern => {
490 |site| matches!(site, SiteKind::GiantTree)
491 },
492 },
493 _ => |_| true,
494 };
495
496 if let Some((id, site)) = data
497 .sites
498 .iter()
499 .filter(|(_, site)| {
500 !site.is_loaded()
501 && site
502 .world_site
503 .and_then(|s| index.sites.get(s).kind)
504 .is_some_and(|s| site_filter(&s))
505 })
506 .choose(&mut rng)
507 {
508 let wpos = site.wpos;
509 let wpos = wpos
510 .as_()
511 .with_z(world.sim().get_alt_approx(wpos).unwrap_or(0.0));
512 data.spawn_npc(
513 Npc::new(rng.gen(), wpos, body, death.role.clone())
514 .with_personality(personality)
515 .with_home(id),
516 );
517 true
518 } else {
519 false
520 }
521 },
522 Role::Monster => {
523 let chunk_filter: fn(&SimChunk) -> bool = match body {
524 Body::BipedLarge(body) => match body.species {
525 comp::biped_large::Species::Tursus
526 | comp::biped_large::Species::Gigasfrost
527 | comp::biped_large::Species::Wendigo => {
528 |chunk| !chunk.is_underwater() && chunk.temp < CONFIG.snow_temp
529 },
530 comp::biped_large::Species::Gigasfire => |chunk| {
531 !chunk.is_underwater()
532 && chunk.temp > CONFIG.desert_temp
533 && chunk.humidity < CONFIG.desert_hum
534 },
535 comp::biped_large::Species::Mountaintroll => {
536 |chunk| !chunk.is_underwater() && chunk.alt > 500.0
537 },
538 comp::biped_large::Species::Swamptroll => {
539 |chunk| !chunk.is_underwater() && chunk.humidity > CONFIG.jungle_hum
540 },
541 _ => |chunk| !chunk.is_underwater(),
542 },
543 Body::Arthropod(_)
544 | Body::Humanoid(_)
545 | Body::QuadrupedSmall(_)
546 | Body::BipedSmall(_)
547 | Body::QuadrupedMedium(_)
548 | Body::Golem(_)
549 | Body::Theropod(_)
550 | Body::QuadrupedLow(_) => |chunk| !chunk.is_underwater(),
551 Body::Dragon(_) | Body::BirdLarge(_) | Body::BirdMedium(_) => |_| true,
552 Body::Crustacean(_) | Body::FishSmall(_) | Body::FishMedium(_) => {
553 |chunk| chunk.is_underwater()
554 },
555 Body::Object(_) | Body::Ship(_) | Body::Item(_) | Body::Plugin(_) => |_| true,
556 };
557
558 for _ in 0..RESPAWN_ATTEMPTS {
559 let cpos = world
560 .sim()
561 .map_size_lg()
562 .chunks()
563 .map(|s| rng.gen_range(0..s as i32));
564
565 if let Some(chunk) = world.sim().get(cpos)
568 && chunk_filter(chunk)
569 {
570 let wpos = cpos.cpos_to_wpos_center();
571 let wpos = wpos.as_().with_z(world.sim().get_surface_alt_approx(wpos));
572
573 data.spawn_npc(
574 Npc::new(rng.gen(), wpos, body, death.role.clone())
575 .with_personality(personality),
576 );
577 return true;
578 }
579 }
580
581 false
582 },
583 Role::Vehicle => {
584 unimplemented!()
586 },
587 }
588 };
589
590 if !did_spawn && death.time.0 + MIN_SPAWN_DELAY * 5.0 < data.time_of_day.0 {
592 match death.role {
593 Role::Civilised(profession) => {
594 if !spawn_profession(
595 data,
596 world,
597 index,
598 death,
599 &mut rng,
600 body,
601 personality,
602 profession,
603 ) {
604 spawn_anywhere(data, world, death, &mut rng, body, personality)
605 }
606 },
607 Role::Wild | Role::Monster => {
608 spawn_anywhere(data, world, death, &mut rng, body, personality)
609 },
610 Role::Vehicle => {
611 unimplemented!()
613 },
614 }
615
616 true
617 } else {
618 did_spawn
619 }
620}