veloren_rtsim/gen/
mod.rs

1pub mod faction;
2pub mod name;
3pub mod site;
4
5use crate::data::{
6    CURRENT_VERSION, Data, Nature,
7    airship::AirshipSpawningLocation,
8    faction::Faction,
9    npc::{Npc, Npcs, Profession},
10    site::Site,
11};
12use common::{
13    comp::{self, Body},
14    resources::TimeOfDay,
15    rtsim::{NpcId, Personality, Role, WorldSettings},
16    terrain::{BiomeKind, CoordinateConversions, TerrainChunkSize},
17    vol::RectVolSize,
18};
19use rand::prelude::*;
20use rand_chacha::ChaChaRng;
21use tracing::info;
22use vek::*;
23use world::{
24    CONFIG, IndexRef, World,
25    civ::airship_travel::{AirshipDockingSide, Airships},
26    site::{PlotKind, SiteKind, plot::PlotKindMeta},
27    util::seed_expan,
28};
29
30impl Data {
31    pub fn generate(settings: &WorldSettings, world: &World, index: IndexRef) -> Self {
32        let mut seed = [0; 32];
33        seed.iter_mut()
34            .zip(&mut index.seed.to_le_bytes())
35            .for_each(|(dst, src)| *dst = *src);
36        let mut rng = SmallRng::from_seed(seed);
37
38        let mut this = Self {
39            version: CURRENT_VERSION,
40            nature: Nature::generate(world),
41            npcs: Npcs::default(),
42            sites: Default::default(),
43            factions: Default::default(),
44            reports: Default::default(),
45            airship_sim: Default::default(),
46            architect: Default::default(),
47
48            tick: 0,
49            time_of_day: TimeOfDay(settings.start_time),
50            should_purge: false,
51        };
52
53        let initial_factions = (0..16)
54            .map(|_| {
55                let faction = Faction::generate(world, index, &mut rng);
56                let wpos = world
57                    .sim()
58                    .get_size()
59                    .map2(TerrainChunkSize::RECT_SIZE, |e, sz| {
60                        rng.gen_range(0..(e * sz) as i32)
61                    });
62                (wpos, this.factions.create(faction))
63            })
64            .collect::<Vec<_>>();
65        info!("Generated {} rtsim factions.", this.factions.len());
66
67        // Register sites with rtsim
68        for (world_site_id, _) in index.sites.iter() {
69            let site = Site::generate(
70                world_site_id,
71                world,
72                index,
73                &initial_factions,
74                &this.factions,
75                &mut rng,
76            );
77            this.sites.create(site);
78        }
79        info!(
80            "Registering {} rtsim sites from world sites.",
81            this.sites.len()
82        );
83
84        let random_humanoid = |rng: &mut SmallRng| {
85            let species = comp::humanoid::ALL_SPECIES.choose(&mut *rng).unwrap();
86            Body::Humanoid(comp::humanoid::Body::random_with(rng, species))
87        };
88
89        // Spawn some test entities at the sites
90        for (site_id, site, world_site) in this.sites.iter()
91        // TODO: Stupid. Only find site towns
92        .filter_map(|(site_id, site)| Some((site_id, site, site.world_site
93            .map(|ws| index.sites.get(ws)).filter(|site| site.meta().is_some_and(|m| matches!(m, common::terrain::SiteKindMeta::Settlement(_))))?)))
94        {
95            let Some(good_or_evil) = site
96                .faction
97                .and_then(|f| this.factions.get(f))
98                .map(|f| f.good_or_evil)
99            else {
100                continue;
101            };
102
103            let rand_wpos = |rng: &mut SmallRng, matches_plot: fn(&PlotKind) -> bool| {
104                let wpos2d = world_site
105                    .plots()
106                    .filter(|plot| matches_plot(plot.kind()))
107                    .choose(&mut thread_rng())
108                    .map(|plot| world_site.tile_center_wpos(plot.root_tile()))
109                    .unwrap_or_else(|| site.wpos.map(|e| e + rng.gen_range(-10..10)));
110                wpos2d
111                    .map(|e| e as f32 + 0.5)
112                    .with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
113            };
114            let matches_buildings = (|kind: &PlotKind| {
115                matches!(
116                    kind,
117                    PlotKind::House(_)
118                        | PlotKind::Workshop(_)
119                        | PlotKind::AirshipDock(_)
120                        | PlotKind::Tavern(_)
121                        | PlotKind::Plaza(_)
122                        | PlotKind::SavannahAirshipDock(_)
123                        | PlotKind::SavannahHut(_)
124                        | PlotKind::SavannahWorkshop(_)
125                        | PlotKind::CliffTower(_)
126                        | PlotKind::DesertCityMultiPlot(_)
127                        | PlotKind::DesertCityTemple(_)
128                        | PlotKind::CoastalHouse(_)
129                        | PlotKind::CoastalWorkshop(_)
130                )
131            }) as _;
132            let matches_plazas = (|kind: &PlotKind| matches!(kind, PlotKind::Plaza(_))) as _;
133            if good_or_evil {
134                for _ in 0..world_site.plots().len() {
135                    this.npcs.create_npc(
136                        Npc::new(
137                            rng.gen(),
138                            rand_wpos(&mut rng, matches_buildings),
139                            random_humanoid(&mut rng),
140                            Role::Civilised(Some(match rng.gen_range(0..20) {
141                                0 => Profession::Hunter,
142                                1 => Profession::Blacksmith,
143                                2 => Profession::Chef,
144                                3 => Profession::Alchemist,
145                                5..=8 => Profession::Farmer,
146                                9..=10 => Profession::Herbalist,
147                                11..=16 => Profession::Guard,
148                                _ => Profession::Adventurer(rng.gen_range(0..=3)),
149                            })),
150                        )
151                        .with_faction(site.faction)
152                        .with_home(site_id)
153                        .with_personality(Personality::random(&mut rng)),
154                    );
155                }
156            } else {
157                for _ in 0..15 {
158                    this.npcs.create_npc(
159                        Npc::new(
160                            rng.gen(),
161                            rand_wpos(&mut rng, matches_buildings),
162                            random_humanoid(&mut rng),
163                            Role::Civilised(Some(Profession::Cultist)),
164                        )
165                        .with_personality(Personality::random_evil(&mut rng))
166                        .with_faction(site.faction)
167                        .with_home(site_id),
168                    );
169                }
170            }
171            // Merchants
172            if good_or_evil {
173                for _ in 0..(world_site.plots().len() / 6) + 1 {
174                    this.npcs.create_npc(
175                        Npc::new(
176                            rng.gen(),
177                            rand_wpos(&mut rng, matches_plazas),
178                            random_humanoid(&mut rng),
179                            Role::Civilised(Some(Profession::Merchant)),
180                        )
181                        .with_home(site_id)
182                        .with_personality(Personality::random_good(&mut rng)),
183                    );
184                }
185            }
186        }
187
188        // Airships
189        // Get the spawning locations for the sites with airship docks. It's possible
190        // that not all docking positions will be used at all sites based on
191        // pairing with routes and how the routes are generated.
192        let spawning_locations = this.airship_spawning_locations(world, index);
193
194        // When generating rtsim data from scratch, put an airship (and captain) at each
195        // available spawning location. Note this is just to get the initial
196        // airship NPCs created. Since the airship route data is not persisted,
197        // but the NPCs themselves are, the rtsim data contains airships and captains,
198        // but not the routes, and the information about routes and route
199        // assignments is generated each time the server is started. This process
200        // of resolving the rtsim data to the world data is done in the `migrate`
201        // module.
202
203        let mut airship_rng = ChaChaRng::from_seed(seed_expan::rng_state(index.index.seed));
204        for spawning_location in spawning_locations.iter() {
205            this.spawn_airship(spawning_location, &mut airship_rng);
206        }
207
208        // Birds
209        for (site_id, site) in this.sites.iter() {
210            let rand_wpos = |rng: &mut SmallRng| {
211                // don't spawn in buildings
212                let spread_factor = rng.gen_range(-3..3) * 50;
213                let spread = if spread_factor == 0 {
214                    100
215                } else {
216                    spread_factor
217                };
218                let wpos2d = site.wpos.map(|e| e + spread);
219                wpos2d
220                    .map(|e| e as f32 + 0.5)
221                    .with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
222            };
223            let site_kind = site.world_site.and_then(|ws| index.sites.get(ws).kind);
224            let Some(species) = [
225                Some(comp::body::bird_large::Species::Phoenix)
226                    .filter(|_| matches!(site_kind, Some(SiteKind::DwarvenMine))),
227                Some(comp::body::bird_large::Species::Cockatrice)
228                    .filter(|_| matches!(site_kind, Some(SiteKind::Myrmidon))),
229                Some(comp::body::bird_large::Species::Roc)
230                    .filter(|_| matches!(site_kind, Some(SiteKind::Haniwa))),
231                Some(comp::body::bird_large::Species::FlameWyvern)
232                    .filter(|_| matches!(site_kind, Some(SiteKind::Terracotta))),
233                Some(comp::body::bird_large::Species::CloudWyvern)
234                    .filter(|_| matches!(site_kind, Some(SiteKind::Sahagin))),
235                Some(comp::body::bird_large::Species::FrostWyvern)
236                    .filter(|_| matches!(site_kind, Some(SiteKind::Adlet))),
237                Some(comp::body::bird_large::Species::SeaWyvern)
238                    .filter(|_| matches!(site_kind, Some(SiteKind::ChapelSite))),
239                Some(comp::body::bird_large::Species::WealdWyvern)
240                    .filter(|_| matches!(site_kind, Some(SiteKind::GiantTree))),
241            ]
242            .into_iter()
243            .flatten()
244            .choose(&mut rng) else {
245                continue;
246            };
247
248            this.npcs.create_npc(
249                Npc::new(
250                    rng.gen(),
251                    rand_wpos(&mut rng),
252                    Body::BirdLarge(comp::body::bird_large::Body::random_with(
253                        &mut rng, &species,
254                    )),
255                    Role::Wild,
256                )
257                .with_home(site_id),
258            );
259        }
260
261        // Spawn monsters into the world
262        for _ in 0..(world.sim().map_size_lg().chunks_len() / 2usize.pow(13)).clamp(5, 1000) {
263            // Try a few times to find a location that's not underwater
264            if let Some((wpos, chunk)) = (0..10)
265                .map(|_| world.sim().get_size().map(|sz| rng.gen_range(0..sz as i32)))
266                .find_map(|pos| Some((pos, world.sim().get(pos).filter(|c| !c.is_underwater())?)))
267                .map(|(pos, chunk)| {
268                    let wpos2d = pos.cpos_to_wpos_center();
269                    (
270                        wpos2d
271                            .map(|e| e as f32 + 0.5)
272                            .with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)),
273                        chunk,
274                    )
275                })
276            {
277                let biome = chunk.get_biome();
278                let Some(species) = [
279                    Some(comp::body::biped_large::Species::Ogre),
280                    Some(comp::body::biped_large::Species::Cyclops),
281                    Some(comp::body::biped_large::Species::Wendigo)
282                        .filter(|_| biome == BiomeKind::Taiga),
283                    Some(comp::body::biped_large::Species::Cavetroll),
284                    Some(comp::body::biped_large::Species::Mountaintroll)
285                        .filter(|_| biome == BiomeKind::Mountain),
286                    Some(comp::body::biped_large::Species::Swamptroll)
287                        .filter(|_| biome == BiomeKind::Swamp),
288                    Some(comp::body::biped_large::Species::Blueoni),
289                    Some(comp::body::biped_large::Species::Redoni),
290                    Some(comp::body::biped_large::Species::Tursus)
291                        .filter(|_| chunk.temp < CONFIG.snow_temp),
292                ]
293                .into_iter()
294                .flatten()
295                .choose(&mut rng) else {
296                    continue;
297                };
298
299                this.npcs.create_npc(Npc::new(
300                    rng.gen(),
301                    wpos,
302                    Body::BipedLarge(comp::body::biped_large::Body::random_with(
303                        &mut rng, &species,
304                    )),
305                    Role::Monster,
306                ));
307            }
308        }
309        // Spawn one monster Gigasfrost into the world
310        // Try a few times to find a location that's not underwater
311        if let Some((wpos, _)) = (0..100)
312            .map(|_| world.sim().get_size().map(|sz| rng.gen_range(0..sz as i32)))
313            .find_map(|pos| Some((pos, world.sim().get(pos).filter(|c| !c.is_underwater())?)))
314            .map(|(pos, chunk)| {
315                let wpos2d = pos.cpos_to_wpos_center();
316                (
317                    wpos2d
318                        .map(|e| e as f32 + 0.5)
319                        .with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)),
320                    chunk,
321                )
322            })
323        {
324            let species = comp::body::biped_large::Species::Gigasfrost;
325
326            this.npcs.create_npc(Npc::new(
327                rng.gen(),
328                wpos,
329                Body::BipedLarge(comp::body::biped_large::Body::random_with(
330                    &mut rng, &species,
331                )),
332                Role::Monster,
333            ));
334        }
335
336        info!("Generated {} rtsim NPCs.", this.npcs.len());
337
338        this
339    }
340
341    /// Get all the places that an airship should be spawned. The site must be a
342    /// town or city that could have one or more airship docks. The plot
343    /// type must be an airship dock, and the docking position must be one
344    /// that the airship can spawn at according to the world airship routes.
345    pub fn airship_spawning_locations(
346        &self,
347        world: &World,
348        index: IndexRef,
349    ) -> Vec<AirshipSpawningLocation> {
350        self.sites
351            .iter()
352            .filter_map(|(site_id, site)| {
353                Some((
354                    site_id,
355                    site,
356                    site.world_site.map(|ws| index.sites.get(ws))?,
357                ))
358            })
359            .flat_map(|(site_id, _, site)| {
360                site.plots
361                    .values()
362                    .filter_map(move |plot| {
363                        if let Some(PlotKindMeta::AirshipDock {
364                            center,
365                            docking_positions,
366                            ..
367                        }) = plot.kind().meta()
368                        {
369                            Some(
370                                docking_positions
371                                    .iter()
372                                    .filter_map(move |docking_pos| {
373                                        if world
374                                            .civs()
375                                            .airships
376                                            .should_spawn_airship_at_docking_position(
377                                                docking_pos,
378                                                site.name(),
379                                            )
380                                        {
381                                            let (airship_pos, airship_dir) =
382                                                Airships::airship_vec_for_docking_pos(
383                                                    docking_pos.map(|i| i as f32),
384                                                    center.map(|i| i as f32),
385                                                    // This is a temporary choice just to make the
386                                                    // spawning location data deterministic.
387                                                    // The actual docking side is selected when the
388                                                    // route and approach are selected in the
389                                                    // migrate module.
390                                                    Some(AirshipDockingSide::Starboard),
391                                                );
392                                            Some(AirshipSpawningLocation {
393                                                pos: airship_pos,
394                                                dir: airship_dir,
395                                                center,
396                                                docking_pos: *docking_pos,
397                                                site_id,
398                                                site_name: site.name().to_string(),
399                                            })
400                                        } else {
401                                            None
402                                        }
403                                    })
404                                    .collect::<Vec<_>>(),
405                            )
406                        } else {
407                            None
408                        }
409                    })
410                    .flatten()
411            })
412            .collect::<Vec<_>>()
413    }
414
415    /// Creates an airship and captain NPC at the given spawning location. The
416    /// location is tempory since the airship will be moved into position
417    /// after the npcs are spawned.
418    pub fn spawn_airship(
419        &mut self,
420        spawning_location: &AirshipSpawningLocation,
421        rng: &mut impl Rng,
422    ) -> (NpcId, NpcId) {
423        let vehicle_id = self.npcs.create_npc(Npc::new(
424            rng.gen(),
425            spawning_location.pos,
426            Body::Ship(comp::body::ship::Body::DefaultAirship),
427            Role::Vehicle,
428        ));
429        let airship = self.npcs.get_mut(vehicle_id).unwrap();
430        let airship_mount_offset = airship.body.mount_offset();
431
432        let captain_pos = spawning_location.pos
433            + Vec3::new(
434                spawning_location.dir.x * airship_mount_offset.x,
435                spawning_location.dir.y * airship_mount_offset.y,
436                airship_mount_offset.z,
437            );
438        let species = comp::humanoid::ALL_SPECIES.choose(&mut *rng).unwrap();
439        let npc_id = self.npcs.create_npc(
440            Npc::new(
441                rng.gen(),
442                captain_pos,
443                Body::Humanoid(comp::humanoid::Body::random_with(rng, species)),
444                Role::Civilised(Some(Profession::Captain)),
445            )
446            // .with_home(spawning_location.site_id)
447            .with_personality(Personality::random_good(rng)),
448        );
449        // airship_captains.push((spawning_location.pos, npc_id, vehicle_id));
450        self.npcs.get_mut(npc_id).unwrap().dir = spawning_location.dir.xy().normalized();
451
452        // The captain is mounted on the airship
453        self.npcs
454            .mounts
455            .steer(vehicle_id, npc_id)
456            .expect("We just created these npcs!");
457
458        self.npcs.get_mut(vehicle_id).unwrap().dir = spawning_location.dir.xy().normalized();
459
460        (npc_id, vehicle_id)
461    }
462}