1use common::{
2 comp::{self, Body},
3 rtsim::{Actor, Personality, Profession, Role},
4 terrain::CoordinateConversions,
5};
6use rand::{
7 Rng,
8 seq::{IteratorRandom, SliceRandom},
9 thread_rng,
10};
11use world::{CONFIG, IndexRef, World, sim::SimChunk, site::SiteKind};
12
13use crate::{
14 Data, EventCtx, OnTick, RtState,
15 data::{Npc, architect::Death},
16 event::OnDeath,
17};
18
19use super::{Rule, RuleError};
20
21const ARCHITECT_TICK_SKIP: u64 = 32;
25const MIN_SPAWN_DELAY: f64 = 60.0 * 60.0 * 24.0;
27const RESPAWN_ATTEMPTS: usize = 30;
30
31pub struct Architect;
32
33impl Rule for Architect {
34 fn start(rtstate: &mut RtState) -> Result<Self, RuleError> {
35 rtstate.bind(on_death);
36 rtstate.bind(architect_tick);
37
38 Ok(Self)
39 }
40}
41
42fn on_death(ctx: EventCtx<Architect, OnDeath>) {
43 let data = &mut *ctx.state.data_mut();
44
45 if let Actor::Npc(npc_id) = ctx.event.actor
46 && let Some(npc) = data.npcs.get(npc_id)
47 {
48 data.architect.on_death(npc, data.time_of_day);
49 }
50}
51
52fn architect_tick(ctx: EventCtx<Architect, OnTick>) {
53 if ctx.event.tick % ARCHITECT_TICK_SKIP != 0 {
54 return;
55 }
56
57 let tod = ctx.event.time_of_day;
58
59 let data = &mut *ctx.state.data_mut();
60
61 let mut count_to_spawn = thread_rng().gen_range(1..20);
62
63 let mut failed_spawn = Vec::new();
65
66 while count_to_spawn > 0
67 && let Some(death) = data.architect.deaths.pop_front()
68 {
69 if death.time.0 + MIN_SPAWN_DELAY > tod.0 {
70 data.architect.deaths.push_front(death);
71 break;
72 }
73
74 if spawn_npc(data, ctx.world, ctx.index, &death) {
75 count_to_spawn -= 1;
76 } else {
77 failed_spawn.push(death);
78 }
79 }
80
81 for death in failed_spawn.into_iter().rev() {
82 data.architect.deaths.push_front(death);
83 }
84}
85
86fn randomize_body(body: Body, rng: &mut impl Rng) -> Body {
87 let mut random_humanoid = || {
88 let species = comp::humanoid::ALL_SPECIES.choose(rng).unwrap();
89 Body::Humanoid(comp::humanoid::Body::random_with(rng, species))
90 };
91 match body {
92 Body::Humanoid(_) => random_humanoid(),
93 body => body,
94 }
95}
96
97fn role_personality(rng: &mut impl Rng, role: &Role) -> Personality {
98 match role {
99 Role::Civilised(profession) => match profession {
100 Some(Profession::Guard | Profession::Merchant | Profession::Captain) => {
101 Personality::random_good(rng)
102 },
103 Some(Profession::Cultist | Profession::Pirate) => Personality::random_evil(rng),
104 None
105 | Some(
106 Profession::Farmer
107 | Profession::Chef
108 | Profession::Hunter
109 | Profession::Blacksmith
110 | Profession::Alchemist
111 | Profession::Herbalist
112 | Profession::Adventurer(_),
113 ) => Personality::random(rng),
114 },
115 Role::Wild => Personality::random(rng),
116 Role::Monster => Personality::random_evil(rng),
117 Role::Vehicle => Personality::default(),
118 }
119}
120
121fn spawn_anywhere(
122 data: &mut Data,
123 world: &World,
124 death: &Death,
125 rng: &mut impl Rng,
126 body: Body,
127 personality: Personality,
128) {
129 let mut attempt = |check: bool| {
130 let cpos = world
131 .sim()
132 .map_size_lg()
133 .chunks()
134 .map(|s| rng.gen_range(0..s as i32));
135
136 if let Some(chunk) = world.sim().get(cpos)
139 && (!check || !chunk.is_underwater())
140 {
141 let wpos = cpos.cpos_to_wpos_center();
142 let wpos = wpos.as_().with_z(world.sim().get_surface_alt_approx(wpos));
143
144 data.spawn_npc(
145 Npc::new(rng.gen(), wpos, body, death.role.clone()).with_personality(personality),
146 );
147 return true;
148 }
149
150 false
151 };
152 for _ in 0..RESPAWN_ATTEMPTS {
153 if attempt(true) {
154 return;
155 }
156 }
157 attempt(false);
158}
159
160fn spawn_any_settlement(
161 data: &mut Data,
162 world: &World,
163 index: IndexRef,
164 death: &Death,
165 rng: &mut impl Rng,
166 body: Body,
167 personality: Personality,
168) -> bool {
169 if let Some((id, site)) = data
170 .sites
171 .iter()
172 .filter(|(_, site)| {
173 !site.is_loaded()
174 && site
175 .world_site
176 .and_then(|s| {
177 Some(index.sites.get(s).site2()?.plots().any(|p| {
178 matches!(
179 p.kind().meta(),
180 Some(world::site2::plot::PlotKindMeta::House { .. })
181 )
182 }))
183 })
184 .unwrap_or(false)
185 })
186 .choose(rng)
187 {
188 let wpos = site.wpos;
189 let wpos = wpos
190 .as_()
191 .with_z(world.sim().get_alt_approx(wpos).unwrap_or(0.0));
192 let mut npc = Npc::new(rng.gen(), wpos, body, death.role.clone())
193 .with_personality(personality)
194 .with_home(id);
195 if let Some(faction) = site.faction {
196 npc = npc.with_faction(faction);
197 }
198 data.spawn_npc(npc);
199
200 true
201 } else {
202 false
203 }
204}
205
206fn spawn_npc(data: &mut Data, world: &World, index: IndexRef, death: &Death) -> bool {
207 let mut rng = thread_rng();
208 let body = randomize_body(death.body, &mut rng);
209 let personality = role_personality(&mut rng, &death.role);
210 let did_spawn = if let Some(faction_id) = death.faction
212 && data.factions.get(faction_id).is_some()
213 {
214 if let Some((id, site)) = data
215 .sites
216 .iter()
217 .filter(|(_, site)| site.faction == Some(faction_id) && !site.is_loaded())
218 .choose(&mut rng)
219 {
220 let wpos = site.wpos;
221 let wpos = wpos
222 .as_()
223 .with_z(world.sim().get_alt_approx(wpos).unwrap_or(0.0));
224 data.spawn_npc(
225 Npc::new(rng.gen(), wpos, body, death.role.clone())
226 .with_personality(personality)
227 .with_home(id)
228 .with_faction(faction_id),
229 );
230
231 true
232 } else {
233 false
234 }
235 } else {
236 match &death.role {
237 Role::Civilised(_) => {
238 spawn_any_settlement(data, world, index, death, &mut rng, body, personality)
239 },
240 Role::Wild => {
241 let site_filter: fn(&SiteKind) -> bool = match body {
242 Body::BirdLarge(body) => match body.species {
243 comp::bird_large::Species::Phoenix => {
244 |site| matches!(site, SiteKind::DwarvenMine(_))
245 },
246 comp::bird_large::Species::Cockatrice => {
247 |site| matches!(site, SiteKind::Myrmidon(_))
248 },
249 comp::bird_large::Species::Roc => {
250 |site| matches!(site, SiteKind::Haniwa(_))
251 },
252 comp::bird_large::Species::FlameWyvern => {
253 |site| matches!(site, SiteKind::Terracotta(_))
254 },
255 comp::bird_large::Species::CloudWyvern => {
256 |site| matches!(site, SiteKind::Sahagin(_))
257 },
258 comp::bird_large::Species::FrostWyvern => {
259 |site| matches!(site, SiteKind::Adlet(_))
260 },
261 comp::bird_large::Species::SeaWyvern => {
262 |site| matches!(site, SiteKind::ChapelSite(_))
263 },
264 comp::bird_large::Species::WealdWyvern => {
265 |site| matches!(site, SiteKind::GiantTree(_))
266 },
267 },
268 _ => |_| true,
269 };
270
271 if let Some((id, site)) = data
272 .sites
273 .iter()
274 .filter(|(_, site)| {
275 !site.is_loaded()
276 && site
277 .world_site
278 .is_some_and(|s| site_filter(&index.sites.get(s).kind))
279 })
280 .choose(&mut rng)
281 {
282 let wpos = site.wpos;
283 let wpos = wpos
284 .as_()
285 .with_z(world.sim().get_alt_approx(wpos).unwrap_or(0.0));
286 data.spawn_npc(
287 Npc::new(rng.gen(), wpos, body, death.role.clone())
288 .with_personality(personality)
289 .with_home(id),
290 );
291 true
292 } else {
293 false
294 }
295 },
296 Role::Monster => {
297 let chunk_filter: fn(&SimChunk) -> bool = match body {
298 Body::BipedLarge(body) => match body.species {
299 comp::biped_large::Species::Tursus
300 | comp::biped_large::Species::Gigasfrost
301 | comp::biped_large::Species::Wendigo => {
302 |chunk| !chunk.is_underwater() && chunk.temp < CONFIG.snow_temp
303 },
304 comp::biped_large::Species::Mountaintroll => {
305 |chunk| !chunk.is_underwater() && chunk.alt > 500.0
306 },
307 comp::biped_large::Species::Swamptroll => {
308 |chunk| !chunk.is_underwater() && chunk.humidity > CONFIG.jungle_hum
309 },
310 _ => |chunk| !chunk.is_underwater(),
311 },
312 Body::Arthropod(_)
313 | Body::Humanoid(_)
314 | Body::QuadrupedSmall(_)
315 | Body::BipedSmall(_)
316 | Body::QuadrupedMedium(_)
317 | Body::Golem(_)
318 | Body::Theropod(_)
319 | Body::QuadrupedLow(_) => |chunk| !chunk.is_underwater(),
320 Body::Dragon(_) | Body::BirdLarge(_) | Body::BirdMedium(_) => |_| true,
321 Body::Crustacean(_) | Body::FishSmall(_) | Body::FishMedium(_) => {
322 |chunk| chunk.is_underwater()
323 },
324 Body::Object(_) | Body::Ship(_) | Body::Item(_) | Body::Plugin(_) => |_| true,
325 };
326
327 for _ in 0..RESPAWN_ATTEMPTS {
328 let cpos = world
329 .sim()
330 .map_size_lg()
331 .chunks()
332 .map(|s| rng.gen_range(0..s as i32));
333
334 if let Some(chunk) = world.sim().get(cpos)
337 && chunk_filter(chunk)
338 {
339 let wpos = cpos.cpos_to_wpos_center();
340 let wpos = wpos.as_().with_z(world.sim().get_surface_alt_approx(wpos));
341
342 data.spawn_npc(
343 Npc::new(rng.gen(), wpos, body, death.role.clone())
344 .with_personality(personality),
345 );
346 return true;
347 }
348 }
349
350 false
351 },
352 Role::Vehicle => {
353 unimplemented!()
355 },
356 }
357 };
358
359 if !did_spawn && death.time.0 + MIN_SPAWN_DELAY * 5.0 < data.time_of_day.0 {
361 match death.role {
362 Role::Civilised(_) => {
363 if !spawn_any_settlement(data, world, index, death, &mut rng, body, personality) {
364 spawn_anywhere(data, world, death, &mut rng, body, personality)
365 }
366 },
367 Role::Wild | Role::Monster => {
368 spawn_anywhere(data, world, death, &mut rng, body, personality)
369 },
370 Role::Vehicle => {
371 unimplemented!()
373 },
374 }
375
376 true
377 } else {
378 did_spawn
379 }
380}