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