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