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