veloren_rtsim/rule/
architect.rs

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
25/// How many ticks the architect skips.
26///
27/// We don't need to run it every tick.
28const ARCHITECT_TICK_SKIP: u64 = 32;
29/// Min spawn delay, in ingame time.
30const MIN_SPAWN_DELAY: f64 = 60.0 * 60.0 * 24.0;
31/// For monsters that respawn in chunks, how many chunks should we try each
32/// respawn.
33const 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    // @perf: Could reuse previous allocation here.
225    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            // If we have more than enough of this npc, we skip spawning a new one.
235            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        // TODO: If we had access to `ChunkStates` here we could make sure
306        // these aren't getting respawned in loaded chunks.
307        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                    // Don't spawn multiple captains at the same site.
395                    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    // First try and respawn in the same faction.
431    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                    // TODO: If we had access to `ChunkStates` here we could make sure
566                    // these aren't getting respawned in loaded chunks.
567                    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                // Vehicles don't die as of now.
585                unimplemented!()
586            },
587        }
588    };
589
590    // If enough time has passed, try spawning anyway.
591    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                // Vehicles don't die as of now.
612                unimplemented!()
613            },
614        }
615
616        true
617    } else {
618        did_spawn
619    }
620}