veloren_world/civ/
mod.rs

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