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 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, site2) in this.sites.iter()
91 .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 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 let spawning_locations = this.airship_spawning_locations(world, index);
200
201 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 for (site_id, site) in this.sites.iter() {
217 let rand_wpos = |rng: &mut SmallRng| {
218 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 for _ in 0..(world.sim().map_size_lg().chunks_len() / 2usize.pow(13)).clamp(5, 1000) {
270 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 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 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 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 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_personality(Personality::random_good(rng)),
466 );
467 self.npcs.get_mut(npc_id).unwrap().dir = spawning_location.dir.xy().normalized();
469
470 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}