veloren_rtsim/rule/
architect.rs

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
21/// How many ticks the architect skips.
22///
23/// We don't need to run it every tick.
24const ARCHITECT_TICK_SKIP: u64 = 32;
25/// Min spawn delay, in ingame time.
26const MIN_SPAWN_DELAY: f64 = 60.0 * 60.0 * 24.0;
27/// For monsters that respawn in chunks, how many chunks should we try each
28/// respawn.
29const 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    // @perf: Could reuse previous allocation here.
64    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        // TODO: If we had access to `ChunkStates` here we could make sure
137        // these aren't getting respawned in loaded chunks.
138        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    // First try and respawn in the same faction.
211    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                    // TODO: If we had access to `ChunkStates` here we could make sure
335                    // these aren't getting respawned in loaded chunks.
336                    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                // Vehicles don't die as of now.
354                unimplemented!()
355            },
356        }
357    };
358
359    // If enough time has passed, try spawning anyway.
360    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                // Vehicles don't die as of now.
372                unimplemented!()
373            },
374        }
375
376        true
377    } else {
378        did_spawn
379    }
380}