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