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 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 for (site_id, site, world_site) in this.sites.iter()
91 .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 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 let spawning_locations = this.airship_spawning_locations(world, index);
193
194 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 for (site_id, site) in this.sites.iter() {
210 let rand_wpos = |rng: &mut SmallRng| {
211 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 for _ in 0..(world.sim().map_size_lg().chunks_len() / 2usize.pow(13)).clamp(5, 1000) {
263 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 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 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 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 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_personality(Personality::random_good(rng)),
448 );
449 self.npcs.get_mut(npc_id).unwrap().dir = spawning_location.dir.xy().normalized();
451
452 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}