Skip to main content

veloren_rtsim/rule/
architect.rs

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
24/// How many ticks the architect skips.
25///
26/// We don't need to run it every tick.
27const ARCHITECT_TICK_SKIP: u64 = 32;
28/// Min spawn delay, in ingame time.
29const MIN_SPAWN_DELAY: f64 = 60.0 * 60.0 * 24.0;
30/// For monsters that respawn in chunks, how many chunks should we try each
31/// respawn.
32const 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    // @perf: Could reuse previous allocation here.
224    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            // If we have more than enough of this npc, we skip spawning a new one.
234            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        // TODO: If we had access to `ChunkStates` here we could make sure
305        // these aren't getting respawned in loaded chunks.
306        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                    // Don't spawn multiple captains at the same site.
395                    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    // First try and respawn in the same faction.
426    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                    // TODO: If we had access to `ChunkStates` here we could make sure
561                    // these aren't getting respawned in loaded chunks.
562                    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                // Vehicles don't die as of now.
580                unimplemented!()
581            },
582        }
583    };
584
585    // If enough time has passed, try spawning anyway.
586    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                // Vehicles don't die as of now.
607                unimplemented!()
608            },
609        }
610
611        true
612    } else {
613        did_spawn
614    }
615}