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