use std::ops::Range; use hashbrown::{HashMap, HashSet}; use vek::*; use rand::prelude::*; use common::{ terrain::TerrainChunkSize, vol::RectVolSize, store::{Id, Store}, path::Path, astar::Astar, }; use crate::sim::WorldSim; const CARDINALS: [Vec2; 4] = [ Vec2::new(1, 0), Vec2::new(-1, 0), Vec2::new(0, 1), Vec2::new(0, -1), ]; const DIAGONALS: [Vec2; 8] = [ Vec2::new(1, 0), Vec2::new(1, 1), Vec2::new(-1, 0), Vec2::new(-1, 1), Vec2::new(0, 1), Vec2::new(1, -1), Vec2::new(0, -1), Vec2::new(-1, -1), ]; fn attempt(max_iters: usize, mut f: impl FnMut() -> Option) -> Option { (0..max_iters).find_map(|_| f()) } const INITIAL_CIV_COUNT: usize = 20; #[derive(Default)] pub struct Civs { civs: Store, places: Store, routes: HashMap<(Id, Id), Route>, } struct GenCtx<'a, R: Rng> { sim: &'a mut WorldSim, rng: &'a mut R, } impl Civs { pub fn generate(seed: u32, sim: &mut WorldSim) -> Self { let mut this = Self::default(); let mut rng = sim.rng.clone(); let mut ctx = GenCtx { sim, rng: &mut rng }; for _ in 0..INITIAL_CIV_COUNT { if let Some(civ) = this.birth_civ(&mut ctx) { println!("Initial civilisation: {:#?}", this.civs.get(civ)); } else { println!("Failed to find starting site"); } } // Temporary! for route in this.routes.values() { for loc in route.path.iter() { sim.get_mut(*loc).unwrap().place = Some(this.civs.iter().next().unwrap().homeland); } } this } fn birth_civ(&mut self, ctx: &mut GenCtx) -> Option> { const CIV_BIRTHPLACE_AREA: Range = 64..256; let place = attempt(5, || { let loc = find_site_loc(ctx, None)?; self.establish_place(ctx, loc, CIV_BIRTHPLACE_AREA) })?; let civ = self.civs.insert(Civ { homeland: place, }); Some(civ) } fn establish_place(&mut self, ctx: &mut GenCtx, loc: Vec2, area: Range) -> Option> { let mut dead = HashSet::new(); let mut alive = HashSet::new(); alive.insert(loc); // Fill the surrounding area while let Some(cloc) = alive.iter().choose(ctx.rng).copied() { for dir in CARDINALS.iter() { if site_in_dir(&ctx.sim, cloc, *dir) { let rloc = cloc + *dir; if !dead.contains(&rloc) && ctx.sim.get(rloc).map(|c| c.place.is_none()).unwrap_or(false) { alive.insert(rloc); } } } alive.remove(&cloc); dead.insert(cloc); if dead.len() + alive.len() >= area.end { break; } } // Make sure the place is large enough if dead.len() + alive.len() <= area.start { return None; } // Find neighbors const MAX_NEIGHBOR_DISTANCE: f32 = 100.0; let mut nearby = self.places .iter_ids() .map(|(id, p)| (id, (p.center.distance_squared(loc) as f32).sqrt())) .filter(|(p, dist)| *dist < MAX_NEIGHBOR_DISTANCE) .collect::>(); nearby.sort_by_key(|(_, dist)| -*dist as i32); let route_count = ctx.rng.gen_range(1, 3); let neighbors = nearby .into_iter() .map(|(p, _)| p) .filter_map(|p| if let Some(path) = find_path(ctx, loc, self.places.get(p).center) { Some((p, path)) } else { None }) .take(route_count) .collect::>(); let place = self.places.insert(Place { center: loc, neighbors: neighbors.iter().map(|(p, _)| *p).collect(), }); // Insert routes to neighbours into route list for (p, path) in neighbors { self.routes.insert((place, p), Route { path }); } // Write place to map for cell in dead.union(&alive) { if let Some(chunk) = ctx.sim.get_mut(*cell) { chunk.place = Some(place); } } Some(place) } } /// Attempt to find a path between two locations fn find_path(ctx: &mut GenCtx, a: Vec2, b: Vec2) -> Option>> { let sim = &ctx.sim; let heuristic = move |l: &Vec2| (l.distance_squared(b) as f32).sqrt(); let neighbors = |l: &Vec2| { let l = *l; DIAGONALS.iter().filter(move |dir| walk_in_dir(sim, l, **dir).is_some()).map(move |dir| l + *dir) }; let transition = |a: &Vec2, b: &Vec2| 1.0 + walk_in_dir(sim, *a, *b - *a).unwrap_or(10000.0); let satisfied = |l: &Vec2| *l == b; Astar::new(5000, a, heuristic) .poll(5000, heuristic, neighbors, transition, satisfied) .into_path() } /// Return true if travel between a location and a chunk next to it is permitted (TODO: by whom?) fn walk_in_dir(sim: &WorldSim, a: Vec2, dir: Vec2) -> Option { if loc_suitable_for_walking(sim, a) && loc_suitable_for_walking(sim, a + dir) { let a_alt = sim.get(a)?.alt; let b_alt = sim.get(a + dir)?.alt; Some((b_alt - a_alt).max(0.0).powf(2.0).abs() / 50.0) } else { None } } /// Return true if a position is suitable for walking on fn loc_suitable_for_walking(sim: &WorldSim, loc: Vec2) -> bool { if let Some(chunk) = sim.get(loc) { !chunk.river.is_ocean() && !chunk.river.is_lake() } else { false } } /// Return true if a site could be constructed between a location and a chunk next to it is permitted (TODO: by whom?) fn site_in_dir(sim: &WorldSim, a: Vec2, dir: Vec2) -> bool { loc_suitable_for_site(sim, a) && loc_suitable_for_site(sim, a + dir) } /// Return true if a position is suitable for site construction (TODO: criteria?) fn loc_suitable_for_site(sim: &WorldSim, loc: Vec2) -> bool { if let Some(chunk) = sim.get(loc) { !chunk.is_underwater() && sim.get_gradient_approx(loc).map(|grad| grad < 1.0).unwrap_or(false) } else { false } } /// Attempt to search for a location that's suitable for site construction fn find_site_loc(ctx: &mut GenCtx, near: Option<(Vec2, f32)>) -> Option> { const MAX_ATTEMPTS: usize = 100; let mut loc = None; for _ in 0..MAX_ATTEMPTS { let test_loc = loc.unwrap_or_else(|| match near { Some((origin, dist)) => origin + (Vec2::new( ctx.rng.gen_range(-1.0, 1.0), ctx.rng.gen_range(-1.0, 1.0), ).try_normalized().unwrap_or(Vec2::zero()) * ctx.rng.gen::() * dist).map(|e| e as i32), None => Vec2::new( ctx.rng.gen_range(0, ctx.sim.get_size().x as i32), ctx.rng.gen_range(0, ctx.sim.get_size().y as i32), ), }); if loc_suitable_for_site(&ctx.sim, test_loc) { return Some(test_loc); } loc = ctx.sim.get(test_loc).and_then(|c| Some(c.downhill?.map2(Vec2::from(TerrainChunkSize::RECT_SIZE), |e, sz: u32| { e / (sz as i32) }))); } None } #[derive(Debug)] pub struct Civ { homeland: Id, } pub struct Place { center: Vec2, neighbors: Vec>, } pub struct Route { path: Path>, }