Skip to main content

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