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                    .map(|s| {
177                        index.sites.get(s).any_plot(|p| {
178                            matches!(
179                                p.kind().meta(),
180                                Some(world::site::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 => |site| matches!(site, SiteKind::Haniwa),
250                        comp::bird_large::Species::FlameWyvern => {
251                            |site| matches!(site, SiteKind::Terracotta)
252                        },
253                        comp::bird_large::Species::CloudWyvern => {
254                            |site| matches!(site, SiteKind::Sahagin)
255                        },
256                        comp::bird_large::Species::FrostWyvern => {
257                            |site| matches!(site, SiteKind::Adlet)
258                        },
259                        comp::bird_large::Species::SeaWyvern => {
260                            |site| matches!(site, SiteKind::ChapelSite)
261                        },
262                        comp::bird_large::Species::WealdWyvern => {
263                            |site| matches!(site, SiteKind::GiantTree)
264                        },
265                    },
266                    _ => |_| true,
267                };
268
269                if let Some((id, site)) = data
270                    .sites
271                    .iter()
272                    .filter(|(_, site)| {
273                        !site.is_loaded()
274                            && site
275                                .world_site
276                                .and_then(|s| index.sites.get(s).kind)
277                                .is_some_and(|s| site_filter(&s))
278                    })
279                    .choose(&mut rng)
280                {
281                    let wpos = site.wpos;
282                    let wpos = wpos
283                        .as_()
284                        .with_z(world.sim().get_alt_approx(wpos).unwrap_or(0.0));
285                    data.spawn_npc(
286                        Npc::new(rng.gen(), wpos, body, death.role.clone())
287                            .with_personality(personality)
288                            .with_home(id),
289                    );
290                    true
291                } else {
292                    false
293                }
294            },
295            Role::Monster => {
296                let chunk_filter: fn(&SimChunk) -> bool = match body {
297                    Body::BipedLarge(body) => match body.species {
298                        comp::biped_large::Species::Tursus
299                        | comp::biped_large::Species::Gigasfrost
300                        | comp::biped_large::Species::Wendigo => {
301                            |chunk| !chunk.is_underwater() && chunk.temp < CONFIG.snow_temp
302                        },
303                        comp::biped_large::Species::Mountaintroll => {
304                            |chunk| !chunk.is_underwater() && chunk.alt > 500.0
305                        },
306                        comp::biped_large::Species::Swamptroll => {
307                            |chunk| !chunk.is_underwater() && chunk.humidity > CONFIG.jungle_hum
308                        },
309                        _ => |chunk| !chunk.is_underwater(),
310                    },
311                    Body::Arthropod(_)
312                    | Body::Humanoid(_)
313                    | Body::QuadrupedSmall(_)
314                    | Body::BipedSmall(_)
315                    | Body::QuadrupedMedium(_)
316                    | Body::Golem(_)
317                    | Body::Theropod(_)
318                    | Body::QuadrupedLow(_) => |chunk| !chunk.is_underwater(),
319                    Body::Dragon(_) | Body::BirdLarge(_) | Body::BirdMedium(_) => |_| true,
320                    Body::Crustacean(_) | Body::FishSmall(_) | Body::FishMedium(_) => {
321                        |chunk| chunk.is_underwater()
322                    },
323                    Body::Object(_) | Body::Ship(_) | Body::Item(_) | Body::Plugin(_) => |_| true,
324                };
325
326                for _ in 0..RESPAWN_ATTEMPTS {
327                    let cpos = world
328                        .sim()
329                        .map_size_lg()
330                        .chunks()
331                        .map(|s| rng.gen_range(0..s as i32));
332
333                    // TODO: If we had access to `ChunkStates` here we could make sure
334                    // these aren't getting respawned in loaded chunks.
335                    if let Some(chunk) = world.sim().get(cpos)
336                        && chunk_filter(chunk)
337                    {
338                        let wpos = cpos.cpos_to_wpos_center();
339                        let wpos = wpos.as_().with_z(world.sim().get_surface_alt_approx(wpos));
340
341                        data.spawn_npc(
342                            Npc::new(rng.gen(), wpos, body, death.role.clone())
343                                .with_personality(personality),
344                        );
345                        return true;
346                    }
347                }
348
349                false
350            },
351            Role::Vehicle => {
352                // Vehicles don't die as of now.
353                unimplemented!()
354            },
355        }
356    };
357
358    // If enough time has passed, try spawning anyway.
359    if !did_spawn && death.time.0 + MIN_SPAWN_DELAY * 5.0 < data.time_of_day.0 {
360        match death.role {
361            Role::Civilised(_) => {
362                if !spawn_any_settlement(data, world, index, death, &mut rng, body, personality) {
363                    spawn_anywhere(data, world, death, &mut rng, body, personality)
364                }
365            },
366            Role::Wild | Role::Monster => {
367                spawn_anywhere(data, world, death, &mut rng, body, personality)
368            },
369            Role::Vehicle => {
370                // Vehicles don't die as of now.
371                unimplemented!()
372            },
373        }
374
375        true
376    } else {
377        did_spawn
378    }
379}