1#![expect(dead_code)]
2
3pub mod airship_travel;
4mod econ;
5
6#[cfg(feature = "airship_maps")]
7pub mod airship_route_map;
8
9use crate::{
10 Index, IndexRef, Land,
11 civ::airship_travel::Airships,
12 config::CONFIG,
13 sim::WorldSim,
14 site::{self, Site as WorldSite, SiteKind, SitesGenMeta, namegen::NameGen},
15 util::{DHashMap, NEIGHBORS, attempt, seed_expan},
16};
17use common::{
18 astar::Astar,
19 calendar::Calendar,
20 path::Path,
21 spiral::Spiral2d,
22 store::{Id, Store},
23 terrain::{BiomeKind, CoordinateConversions, MapSizeLg, TerrainChunkSize, uniform_idx_as_vec2},
24 vol::RectVolSize,
25};
26use common_base::prof_span;
27use core::{fmt, hash::BuildHasherDefault, ops::Range};
28use fxhash::FxHasher64;
29use rand::prelude::*;
30use rand_chacha::ChaChaRng;
31use tracing::{debug, info, warn};
32use vek::*;
33
34fn initial_civ_count(map_size_lg: MapSizeLg) -> u32 {
35 let cnt = (3 << (map_size_lg.vec().x + map_size_lg.vec().y)) >> 16;
40 cnt.max(1) }
42
43#[derive(Default)]
44pub struct Civs {
45 pub civs: Store<Civ>,
46 pub places: Store<Place>,
47 pub pois: Store<PointOfInterest>,
48
49 pub tracks: Store<Track>,
50 pub track_map: DHashMap<Id<Site>, DHashMap<Id<Site>, Id<Track>>>,
55
56 pub bridges: DHashMap<Vec2<i32>, (Vec2<i32>, Id<Site>)>,
57
58 pub sites: Store<Site>,
59 pub airships: Airships,
60}
61
62const SEED_SKIP: u8 = 5;
64const POI_THINNING_DIST_SQRD: i32 = 300;
65
66pub struct GenCtx<'a, R: Rng> {
67 sim: &'a mut WorldSim,
68 rng: R,
69}
70
71struct ProximitySpec {
72 location: Vec2<i32>,
73 min_distance: Option<i32>,
74 max_distance: Option<i32>,
75}
76
77impl ProximitySpec {
78 pub fn satisfied_by(&self, site: Vec2<i32>) -> bool {
79 let distance_squared = site.distance_squared(self.location);
80 let min_ok = self
81 .min_distance
82 .map(|mind| distance_squared > (mind * mind))
83 .unwrap_or(true);
84 let max_ok = self
85 .max_distance
86 .map(|maxd| distance_squared < (maxd * maxd))
87 .unwrap_or(true);
88 min_ok && max_ok
89 }
90
91 pub fn avoid(location: Vec2<i32>, min_distance: i32) -> Self {
92 ProximitySpec {
93 location,
94 min_distance: Some(min_distance),
95 max_distance: None,
96 }
97 }
98
99 pub fn be_near(location: Vec2<i32>, max_distance: i32) -> Self {
100 ProximitySpec {
101 location,
102 min_distance: None,
103 max_distance: Some(max_distance),
104 }
105 }
106}
107
108struct ProximityRequirementsBuilder {
109 all_of: Vec<ProximitySpec>,
110 any_of: Vec<ProximitySpec>,
111}
112
113impl ProximityRequirementsBuilder {
114 pub fn finalize(self, world_dims: &Aabr<i32>) -> ProximityRequirements {
115 let location_hint = self.location_hint(world_dims);
116 ProximityRequirements {
117 all_of: self.all_of,
118 any_of: self.any_of,
119 location_hint,
120 }
121 }
122
123 fn location_hint(&self, world_dims: &Aabr<i32>) -> Aabr<i32> {
124 let bounding_box_of_point = |point: Vec2<i32>, max_distance: i32| Aabr {
125 min: Vec2 {
126 x: point.x - max_distance,
127 y: point.y - max_distance,
128 },
129 max: Vec2 {
130 x: point.x + max_distance,
131 y: point.y + max_distance,
132 },
133 };
134 let any_of_hint = self
135 .any_of
136 .iter()
137 .fold(None, |acc, spec| match spec.max_distance {
138 None => acc,
139 Some(max_distance) => {
140 let bounding_box_of_new_point =
141 bounding_box_of_point(spec.location, max_distance);
142 match acc {
143 None => Some(bounding_box_of_new_point),
144 Some(acc) => Some(acc.union(bounding_box_of_new_point)),
145 }
146 },
147 })
148 .map(|hint| hint.intersection(*world_dims))
149 .unwrap_or_else(|| world_dims.to_owned());
150 let hint = self
151 .all_of
152 .iter()
153 .fold(any_of_hint, |acc, spec| match spec.max_distance {
154 None => acc,
155 Some(max_distance) => {
156 let bounding_box_of_new_point =
157 bounding_box_of_point(spec.location, max_distance);
158 acc.intersection(bounding_box_of_new_point)
159 },
160 });
161 hint
162 }
163
164 pub fn new() -> Self {
165 Self {
166 all_of: Vec::new(),
167 any_of: Vec::new(),
168 }
169 }
170
171 pub fn avoid_all_of(
172 mut self,
173 locations: impl Iterator<Item = Vec2<i32>>,
174 distance: i32,
175 ) -> Self {
176 let specs = locations.map(|loc| ProximitySpec::avoid(loc, distance));
177 self.all_of.extend(specs);
178 self
179 }
180
181 pub fn close_to_one_of(
182 mut self,
183 locations: impl Iterator<Item = Vec2<i32>>,
184 distance: i32,
185 ) -> Self {
186 let specs = locations.map(|loc| ProximitySpec::be_near(loc, distance));
187 self.any_of.extend(specs);
188 self
189 }
190}
191
192struct ProximityRequirements {
193 all_of: Vec<ProximitySpec>,
194 any_of: Vec<ProximitySpec>,
195 location_hint: Aabr<i32>,
196}
197
198impl ProximityRequirements {
199 pub fn satisfied_by(&self, site: Vec2<i32>) -> bool {
200 if self.location_hint.contains_point(site) {
201 let all_of_compliance = self.all_of.iter().all(|spec| spec.satisfied_by(site));
202 let any_of_compliance =
203 self.any_of.is_empty() || self.any_of.iter().any(|spec| spec.satisfied_by(site));
204 all_of_compliance && any_of_compliance
205 } else {
206 false
207 }
208 }
209}
210
211impl<R: Rng> GenCtx<'_, R> {
212 pub fn reseed(&mut self) -> GenCtx<'_, impl Rng + use<R>> {
213 let mut entropy = self.rng.gen::<[u8; 32]>();
214 entropy[0] = entropy[0].wrapping_add(SEED_SKIP); GenCtx {
216 sim: self.sim,
217 rng: ChaChaRng::from_seed(entropy),
218 }
219 }
220}
221
222#[derive(Debug)]
223pub enum WorldCivStage {
224 CivCreation(u32, u32),
227 SiteGeneration,
228}
229
230impl Civs {
231 pub fn generate(
232 seed: u32,
233 sim: &mut WorldSim,
234 index: &mut Index,
235 calendar: Option<&Calendar>,
236 report_stage: &dyn Fn(WorldCivStage),
237 ) -> Self {
238 prof_span!("Civs::generate");
239 let mut this = Self::default();
240 let rng = ChaChaRng::from_seed(seed_expan::rng_state(seed));
241 let name_rng = rng.clone();
242 let mut name_ctx = GenCtx { sim, rng: name_rng };
243 if index.features().peak_naming {
244 info!("starting peak naming");
245 this.name_peaks(&mut name_ctx);
246 }
247 if index.features().biome_naming {
248 info!("starting biome naming");
249 this.name_biomes(&mut name_ctx);
250 }
251
252 let initial_civ_count = initial_civ_count(sim.map_size_lg());
253 let mut ctx = GenCtx { sim, rng };
254
255 info!("starting civilisation creation");
259 prof_span!(guard, "create civs");
260 for i in 0..initial_civ_count {
261 prof_span!("create civ");
262 debug!("Creating civilisation...");
263 if this.birth_civ(&mut ctx.reseed()).is_none() {
264 warn!("Failed to find starting site for civilisation.");
265 }
266 report_stage(WorldCivStage::CivCreation(i, initial_civ_count));
267 }
268 drop(guard);
269 info!(?initial_civ_count, "all civilisations created");
270
271 report_stage(WorldCivStage::SiteGeneration);
272 prof_span!(guard, "find locations and establish sites");
273 let world_dims = ctx.sim.get_aabr();
274 for _ in 0..initial_civ_count * 3 {
275 attempt(5, || {
276 let (loc, kind) = match ctx.rng.gen_range(0..116) {
277 0..=4 => (
278 find_site_loc(
279 &mut ctx,
280 &ProximityRequirementsBuilder::new()
281 .avoid_all_of(this.tree_enemies(), 40)
282 .finalize(&world_dims),
283 &SiteKind::GiantTree,
284 )?,
285 SiteKind::GiantTree,
286 ),
287 5..=15 => (
288 find_site_loc(
289 &mut ctx,
290 &ProximityRequirementsBuilder::new()
291 .avoid_all_of(this.gnarling_enemies(), 40)
292 .finalize(&world_dims),
293 &SiteKind::Gnarling,
294 )?,
295 SiteKind::Gnarling,
296 ),
297 16..=20 => (
298 find_site_loc(
299 &mut ctx,
300 &ProximityRequirementsBuilder::new()
301 .avoid_all_of(this.chapel_site_enemies(), 40)
302 .finalize(&world_dims),
303 &SiteKind::ChapelSite,
304 )?,
305 SiteKind::ChapelSite,
306 ),
307 21..=27 => (
308 find_site_loc(
309 &mut ctx,
310 &ProximityRequirementsBuilder::new()
311 .avoid_all_of(this.gnarling_enemies(), 40)
312 .finalize(&world_dims),
313 &SiteKind::Adlet,
314 )?,
315 SiteKind::Adlet,
316 ),
317 28..=38 => (
318 find_site_loc(
319 &mut ctx,
320 &ProximityRequirementsBuilder::new()
321 .avoid_all_of(this.pirate_hideout_enemies(), 40)
322 .finalize(&world_dims),
323 &SiteKind::PirateHideout,
324 )?,
325 SiteKind::PirateHideout,
326 ),
327 39..=45 => (
328 find_site_loc(
329 &mut ctx,
330 &ProximityRequirementsBuilder::new()
331 .avoid_all_of(this.jungle_ruin_enemies(), 40)
332 .finalize(&world_dims),
333 &SiteKind::JungleRuin,
334 )?,
335 SiteKind::JungleRuin,
336 ),
337 46..=55 => (
338 find_site_loc(
339 &mut ctx,
340 &ProximityRequirementsBuilder::new()
341 .avoid_all_of(this.rock_circle_enemies(), 40)
342 .finalize(&world_dims),
343 &SiteKind::RockCircle,
344 )?,
345 SiteKind::RockCircle,
346 ),
347 56..=66 => (
348 find_site_loc(
349 &mut ctx,
350 &ProximityRequirementsBuilder::new()
351 .avoid_all_of(this.troll_cave_enemies(), 40)
352 .finalize(&world_dims),
353 &SiteKind::TrollCave,
354 )?,
355 SiteKind::TrollCave,
356 ),
357 67..=72 => (
358 find_site_loc(
359 &mut ctx,
360 &ProximityRequirementsBuilder::new()
361 .avoid_all_of(this.camp_enemies(), 40)
362 .finalize(&world_dims),
363 &SiteKind::Camp,
364 )?,
365 SiteKind::Camp,
366 ),
367 73..=76 => (
368 find_site_loc(
369 &mut ctx,
370 &ProximityRequirementsBuilder::new()
371 .avoid_all_of(this.mine_site_enemies(), 40)
372 .finalize(&world_dims),
373 &SiteKind::Haniwa,
374 )?,
375 SiteKind::Haniwa,
376 ),
377 77..=81 => (
378 find_site_loc(
379 &mut ctx,
380 &ProximityRequirementsBuilder::new()
381 .avoid_all_of(this.terracotta_enemies(), 40)
382 .finalize(&world_dims),
383 &SiteKind::Terracotta,
384 )?,
385 SiteKind::Terracotta,
386 ),
387 82..=87 => (
388 find_site_loc(
389 &mut ctx,
390 &ProximityRequirementsBuilder::new()
391 .avoid_all_of(this.mine_site_enemies(), 40)
392 .finalize(&world_dims),
393 &SiteKind::DwarvenMine,
394 )?,
395 SiteKind::DwarvenMine,
396 ),
397 88..=91 => (
398 find_site_loc(
399 &mut ctx,
400 &ProximityRequirementsBuilder::new()
401 .avoid_all_of(this.cultist_enemies(), 40)
402 .finalize(&world_dims),
403 &SiteKind::Cultist,
404 )?,
405 SiteKind::Cultist,
406 ),
407 92..=96 => (
408 find_site_loc(
409 &mut ctx,
410 &ProximityRequirementsBuilder::new()
411 .avoid_all_of(this.sahagin_enemies(), 40)
412 .finalize(&world_dims),
413 &SiteKind::Sahagin,
414 )?,
415 SiteKind::Sahagin,
416 ),
417 97..=102 => (
418 find_site_loc(
419 &mut ctx,
420 &ProximityRequirementsBuilder::new()
421 .avoid_all_of(this.vampire_castle_enemies(), 40)
422 .finalize(&world_dims),
423 &SiteKind::VampireCastle,
424 )?,
425 SiteKind::VampireCastle,
426 ),
427 103..108 => (
428 find_site_loc(
429 &mut ctx,
430 &ProximityRequirementsBuilder::new().finalize(&world_dims),
431 &SiteKind::GliderCourse,
432 )?,
433 SiteKind::GliderCourse,
434 ),
435 _ => (
449 find_site_loc(
450 &mut ctx,
451 &ProximityRequirementsBuilder::new()
452 .avoid_all_of(this.myrmidon_enemies(), 40)
453 .finalize(&world_dims),
454 &SiteKind::Myrmidon,
455 )?,
456 SiteKind::Myrmidon,
457 ),
458 };
459 Some(this.establish_site(&mut ctx.reseed(), loc, |place| Site {
460 kind,
461 center: loc,
462 place,
463 site_tmp: None,
464 }))
465 });
466 }
467 drop(guard);
468
469 prof_span!(guard, "Flatten ground around sites");
474 for site in this.sites.values() {
475 let wpos = site.center * TerrainChunkSize::RECT_SIZE.map(|e: u32| e as i32);
476
477 let (radius, flatten_radius) = match &site.kind {
478 SiteKind::Refactor => (32i32, 10.0),
479 SiteKind::CliffTown => (2i32, 1.0),
480 SiteKind::SavannahTown => (48i32, 25.0),
481 SiteKind::CoastalTown => (64i32, 35.0),
482 SiteKind::JungleRuin => (8i32, 3.0),
483 SiteKind::DesertCity => (64i32, 25.0),
484 SiteKind::ChapelSite => (36i32, 10.0),
485 SiteKind::Terracotta => (64i32, 35.0),
486 SiteKind::GiantTree => (12i32, 8.0),
487 SiteKind::Gnarling => (16i32, 10.0),
488 SiteKind::Citadel => (16i32, 0.0),
489 SiteKind::Bridge(_, _) => (0, 0.0),
490 SiteKind::Adlet => (16i32, 0.0),
491 SiteKind::Haniwa => (32i32, 16.0),
492 SiteKind::PirateHideout => (8i32, 3.0),
493 SiteKind::RockCircle => (8i32, 3.0),
494 SiteKind::TrollCave => (4i32, 1.5),
495 SiteKind::Camp => (4i32, 1.5),
496 SiteKind::DwarvenMine => (8i32, 3.0),
497 SiteKind::Cultist => (24i32, 10.0),
498 SiteKind::Sahagin => (8i32, 3.0),
499 SiteKind::VampireCastle => (10i32, 16.0),
500 SiteKind::GliderCourse => (0, 0.0),
501 SiteKind::Myrmidon => (64i32, 35.0),
502 };
503
504 if let Some(center_alt) = ctx.sim.get_alt_approx(wpos) {
506 for offs in Spiral2d::new().take(radius.pow(2) as usize) {
507 let pos = site.center + offs;
508 let factor = ((1.0
509 - (site.center - pos).map(|e| e as f32).magnitude()
510 / f32::max(flatten_radius, 0.01))
511 * 1.25)
512 .min(1.0);
513 let rng = &mut ctx.rng;
514 ctx.sim
515 .get_mut(pos)
516 .filter(|chunk| !chunk.river.near_water())
518 .map(|chunk| {
519 let diff = Lerp::lerp_precise(chunk.alt, center_alt, factor) - chunk.alt;
520 chunk.water_alt = CONFIG.sea_level.max(chunk.water_alt + diff);
525 chunk.alt += diff;
526 chunk.basement += diff;
527 chunk.rockiness = 0.0;
528 chunk.surface_veg *= 1.0 - factor * rng.gen_range(0.25..0.9);
529 });
530 }
531 }
532 }
533 drop(guard);
534
535 prof_span!(guard, "Place sites in world");
537 let mut cnt = 0;
538 let mut gen_meta = SitesGenMeta::new(seed);
539 for sim_site in this.sites.values_mut() {
540 cnt += 1;
541 let wpos = sim_site
542 .center
543 .map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| {
544 e * sz as i32 + sz as i32 / 2
545 });
546
547 let mut rng = ctx.reseed().rng;
548 let site = index.sites.insert({
549 let index_ref = IndexRef {
550 colors: &index.colors(),
551 features: &index.features(),
552 index,
553 };
554 match &sim_site.kind {
555 SiteKind::Refactor => {
556 let size = Lerp::lerp(0.03, 1.0, rng.gen_range(0.0..1f32).powi(5));
557 WorldSite::generate_city(
558 &Land::from_sim(ctx.sim),
559 index_ref,
560 &mut rng,
561 wpos,
562 size,
563 calendar,
564 &mut gen_meta,
565 )
566 },
567 SiteKind::GliderCourse => WorldSite::generate_glider_course(
568 &Land::from_sim(ctx.sim),
569 index_ref,
570 &mut rng,
571 wpos,
572 ),
573 SiteKind::CliffTown => WorldSite::generate_cliff_town(
574 &Land::from_sim(ctx.sim),
575 index_ref,
576 &mut rng,
577 wpos,
578 &mut gen_meta,
579 ),
580 SiteKind::SavannahTown => WorldSite::generate_savannah_town(
581 &Land::from_sim(ctx.sim),
582 index_ref,
583 &mut rng,
584 wpos,
585 &mut gen_meta,
586 ),
587 SiteKind::CoastalTown => WorldSite::generate_coastal_town(
588 &Land::from_sim(ctx.sim),
589 index_ref,
590 &mut rng,
591 wpos,
592 &mut gen_meta,
593 ),
594 SiteKind::PirateHideout => {
595 WorldSite::generate_pirate_hideout(&Land::from_sim(ctx.sim), &mut rng, wpos)
596 },
597 SiteKind::JungleRuin => {
598 WorldSite::generate_jungle_ruin(&Land::from_sim(ctx.sim), &mut rng, wpos)
599 },
600 SiteKind::RockCircle => {
601 WorldSite::generate_rock_circle(&Land::from_sim(ctx.sim), &mut rng, wpos)
602 },
603
604 SiteKind::TrollCave => {
605 WorldSite::generate_troll_cave(&Land::from_sim(ctx.sim), &mut rng, wpos)
606 },
607 SiteKind::Camp => {
608 WorldSite::generate_camp(&Land::from_sim(ctx.sim), &mut rng, wpos)
609 },
610 SiteKind::DesertCity => WorldSite::generate_desert_city(
611 &Land::from_sim(ctx.sim),
612 index_ref,
613 &mut rng,
614 wpos,
615 &mut gen_meta,
616 ),
617 SiteKind::GiantTree => {
618 WorldSite::generate_giant_tree(&Land::from_sim(ctx.sim), &mut rng, wpos)
619 },
620 SiteKind::Gnarling => {
621 WorldSite::generate_gnarling(&Land::from_sim(ctx.sim), &mut rng, wpos)
622 },
623 SiteKind::DwarvenMine => {
624 WorldSite::generate_mine(&Land::from_sim(ctx.sim), &mut rng, wpos)
625 },
626 SiteKind::ChapelSite => {
627 WorldSite::generate_chapel_site(&Land::from_sim(ctx.sim), &mut rng, wpos)
628 },
629 SiteKind::Terracotta => WorldSite::generate_terracotta(
630 &Land::from_sim(ctx.sim),
631 index_ref,
632 &mut rng,
633 wpos,
634 &mut gen_meta,
635 ),
636 SiteKind::Citadel => {
637 WorldSite::generate_citadel(&Land::from_sim(ctx.sim), &mut rng, wpos)
638 },
639 SiteKind::Bridge(a, b) => {
640 let mut bridge_site = WorldSite::generate_bridge(
641 &Land::from_sim(ctx.sim),
642 index_ref,
643 &mut rng,
644 *a,
645 *b,
646 );
647
648 if let Some(bridge) =
650 bridge_site
651 .plots
652 .values()
653 .find_map(|plot| match &plot.kind {
654 site::PlotKind::Bridge(bridge) => Some(bridge),
655 _ => None,
656 })
657 {
658 let mut update_offset = |original: Vec2<i32>, new: Vec2<i32>| {
659 let chunk = original.wpos_to_cpos();
660 if let Some(c) = ctx.sim.get_mut(chunk) {
661 c.path.0.offset = (new - chunk.cpos_to_wpos_center())
662 .map(|e| e.clamp(-16, 16) as i8);
663 }
664 };
665
666 update_offset(bridge.original_start, bridge.start.xy());
667 update_offset(bridge.original_end, bridge.end.xy());
668 }
669 bridge_site.demarcate_obstacles(&Land::from_sim(ctx.sim));
670 bridge_site
671 },
672 SiteKind::Adlet => WorldSite::generate_adlet(
673 &Land::from_sim(ctx.sim),
674 &mut rng,
675 wpos,
676 index_ref,
677 ),
678 SiteKind::Haniwa => {
679 WorldSite::generate_haniwa(&Land::from_sim(ctx.sim), &mut rng, wpos)
680 },
681 SiteKind::Cultist => {
682 WorldSite::generate_cultist(&Land::from_sim(ctx.sim), &mut rng, wpos)
683 },
684 SiteKind::Myrmidon => WorldSite::generate_myrmidon(
685 &Land::from_sim(ctx.sim),
686 index_ref,
687 &mut rng,
688 wpos,
689 &mut gen_meta,
690 ),
691 SiteKind::Sahagin => WorldSite::generate_sahagin(
692 &Land::from_sim(ctx.sim),
693 index_ref,
694 &mut rng,
695 wpos,
696 ),
697 SiteKind::VampireCastle => {
698 WorldSite::generate_vampire_castle(&Land::from_sim(ctx.sim), &mut rng, wpos)
699 },
700 }
701 });
702 sim_site.site_tmp = Some(site);
703 let site_ref = &index.sites[site];
704
705 let radius_chunks =
706 (site_ref.radius() / TerrainChunkSize::RECT_SIZE.x as f32).ceil() as usize;
707 for pos in Spiral2d::new()
708 .map(|offs| sim_site.center + offs)
709 .take((radius_chunks * 2).pow(2))
710 {
711 ctx.sim.get_mut(pos).map(|chunk| chunk.sites.push(site));
712 }
713 debug!(?sim_site.center, "Placed site at location");
714 }
715 drop(guard);
716 info!(?cnt, "all sites placed");
717 gen_meta.log();
718
719 for (s1, val) in this.track_map.iter() {
723 if let Some(index1) = this.sites.get(*s1).site_tmp {
724 for (s2, t) in val.iter() {
725 if let Some(index2) = this.sites.get(*s2).site_tmp {
726 if index.sites.get(index1).do_economic_simulation()
727 && index.sites.get(index2).do_economic_simulation()
728 {
729 let cost = this.tracks.get(*t).path.len();
730 index
731 .sites
732 .get_mut(index1)
733 .economy_mut()
734 .add_neighbor(index2, cost);
735 index
736 .sites
737 .get_mut(index2)
738 .economy_mut()
739 .add_neighbor(index1, cost);
740 }
741 }
742 }
743 }
744 }
745
746 prof_span!(guard, "generate airship routes");
747 this.airships.generate_airship_routes(ctx.sim, index);
748 drop(guard);
749
750 prof_span!(guard, "collect natural resources");
754 let sites = &mut index.sites;
755 (0..ctx.sim.map_size_lg().chunks_len()).for_each(|posi| {
756 let chpos = uniform_idx_as_vec2(ctx.sim.map_size_lg(), posi);
757 let wpos = chpos.map(|e| e as i64) * TerrainChunkSize::RECT_SIZE.map(|e| e as i64);
758 let closest_site = (*sites)
759 .iter_mut()
760 .filter(|s| !matches!(s.1.kind, Some(crate::site::SiteKind::Myrmidon)))
761 .min_by_key(|(_id, s)| s.origin.map(|e| e as i64).distance_squared(wpos));
762 if let Some((_id, s)) = closest_site
763 && s.do_economic_simulation()
764 {
765 let distance_squared = s.origin.map(|e| e as i64).distance_squared(wpos);
766 s.economy_mut()
767 .add_chunk(ctx.sim.get(chpos).unwrap(), distance_squared);
768 }
769 });
770 drop(guard);
771
772 sites.iter_mut().for_each(|(_, s)| {
773 if let Some(econ) = s.economy.as_mut() {
774 econ.cache_economy()
775 }
776 });
777
778 this
779 }
780
781 pub fn place(&self, id: Id<Place>) -> &Place { self.places.get(id) }
782
783 pub fn sites(&self) -> impl Iterator<Item = &Site> + '_ { self.sites.values() }
784
785 #[expect(dead_code)]
786 fn display_info(&self) {
787 for (id, civ) in self.civs.iter() {
788 println!("# Civilisation {:?}", id);
789 println!("Name: <unnamed>");
790 println!("Homeland: {:#?}", self.places.get(civ.homeland));
791 }
792
793 for (id, site) in self.sites.iter() {
794 println!("# Site {:?}", id);
795 println!("{:#?}", site);
796 }
797 }
798
799 pub fn track_between(&self, a: Id<Site>, b: Id<Site>) -> Option<(Id<Track>, bool)> {
802 self.track_map
803 .get(&a)
804 .and_then(|dests| Some((*dests.get(&b)?, false)))
805 .or_else(|| {
806 self.track_map
807 .get(&b)
808 .and_then(|dests| Some((*dests.get(&a)?, true)))
809 })
810 }
811
812 pub fn neighbors(&self, site: Id<Site>) -> impl Iterator<Item = Id<Site>> + '_ {
814 let to = self
815 .track_map
816 .get(&site)
817 .map(|dests| dests.keys())
818 .into_iter()
819 .flatten();
820 let fro = self
821 .track_map
822 .iter()
823 .filter(move |(_, dests)| dests.contains_key(&site))
824 .map(|(p, _)| p);
825 to.chain(fro).filter(move |p| **p != site).copied()
826 }
827
828 fn route_between(&self, a: Id<Site>, b: Id<Site>) -> Option<(Path<Id<Site>>, f32)> {
830 let heuristic = move |p: &Id<Site>| {
831 (self
832 .sites
833 .get(*p)
834 .center
835 .distance_squared(self.sites.get(b).center) as f32)
836 .sqrt()
837 };
838 let transition =
839 |a: Id<Site>, b: Id<Site>| self.tracks.get(self.track_between(a, b).unwrap().0).cost;
840 let neighbors = |p: &Id<Site>| {
841 let p = *p;
842 self.neighbors(p)
843 .map(move |neighbor| (neighbor, transition(p, neighbor)))
844 };
845 let satisfied = |p: &Id<Site>| *p == b;
846 let mut astar = Astar::new(100, a, BuildHasherDefault::<FxHasher64>::default());
851 astar.poll(100, heuristic, neighbors, satisfied).into_path()
852 }
853
854 fn birth_civ(&mut self, ctx: &mut GenCtx<impl Rng>) -> Option<Id<Civ>> {
855 let kind = match ctx.rng.gen_range(0..64) {
857 0..=8 => SiteKind::CliffTown,
858 9..=17 => SiteKind::DesertCity,
859 18..=23 => SiteKind::SavannahTown,
860 24..=33 => SiteKind::CoastalTown,
861 _ => SiteKind::Refactor,
862 };
863 let world_dims = ctx.sim.get_aabr();
864 let avoid_town_enemies = ProximityRequirementsBuilder::new()
865 .avoid_all_of(self.town_enemies(), 60)
866 .finalize(&world_dims);
867 let loc = (0..100)
868 .flat_map(|_| {
869 find_site_loc(ctx, &avoid_town_enemies, &kind).and_then(|loc| {
870 town_attributes_of_site(loc, ctx.sim)
871 .map(|town_attrs| (loc, town_attrs.score()))
872 })
873 })
874 .reduce(|a, b| if a.1 > b.1 { a } else { b })?
875 .0;
876
877 let site = self.establish_site(ctx, loc, |place| Site {
878 kind,
879 site_tmp: None,
880 center: loc,
881 place,
882 });
887
888 let civ = self.civs.insert(Civ {
889 capital: site,
890 homeland: self.sites.get(site).place,
891 });
892
893 Some(civ)
894 }
895
896 fn establish_place(
897 &mut self,
898 _ctx: &mut GenCtx<impl Rng>,
899 loc: Vec2<i32>,
900 _area: Range<usize>,
901 ) -> Id<Place> {
902 self.places.insert(Place { center: loc })
903 }
904
905 fn name_biomes(&mut self, ctx: &mut GenCtx<impl Rng>) {
907 prof_span!("name_biomes");
908 let map_size_lg = ctx.sim.map_size_lg();
909 let world_size = map_size_lg.chunks();
910 let mut biomes: Vec<(common::terrain::BiomeKind, Vec<usize>)> = Vec::new();
911 let mut explored = vec![false; world_size.x as usize * world_size.y as usize];
912 let mut to_floodfill = Vec::new();
913 let mut to_explore = Vec::new();
914 let start_point = 0;
916 to_explore.push(start_point);
917
918 while let Some(exploring) = to_explore.pop() {
919 if explored[exploring] {
920 continue;
921 }
922 to_floodfill.push(exploring);
923 let biome = ctx.sim.chunks[exploring].get_biome();
925 let mut filled = Vec::new();
926
927 while let Some(filling) = to_floodfill.pop() {
928 explored[filling] = true;
929 filled.push(filling);
930 for neighbour in common::terrain::neighbors(map_size_lg, filling) {
931 if explored[neighbour] {
932 continue;
933 }
934 let n_biome = ctx.sim.chunks[neighbour].get_biome();
935 if n_biome == biome {
936 to_floodfill.push(neighbour);
937 } else {
938 to_explore.push(neighbour);
939 }
940 }
941 }
942
943 biomes.push((biome, filled));
944 }
945
946 prof_span!("after flood fill");
947 let mut biome_count = 0;
948 for biome in biomes {
949 let name = match biome.0 {
950 common::terrain::BiomeKind::Lake if biome.1.len() as u32 > 200 => Some(format!(
951 "{} {}",
952 ["Lake", "Loch"].choose(&mut ctx.rng).unwrap(),
953 NameGen::location(&mut ctx.rng).generate_lake_custom()
954 )),
955 common::terrain::BiomeKind::Lake if biome.1.len() as u32 > 10 => Some(format!(
956 "{} {}",
957 NameGen::location(&mut ctx.rng).generate_lake_custom(),
958 ["Pool", "Well", "Pond"].choose(&mut ctx.rng).unwrap()
959 )),
960 common::terrain::BiomeKind::Grassland if biome.1.len() as u32 > 750 => {
961 Some(format!(
962 "{} {}",
963 [
964 NameGen::location(&mut ctx.rng).generate_grassland_engl(),
965 NameGen::location(&mut ctx.rng).generate_grassland_custom()
966 ]
967 .choose(&mut ctx.rng)
968 .unwrap(),
969 [
970 "Grasslands",
971 "Plains",
972 "Meadows",
973 "Fields",
974 "Heath",
975 "Hills",
976 "Prairie",
977 "Lowlands",
978 "Steppe",
979 "Downs",
980 "Greens",
981 ]
982 .choose(&mut ctx.rng)
983 .unwrap()
984 ))
985 },
986 common::terrain::BiomeKind::Ocean if biome.1.len() as u32 > 750 => Some(format!(
987 "{} {}",
988 [
989 NameGen::location(&mut ctx.rng).generate_ocean_engl(),
990 NameGen::location(&mut ctx.rng).generate_ocean_custom()
991 ]
992 .choose(&mut ctx.rng)
993 .unwrap(),
994 ["Sea", "Bay", "Gulf", "Deep", "Depths", "Ocean", "Blue",]
995 .choose(&mut ctx.rng)
996 .unwrap()
997 )),
998 common::terrain::BiomeKind::Mountain if biome.1.len() as u32 > 750 => {
999 Some(format!(
1000 "{} {}",
1001 [
1002 NameGen::location(&mut ctx.rng).generate_mountain_engl(),
1003 NameGen::location(&mut ctx.rng).generate_mountain_custom()
1004 ]
1005 .choose(&mut ctx.rng)
1006 .unwrap(),
1007 [
1008 "Mountains",
1009 "Range",
1010 "Reach",
1011 "Massif",
1012 "Rocks",
1013 "Cliffs",
1014 "Peaks",
1015 "Heights",
1016 "Bluffs",
1017 "Ridge",
1018 "Canyon",
1019 "Plateau",
1020 ]
1021 .choose(&mut ctx.rng)
1022 .unwrap()
1023 ))
1024 },
1025 common::terrain::BiomeKind::Snowland if biome.1.len() as u32 > 750 => {
1026 Some(format!(
1027 "{} {}",
1028 [
1029 NameGen::location(&mut ctx.rng).generate_snowland_engl(),
1030 NameGen::location(&mut ctx.rng).generate_snowland_custom()
1031 ]
1032 .choose(&mut ctx.rng)
1033 .unwrap(),
1034 [
1035 "Snowlands",
1036 "Glacier",
1037 "Tundra",
1038 "Drifts",
1039 "Snowfields",
1040 "Hills",
1041 "Downs",
1042 "Uplands",
1043 "Highlands",
1044 ]
1045 .choose(&mut ctx.rng)
1046 .unwrap()
1047 ))
1048 },
1049 common::terrain::BiomeKind::Desert if biome.1.len() as u32 > 750 => Some(format!(
1050 "{} {}",
1051 [
1052 NameGen::location(&mut ctx.rng).generate_desert_engl(),
1053 NameGen::location(&mut ctx.rng).generate_desert_custom()
1054 ]
1055 .choose(&mut ctx.rng)
1056 .unwrap(),
1057 [
1058 "Desert", "Sands", "Sandsea", "Drifts", "Dunes", "Droughts", "Flats",
1059 ]
1060 .choose(&mut ctx.rng)
1061 .unwrap()
1062 )),
1063 common::terrain::BiomeKind::Swamp if biome.1.len() as u32 > 200 => Some(format!(
1064 "{} {}",
1065 NameGen::location(&mut ctx.rng).generate_swamp_engl(),
1066 [
1067 "Swamp",
1068 "Swamps",
1069 "Swamplands",
1070 "Marsh",
1071 "Marshlands",
1072 "Morass",
1073 "Mire",
1074 "Bog",
1075 "Wetlands",
1076 "Fen",
1077 "Moors",
1078 ]
1079 .choose(&mut ctx.rng)
1080 .unwrap()
1081 )),
1082 common::terrain::BiomeKind::Jungle if biome.1.len() as u32 > 85 => Some(format!(
1083 "{} {}",
1084 [
1085 NameGen::location(&mut ctx.rng).generate_jungle_engl(),
1086 NameGen::location(&mut ctx.rng).generate_jungle_custom()
1087 ]
1088 .choose(&mut ctx.rng)
1089 .unwrap(),
1090 [
1091 "Jungle",
1092 "Rainforest",
1093 "Greatwood",
1094 "Wilds",
1095 "Wildwood",
1096 "Tangle",
1097 "Tanglewood",
1098 "Bush",
1099 ]
1100 .choose(&mut ctx.rng)
1101 .unwrap()
1102 )),
1103 common::terrain::BiomeKind::Forest if biome.1.len() as u32 > 750 => Some(format!(
1104 "{} {}",
1105 [
1106 NameGen::location(&mut ctx.rng).generate_forest_engl(),
1107 NameGen::location(&mut ctx.rng).generate_forest_custom()
1108 ]
1109 .choose(&mut ctx.rng)
1110 .unwrap(),
1111 ["Forest", "Woodlands", "Woods", "Glades", "Grove", "Weald",]
1112 .choose(&mut ctx.rng)
1113 .unwrap()
1114 )),
1115 common::terrain::BiomeKind::Savannah if biome.1.len() as u32 > 750 => {
1116 Some(format!(
1117 "{} {}",
1118 [
1119 NameGen::location(&mut ctx.rng).generate_savannah_engl(),
1120 NameGen::location(&mut ctx.rng).generate_savannah_custom()
1121 ]
1122 .choose(&mut ctx.rng)
1123 .unwrap(),
1124 [
1125 "Savannah",
1126 "Shrublands",
1127 "Sierra",
1128 "Prairie",
1129 "Lowlands",
1130 "Flats",
1131 ]
1132 .choose(&mut ctx.rng)
1133 .unwrap()
1134 ))
1135 },
1136 common::terrain::BiomeKind::Taiga if biome.1.len() as u32 > 750 => Some(format!(
1137 "{} {}",
1138 [
1139 NameGen::location(&mut ctx.rng).generate_taiga_engl(),
1140 NameGen::location(&mut ctx.rng).generate_taiga_custom()
1141 ]
1142 .choose(&mut ctx.rng)
1143 .unwrap(),
1144 [
1145 "Forest",
1146 "Woodlands",
1147 "Woods",
1148 "Timberlands",
1149 "Highlands",
1150 "Uplands",
1151 ]
1152 .choose(&mut ctx.rng)
1153 .unwrap()
1154 )),
1155 _ => None,
1156 };
1157 if let Some(name) = name {
1158 let center = biome
1160 .1
1161 .iter()
1162 .map(|b| {
1163 uniform_idx_as_vec2(map_size_lg, *b).as_::<f32>() / biome.1.len() as f32
1164 })
1165 .sum::<Vec2<f32>>()
1166 .as_::<i32>();
1167 let idx = *biome
1169 .1
1170 .iter()
1171 .min_by_key(|&b| center.distance_squared(uniform_idx_as_vec2(map_size_lg, *b)))
1172 .unwrap();
1173 let id = self.pois.insert(PointOfInterest {
1174 name,
1175 loc: uniform_idx_as_vec2(map_size_lg, idx),
1176 kind: PoiKind::Biome(biome.1.len() as u32),
1177 });
1178 for chunk in biome.1 {
1179 ctx.sim.chunks[chunk].poi = Some(id);
1180 }
1181 biome_count += 1;
1182 }
1183 }
1184
1185 info!(?biome_count, "all biomes named");
1186 }
1187
1188 fn name_peaks(&mut self, ctx: &mut GenCtx<impl Rng>) {
1190 prof_span!("name_peaks");
1191 let map_size_lg = ctx.sim.map_size_lg();
1192 const MIN_MOUNTAIN_ALT: f32 = 600.0;
1193 const MIN_MOUNTAIN_CHAOS: f32 = 0.35;
1194 let rng = &mut ctx.rng;
1195 let sim_chunks = &ctx.sim.chunks;
1196 let peaks = sim_chunks
1197 .iter()
1198 .enumerate()
1199 .filter(|(posi, chunk)| {
1200 let neighbor_alts_max = common::terrain::neighbors(map_size_lg, *posi)
1201 .map(|i| sim_chunks[i].alt as u32)
1202 .max();
1203 chunk.alt > MIN_MOUNTAIN_ALT
1204 && chunk.chaos > MIN_MOUNTAIN_CHAOS
1205 && neighbor_alts_max.is_some_and(|n_alt| chunk.alt as u32 > n_alt)
1206 })
1207 .map(|(posi, chunk)| {
1208 (
1209 posi,
1210 uniform_idx_as_vec2(map_size_lg, posi),
1211 (chunk.alt - CONFIG.sea_level) as u32,
1212 )
1213 })
1214 .collect::<Vec<(usize, Vec2<i32>, u32)>>();
1215 let mut num_peaks = 0;
1216 let mut removals = vec![false; peaks.len()];
1217 for (i, peak) in peaks.iter().enumerate() {
1218 for (k, n_peak) in peaks.iter().enumerate() {
1219 if i != k
1223 && (peak.1).distance_squared(n_peak.1) < POI_THINNING_DIST_SQRD
1224 && peak.2 <= n_peak.2
1225 {
1226 removals[i] = true;
1230 }
1231 }
1232 }
1233 peaks
1234 .iter()
1235 .enumerate()
1236 .filter(|&(i, _)| !removals[i])
1237 .for_each(|(_, (_, loc, alt))| {
1238 num_peaks += 1;
1239 self.pois.insert(PointOfInterest {
1240 name: {
1241 let name = NameGen::location(rng).generate();
1242 if *alt < 1000 {
1243 match rng.gen_range(0..6) {
1244 0 => format!("{} Bluff", name),
1245 1 => format!("{} Crag", name),
1246 _ => format!("{} Hill", name),
1247 }
1248 } else {
1249 match rng.gen_range(0..8) {
1250 0 => format!("{}'s Peak", name),
1251 1 => format!("{} Peak", name),
1252 2 => format!("{} Summit", name),
1253 _ => format!("Mount {}", name),
1254 }
1255 }
1256 },
1257 kind: PoiKind::Peak(*alt),
1258 loc: *loc,
1259 });
1260 });
1261 info!(?num_peaks, "all peaks named");
1262 }
1263
1264 fn establish_site(
1265 &mut self,
1266 ctx: &mut GenCtx<impl Rng>,
1267 loc: Vec2<i32>,
1268 site_fn: impl FnOnce(Id<Place>) -> Site,
1269 ) -> Id<Site> {
1270 prof_span!("establish_site");
1271 const SITE_AREA: Range<usize> = 1..4; fn establish_site(
1274 civs: &mut Civs,
1275 ctx: &mut GenCtx<impl Rng>,
1276 loc: Vec2<i32>,
1277 site_fn: impl FnOnce(Id<Place>) -> Site,
1278 ) -> Id<Site> {
1279 let place = match ctx.sim.get(loc).and_then(|site| site.place) {
1280 Some(place) => place,
1281 None => civs.establish_place(ctx, loc, SITE_AREA),
1282 };
1283
1284 civs.sites.insert(site_fn(place))
1285 }
1286
1287 let site = establish_site(self, ctx, loc, site_fn);
1288
1289 const MAX_NEIGHBOR_DISTANCE: f32 = 400.0;
1297 let mut nearby = self
1298 .sites
1299 .iter()
1300 .filter(|&(id, _)| id != site)
1301 .filter(|(_, p)| {
1302 matches!(
1303 p.kind,
1304 SiteKind::Refactor
1305 | SiteKind::CliffTown
1306 | SiteKind::SavannahTown
1307 | SiteKind::CoastalTown
1308 | SiteKind::DesertCity
1309 )
1310 })
1311 .map(|(id, p)| (id, (p.center.distance_squared(loc) as f32).sqrt()))
1312 .filter(|(_, dist)| *dist < MAX_NEIGHBOR_DISTANCE)
1313 .collect::<Vec<_>>();
1314 nearby.sort_by_key(|(_, dist)| *dist as i32);
1315
1316 if let SiteKind::Refactor
1317 | SiteKind::CliffTown
1318 | SiteKind::SavannahTown
1319 | SiteKind::CoastalTown
1320 | SiteKind::DesertCity = self.sites[site].kind
1321 {
1322 for (nearby, _) in nearby.into_iter().take(4) {
1323 prof_span!("for nearby");
1324 let max_novel_cost = self
1328 .route_between(site, nearby)
1329 .map_or(f32::MAX, |(_, route_cost)| route_cost / 3.0);
1330
1331 let start = loc;
1332 let end = self.sites.get(nearby).center;
1333 let get_bridge = |start| self.bridges.get(&start).map(|(end, _)| *end);
1335 if let Some((path, cost)) = find_path(ctx, get_bridge, start, end, max_novel_cost) {
1336 for locs in path.nodes().windows(3) {
1338 if let Some((i, _)) = NEIGHBORS
1339 .iter()
1340 .enumerate()
1341 .find(|(_, dir)| **dir == locs[0] - locs[1])
1342 {
1343 ctx.sim.get_mut(locs[0]).unwrap().path.0.neighbors |=
1344 1 << ((i as u8 + 4) % 8);
1345 ctx.sim.get_mut(locs[1]).unwrap().path.0.neighbors |= 1 << (i as u8);
1346 }
1347
1348 if let Some((i, _)) = NEIGHBORS
1349 .iter()
1350 .enumerate()
1351 .find(|(_, dir)| **dir == locs[2] - locs[1])
1352 {
1353 ctx.sim.get_mut(locs[2]).unwrap().path.0.neighbors |=
1354 1 << ((i as u8 + 4) % 8);
1355
1356 ctx.sim.get_mut(locs[1]).unwrap().path.0.neighbors |= 1 << (i as u8);
1357 ctx.sim.get_mut(locs[1]).unwrap().path.0.offset =
1358 Vec2::new(ctx.rng.gen_range(-16..17), ctx.rng.gen_range(-16..17));
1359 } else if !self.bridges.contains_key(&locs[1]) {
1360 let center = (locs[1] + locs[2]) / 2;
1361 let id =
1362 establish_site(self, &mut ctx.reseed(), center, move |place| {
1363 Site {
1364 kind: SiteKind::Bridge(locs[1], locs[2]),
1365 site_tmp: None,
1366 center,
1367 place,
1368 }
1369 });
1370 self.bridges.insert(locs[1], (locs[2], id));
1371 self.bridges.insert(locs[2], (locs[1], id));
1372 }
1373 }
1397
1398 let track = self.tracks.insert(Track { cost, path });
1400 self.track_map
1401 .entry(site)
1402 .or_default()
1403 .insert(nearby, track);
1404 }
1405 }
1406 }
1407
1408 site
1409 }
1410
1411 fn gnarling_enemies(&self) -> impl Iterator<Item = Vec2<i32>> + '_ {
1412 self.sites().filter_map(|s| match s.kind {
1413 SiteKind::GiantTree => None,
1414 _ => Some(s.center),
1415 })
1416 }
1417
1418 fn adlet_enemies(&self) -> impl Iterator<Item = Vec2<i32>> + '_ {
1419 self.sites().map(|s| s.center)
1420 }
1421
1422 fn haniwa_enemies(&self) -> impl Iterator<Item = Vec2<i32>> + '_ {
1423 self.sites().map(|s| s.center)
1424 }
1425
1426 fn chapel_site_enemies(&self) -> impl Iterator<Item = Vec2<i32>> + '_ {
1427 self.sites().map(|s| s.center)
1428 }
1429
1430 fn mine_site_enemies(&self) -> impl Iterator<Item = Vec2<i32>> + '_ {
1431 self.sites().map(|s| s.center)
1432 }
1433
1434 fn terracotta_enemies(&self) -> impl Iterator<Item = Vec2<i32>> + '_ {
1435 self.sites().map(|s| s.center)
1436 }
1437
1438 fn cultist_enemies(&self) -> impl Iterator<Item = Vec2<i32>> + '_ {
1439 self.sites().map(|s| s.center)
1440 }
1441
1442 fn myrmidon_enemies(&self) -> impl Iterator<Item = Vec2<i32>> + '_ {
1443 self.sites().map(|s| s.center)
1444 }
1445
1446 fn vampire_castle_enemies(&self) -> impl Iterator<Item = Vec2<i32>> + '_ {
1447 self.sites().map(|s| s.center)
1448 }
1449
1450 fn tree_enemies(&self) -> impl Iterator<Item = Vec2<i32>> + '_ {
1451 self.sites().map(|s| s.center)
1452 }
1453
1454 fn castle_enemies(&self) -> impl Iterator<Item = Vec2<i32>> + '_ {
1455 self.sites().filter_map(|s| {
1456 if s.is_settlement() {
1457 None
1458 } else {
1459 Some(s.center)
1460 }
1461 })
1462 }
1463
1464 fn jungle_ruin_enemies(&self) -> impl Iterator<Item = Vec2<i32>> + '_ {
1465 self.sites().map(|s| s.center)
1466 }
1467
1468 fn town_enemies(&self) -> impl Iterator<Item = Vec2<i32>> + '_ {
1469 self.sites().filter_map(|s| match s.kind {
1470 SiteKind::Citadel => None,
1471 _ => Some(s.center),
1472 })
1473 }
1474
1475 fn towns(&self) -> impl Iterator<Item = Vec2<i32>> + '_ {
1476 self.sites().filter_map(|s| {
1477 if s.is_settlement() {
1478 Some(s.center)
1479 } else {
1480 None
1481 }
1482 })
1483 }
1484
1485 fn pirate_hideout_enemies(&self) -> impl Iterator<Item = Vec2<i32>> + '_ {
1486 self.sites().map(|s| s.center)
1487 }
1488
1489 fn sahagin_enemies(&self) -> impl Iterator<Item = Vec2<i32>> + '_ {
1490 self.sites().map(|s| s.center)
1491 }
1492
1493 fn rock_circle_enemies(&self) -> impl Iterator<Item = Vec2<i32>> + '_ {
1494 self.sites().map(|s| s.center)
1495 }
1496
1497 fn troll_cave_enemies(&self) -> impl Iterator<Item = Vec2<i32>> + '_ {
1498 self.sites().map(|s| s.center)
1499 }
1500
1501 fn camp_enemies(&self) -> impl Iterator<Item = Vec2<i32>> + '_ {
1502 self.sites().map(|s| s.center)
1503 }
1504}
1505
1506fn find_path(
1508 ctx: &mut GenCtx<impl Rng>,
1509 get_bridge: impl Fn(Vec2<i32>) -> Option<Vec2<i32>>,
1510 a: Vec2<i32>,
1511 b: Vec2<i32>,
1512 max_path_cost: f32,
1513) -> Option<(Path<Vec2<i32>>, f32)> {
1514 prof_span!("find_path");
1515 const MAX_PATH_ITERS: usize = 100_000;
1516 let sim = &ctx.sim;
1517 let heuristic = move |l: &Vec2<i32>| (l.distance_squared(b) as f32).sqrt();
1524 let neighbors = |l: &Vec2<i32>| {
1525 let l = *l;
1526 let bridge = get_bridge(l);
1527 let potential = walk_in_all_dirs(sim, bridge, l);
1528 potential
1529 .into_iter()
1530 .filter_map(|p| p.map(|(node, cost)| (node, cost + 1.0)))
1531 };
1532 let satisfied = |l: &Vec2<i32>| *l == b;
1533 let mut astar = Astar::new(
1538 MAX_PATH_ITERS,
1539 a,
1540 BuildHasherDefault::<FxHasher64>::default(),
1541 )
1542 .with_max_cost(max_path_cost);
1543 astar
1544 .poll(MAX_PATH_ITERS, heuristic, neighbors, satisfied)
1545 .into_path()
1546}
1547
1548fn walk_in_all_dirs(
1555 sim: &WorldSim,
1556 bridge: Option<Vec2<i32>>,
1557 a: Vec2<i32>,
1558) -> [Option<(Vec2<i32>, f32)>; 8] {
1559 let mut potential = [None; 8];
1560
1561 let adjacents = NEIGHBORS.map(|dir| a + dir);
1562
1563 let Some(a_chunk) = sim.get(a) else {
1564 return potential;
1565 };
1566 let mut chunks = [None; 8];
1567 for i in 0..8 {
1568 if loc_suitable_for_walking(sim, adjacents[i]) {
1569 chunks[i] = sim.get(adjacents[i]);
1570 }
1571 }
1572
1573 for i in 0..8 {
1574 let Some(b_chunk) = chunks[i] else { continue };
1575
1576 let hill_cost = ((b_chunk.alt - a_chunk.alt).abs() / 5.0).powi(2);
1577 let water_cost = (b_chunk.water_alt - b_chunk.alt + 8.0).clamped(0.0, 8.0) * 3.0; let wild_cost = if b_chunk.path.0.is_way() {
1579 0.0 } else {
1581 3.0 };
1583
1584 let cost = 1.0 + hill_cost + water_cost + wild_cost;
1585 potential[i] = Some((adjacents[i], cost));
1586 }
1587
1588 for (i, &dir) in NEIGHBORS.iter().enumerate() {
1591 let is_cardinal_dir = dir.x == 0 || dir.y == 0;
1592 if is_cardinal_dir && potential[i].is_none() {
1593 potential[i] = (4..=5).find_map(|i| {
1595 loc_suitable_for_walking(sim, a + dir * i)
1596 .then(|| (a + dir * i, 120.0 + (i - 4) as f32 * 10.0))
1597 });
1598 }
1599 }
1600
1601 if let Some(p) = bridge {
1603 let dir = (p - a).map(|e| e.signum());
1604 if let Some((dir_index, _)) = NEIGHBORS
1605 .iter()
1606 .enumerate()
1607 .find(|(_, n_dir)| **n_dir == dir)
1608 {
1609 potential[dir_index] = Some((p, (p - a).map(|e| e.abs()).reduce_max() as f32));
1610 }
1611 }
1612
1613 potential
1614}
1615
1616fn loc_suitable_for_walking(sim: &WorldSim, loc: Vec2<i32>) -> bool {
1618 if sim.get(loc).is_some() {
1619 NEIGHBORS.iter().all(|n| {
1620 sim.get(loc + *n)
1621 .is_some_and(|chunk| !chunk.river.near_water())
1622 })
1623 } else {
1624 false
1625 }
1626}
1627
1628fn find_site_loc(
1633 ctx: &mut GenCtx<impl Rng>,
1634 proximity_reqs: &ProximityRequirements,
1635 site_kind: &SiteKind,
1636) -> Option<Vec2<i32>> {
1637 prof_span!("find_site_loc");
1638 const MAX_ATTEMPTS: usize = 10000;
1639 let mut loc = None;
1640 let location_hint = proximity_reqs.location_hint;
1641 for _ in 0..MAX_ATTEMPTS {
1642 let test_loc = loc.unwrap_or_else(|| {
1643 Vec2::new(
1644 ctx.rng.gen_range(location_hint.min.x..location_hint.max.x),
1645 ctx.rng.gen_range(location_hint.min.y..location_hint.max.y),
1646 )
1647 });
1648
1649 let is_suitable_loc = site_kind.is_suitable_loc(test_loc, ctx.sim);
1650 if is_suitable_loc && proximity_reqs.satisfied_by(test_loc) {
1651 if site_kind.exclusion_radius_clear(ctx.sim, test_loc) {
1652 return Some(test_loc);
1653 }
1654
1655 loc = ctx.sim.get(test_loc).and_then(|c| c.downhill);
1658 }
1659 }
1660
1661 debug!("Failed to place site {:?}.", site_kind);
1662 None
1663}
1664
1665fn town_attributes_of_site(loc: Vec2<i32>, sim: &WorldSim) -> Option<TownSiteAttributes> {
1666 sim.get(loc).map(|chunk| {
1667 const RESOURCE_RADIUS: i32 = 1;
1668 let mut river_chunks = 0;
1669 let mut lake_chunks = 0;
1670 let mut ocean_chunks = 0;
1671 let mut rock_chunks = 0;
1672 let mut tree_chunks = 0;
1673 let mut farmable_chunks = 0;
1674 let mut farmable_needs_irrigation_chunks = 0;
1675 let mut land_chunks = 0;
1676 for x in (-RESOURCE_RADIUS)..RESOURCE_RADIUS {
1677 for y in (-RESOURCE_RADIUS)..RESOURCE_RADIUS {
1678 let check_loc = loc + Vec2::new(x, y).cpos_to_wpos();
1679 sim.get(check_loc).map(|c| {
1680 if num::abs(chunk.alt - c.alt) < 200.0 {
1681 if c.river.is_river() {
1682 river_chunks += 1;
1683 }
1684 if c.river.is_lake() {
1685 lake_chunks += 1;
1686 }
1687 if c.river.is_ocean() {
1688 ocean_chunks += 1;
1689 }
1690 if c.tree_density > 0.7 {
1691 tree_chunks += 1;
1692 }
1693 if c.rockiness < 0.3 && c.temp > CONFIG.snow_temp {
1694 if c.surface_veg > 0.5 {
1695 farmable_chunks += 1;
1696 } else {
1697 match c.get_biome() {
1698 common::terrain::BiomeKind::Savannah => {
1699 farmable_needs_irrigation_chunks += 1
1700 },
1701 common::terrain::BiomeKind::Desert => {
1702 farmable_needs_irrigation_chunks += 1
1703 },
1704 _ => (),
1705 }
1706 }
1707 }
1708 if !c.river.is_river() && !c.river.is_lake() && !c.river.is_ocean() {
1709 land_chunks += 1;
1710 }
1711 }
1712 if c.rockiness > 0.7 && c.alt - chunk.alt > -10.0 {
1714 rock_chunks += 1;
1715 }
1716 });
1717 }
1718 }
1719 let has_river = river_chunks > 1;
1720 let has_lake = lake_chunks > 1;
1721 let vegetation_implies_potable_water = chunk.tree_density > 0.4
1722 && !matches!(chunk.get_biome(), common::terrain::BiomeKind::Swamp);
1723 let has_many_rocks = chunk.rockiness > 1.2;
1724 let warm_or_firewood = chunk.temp > CONFIG.snow_temp || tree_chunks > 2;
1725 let has_potable_water =
1726 { has_river || (has_lake && chunk.alt > 100.0) || vegetation_implies_potable_water };
1727 let has_building_materials = tree_chunks > 0
1728 || rock_chunks > 0
1729 || chunk.temp > CONFIG.tropical_temp && (has_river || has_lake);
1730 let water_rich = lake_chunks + river_chunks > 2;
1731 let can_grow_rice = water_rich
1732 && chunk.humidity + 1.0 > CONFIG.jungle_hum
1733 && chunk.temp + 1.0 > CONFIG.tropical_temp;
1734 let farming_score = if can_grow_rice {
1735 farmable_chunks * 2
1736 } else {
1737 farmable_chunks
1738 } + if water_rich {
1739 farmable_needs_irrigation_chunks
1740 } else {
1741 0
1742 };
1743 let fish_score = lake_chunks + ocean_chunks;
1744 let food_score = farming_score + fish_score;
1745 let mining_score = if tree_chunks > 1 { rock_chunks } else { 0 };
1746 let forestry_score = if has_river { tree_chunks } else { 0 };
1747 let trading_score = std::cmp::min(std::cmp::min(land_chunks, ocean_chunks), river_chunks);
1748 TownSiteAttributes {
1749 food_score,
1750 mining_score,
1751 forestry_score,
1752 trading_score,
1753 heating: warm_or_firewood,
1754 potable_water: has_potable_water,
1755 building_materials: has_building_materials,
1756 aquifer: has_many_rocks,
1757 }
1758 })
1759}
1760
1761pub struct TownSiteAttributes {
1762 food_score: i32,
1763 mining_score: i32,
1764 forestry_score: i32,
1765 trading_score: i32,
1766 heating: bool,
1767 potable_water: bool,
1768 building_materials: bool,
1769 aquifer: bool,
1770}
1771
1772impl TownSiteAttributes {
1773 pub fn score(&self) -> f32 {
1774 3.0 * (self.food_score as f32 + 1.0).log2()
1775 + 2.0 * (self.forestry_score as f32 + 1.0).log2()
1776 + (self.mining_score as f32 + 1.0).log2()
1777 + (self.trading_score as f32 + 1.0).log2()
1778 }
1779}
1780
1781#[derive(Debug)]
1782pub struct Civ {
1783 capital: Id<Site>,
1784 homeland: Id<Place>,
1785}
1786
1787#[derive(Debug)]
1788pub struct Place {
1789 pub center: Vec2<i32>,
1790 }
1794
1795pub struct Track {
1796 pub cost: f32,
1800 path: Path<Vec2<i32>>,
1801}
1802
1803impl Track {
1804 pub fn path(&self) -> &Path<Vec2<i32>> { &self.path }
1805}
1806
1807#[derive(Debug)]
1808pub struct Site {
1809 pub kind: SiteKind,
1810 pub site_tmp: Option<Id<crate::site::Site>>,
1812 pub center: Vec2<i32>,
1813 pub place: Id<Place>,
1814}
1815
1816impl fmt::Display for Site {
1817 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1818 writeln!(f, "{:?}", self.kind)?;
1819
1820 Ok(())
1821 }
1822}
1823
1824impl SiteKind {
1825 pub fn is_suitable_loc(&self, loc: Vec2<i32>, sim: &WorldSim) -> bool {
1826 let on_land = || -> bool {
1827 if let Some(chunk) = sim.get(loc) {
1828 !chunk.river.is_ocean()
1829 && !chunk.river.is_lake()
1830 && !chunk.river.is_river()
1831 && !chunk.is_underwater()
1832 && !matches!(
1833 chunk.get_biome(),
1834 common::terrain::BiomeKind::Lake | common::terrain::BiomeKind::Ocean
1835 )
1836 } else {
1837 false
1838 }
1839 };
1840 let on_flat_terrain = || -> bool {
1841 sim.get_gradient_approx(loc)
1842 .map(|grad| grad < 1.0)
1843 .unwrap_or(false)
1844 };
1845
1846 sim.get(loc).is_some_and(|chunk| {
1847 let suitable_for_town = || -> bool {
1848 let attributes = town_attributes_of_site(loc, sim);
1849 attributes.is_some_and(|attributes| {
1850 (attributes.potable_water || (attributes.aquifer && matches!(self, SiteKind::CliffTown)))
1852 && attributes.building_materials
1853 && attributes.heating
1854 && on_land()
1856 })
1857 };
1858 match self {
1859 SiteKind::Gnarling => {
1860 on_land()
1861 && on_flat_terrain()
1862 && (-0.3..0.4).contains(&chunk.temp)
1863 && chunk.tree_density > 0.75
1864 },
1865 SiteKind::Adlet => chunk.temp < -0.2 && chunk.cliff_height > 25.0,
1866 SiteKind::DwarvenMine => {
1867 matches!(chunk.get_biome(), BiomeKind::Forest | BiomeKind::Desert)
1868 && !chunk.near_cliffs()
1869 && !chunk.river.near_water()
1870 && on_flat_terrain()
1871 },
1872 SiteKind::Haniwa => {
1873 on_land()
1874 && on_flat_terrain()
1875 && (-0.3..0.4).contains(&chunk.temp)
1876 },
1877 SiteKind::GiantTree => {
1878 on_land()
1879 && on_flat_terrain()
1880 && chunk.tree_density > 0.4
1881 && (-0.3..0.4).contains(&chunk.temp)
1882 },
1883 SiteKind::Citadel => true,
1884 SiteKind::CliffTown => {
1885 chunk.temp >= CONFIG.desert_temp
1886 && chunk.cliff_height > 40.0
1887 && chunk.rockiness > 1.2
1888 && suitable_for_town()
1889 },
1890 SiteKind::GliderCourse => {
1891 chunk.alt > 1400.0
1892 },
1893 SiteKind::SavannahTown => {
1894 matches!(chunk.get_biome(), BiomeKind::Savannah)
1895 && !chunk.near_cliffs()
1896 && !chunk.river.near_water()
1897 && suitable_for_town()
1898 },
1899 SiteKind::CoastalTown => {
1900 (2.0..3.5).contains(&(chunk.water_alt - CONFIG.sea_level))
1901 && suitable_for_town()
1902 },
1903 SiteKind::PirateHideout => {
1904 (0.5..3.5).contains(&(chunk.water_alt - CONFIG.sea_level))
1905 },
1906 SiteKind::Sahagin => {
1907 matches!(chunk.get_biome(), BiomeKind::Ocean)
1908 && (40.0..45.0).contains(&(CONFIG.sea_level - chunk.alt))
1909 },
1910 SiteKind::JungleRuin => {
1911 matches!(chunk.get_biome(), BiomeKind::Jungle)
1912 },
1913 SiteKind::RockCircle => !chunk.near_cliffs() && !chunk.river.near_water(),
1914 SiteKind::TrollCave => {
1915 !chunk.near_cliffs()
1916 && on_flat_terrain()
1917 && !chunk.river.near_water()
1918 && chunk.temp < 0.6
1919 },
1920 SiteKind::Camp => {
1921 !chunk.near_cliffs() && on_flat_terrain() && !chunk.river.near_water()
1922 },
1923 SiteKind::DesertCity => {
1924 (0.9..1.0).contains(&chunk.temp) && !chunk.near_cliffs() && suitable_for_town()
1925 && on_land()
1926 && !chunk.river.near_water()
1927 },
1928 SiteKind::ChapelSite => {
1929 matches!(chunk.get_biome(), BiomeKind::Ocean)
1930 && CONFIG.sea_level < chunk.alt + 1.0
1931 },
1932 SiteKind::Terracotta => {
1933 (0.9..1.0).contains(&chunk.temp)
1934 && on_land()
1935 && (chunk.water_alt - CONFIG.sea_level) > 50.0
1936 && on_flat_terrain()
1937 && !chunk.river.near_water()
1938 && !chunk.near_cliffs()
1939 },
1940 SiteKind::Myrmidon => {
1941 (0.9..1.0).contains(&chunk.temp)
1942 && on_land()
1943 && (chunk.water_alt - CONFIG.sea_level) > 50.0
1944 && on_flat_terrain()
1945 && !chunk.river.near_water()
1946 && !chunk.near_cliffs()
1947 },
1948 SiteKind::Cultist => on_land() && chunk.temp < 0.5 && chunk.near_cliffs(),
1949 SiteKind::VampireCastle => on_land() && chunk.temp <= -0.8 && chunk.near_cliffs(),
1950 SiteKind::Refactor => suitable_for_town(),
1951 SiteKind::Bridge(_, _) => true,
1952 }
1953 })
1954 }
1955
1956 pub fn exclusion_radius(&self) -> i32 {
1957 match self {
1959 SiteKind::Myrmidon => 7,
1960 _ => 8, }
1962 }
1963
1964 pub fn exclusion_radius_clear(&self, sim: &WorldSim, loc: Vec2<i32>) -> bool {
1965 let radius = self.exclusion_radius();
1966 for x in (-radius)..radius {
1967 for y in (-radius)..radius {
1968 let check_loc = loc + Vec2::new(x, y);
1969 if sim.get(check_loc).is_some_and(|c| !c.sites.is_empty()) {
1970 return false;
1971 }
1972 }
1973 }
1974 true
1975 }
1976}
1977
1978impl Site {
1979 pub fn is_dungeon(&self) -> bool {
1980 matches!(
1981 self.kind,
1982 SiteKind::Adlet
1983 | SiteKind::Gnarling
1984 | SiteKind::ChapelSite
1985 | SiteKind::Terracotta
1986 | SiteKind::Haniwa
1987 | SiteKind::Myrmidon
1988 | SiteKind::DwarvenMine
1989 | SiteKind::Cultist
1990 | SiteKind::Sahagin
1991 | SiteKind::VampireCastle
1992 )
1993 }
1994
1995 pub fn is_settlement(&self) -> bool {
1996 matches!(
1997 self.kind,
1998 SiteKind::Refactor
1999 | SiteKind::CliffTown
2000 | SiteKind::DesertCity
2001 | SiteKind::SavannahTown
2002 | SiteKind::CoastalTown
2003 )
2004 }
2005
2006 pub fn is_bridge(&self) -> bool { matches!(self.kind, SiteKind::Bridge(_, _)) }
2007}
2008
2009#[derive(PartialEq, Eq, Debug, Clone)]
2010pub struct PointOfInterest {
2011 pub name: String,
2012 pub kind: PoiKind,
2013 pub loc: Vec2<i32>,
2014}
2015
2016#[derive(PartialEq, Eq, Debug, Clone)]
2017pub enum PoiKind {
2018 Peak(u32),
2020 Biome(u32),
2022}
2023
2024#[cfg(test)]
2025mod tests {
2026 use super::*;
2027
2028 #[test]
2029 fn empty_proximity_requirements() {
2030 let world_dims = Aabr {
2031 min: Vec2 { x: 0, y: 0 },
2032 max: Vec2 {
2033 x: 200_i32,
2034 y: 200_i32,
2035 },
2036 };
2037 let reqs = ProximityRequirementsBuilder::new().finalize(&world_dims);
2038 assert!(reqs.satisfied_by(Vec2 { x: 0, y: 0 }));
2039 }
2040
2041 #[test]
2042 fn avoid_proximity_requirements() {
2043 let world_dims = Aabr {
2044 min: Vec2 {
2045 x: -200_i32,
2046 y: -200_i32,
2047 },
2048 max: Vec2 {
2049 x: 200_i32,
2050 y: 200_i32,
2051 },
2052 };
2053 let reqs = ProximityRequirementsBuilder::new()
2054 .avoid_all_of(vec![Vec2 { x: 0, y: 0 }].into_iter(), 10)
2055 .finalize(&world_dims);
2056 assert!(reqs.satisfied_by(Vec2 { x: 8, y: -8 }));
2057 assert!(!reqs.satisfied_by(Vec2 { x: -1, y: 1 }));
2058 }
2059
2060 #[test]
2061 fn near_proximity_requirements() {
2062 let world_dims = Aabr {
2063 min: Vec2 {
2064 x: -200_i32,
2065 y: -200_i32,
2066 },
2067 max: Vec2 {
2068 x: 200_i32,
2069 y: 200_i32,
2070 },
2071 };
2072 let reqs = ProximityRequirementsBuilder::new()
2073 .close_to_one_of(vec![Vec2 { x: 0, y: 0 }].into_iter(), 10)
2074 .finalize(&world_dims);
2075 assert!(reqs.satisfied_by(Vec2 { x: 1, y: -1 }));
2076 assert!(!reqs.satisfied_by(Vec2 { x: -8, y: 8 }));
2077 }
2078
2079 #[test]
2080 fn complex_proximity_requirements() {
2081 let a_site = Vec2 { x: 572, y: 724 };
2082 let world_dims = Aabr {
2083 min: Vec2 { x: 0, y: 0 },
2084 max: Vec2 {
2085 x: 1000_i32,
2086 y: 1000_i32,
2087 },
2088 };
2089 let reqs = ProximityRequirementsBuilder::new()
2090 .close_to_one_of(vec![a_site].into_iter(), 60)
2091 .avoid_all_of(vec![a_site].into_iter(), 40)
2092 .finalize(&world_dims);
2093 assert!(reqs.satisfied_by(Vec2 { x: 572, y: 774 }));
2094 assert!(!reqs.satisfied_by(a_site));
2095 }
2096
2097 #[test]
2098 fn location_hint() {
2099 let reqs = ProximityRequirementsBuilder::new().close_to_one_of(
2100 vec![Vec2 { x: 1, y: 0 }, Vec2 { x: 13, y: 12 }].into_iter(),
2101 10,
2102 );
2103 let expected = Aabr {
2104 min: Vec2 { x: 0, y: 0 },
2105 max: Vec2 { x: 23, y: 22 },
2106 };
2107 let map_dims = Aabr {
2108 min: Vec2 { x: 0, y: 0 },
2109 max: Vec2 { x: 200, y: 300 },
2110 };
2111 assert_eq!(expected, reqs.location_hint(&map_dims));
2112 }
2113}