mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
1348 lines
52 KiB
Rust
1348 lines
52 KiB
Rust
#![allow(dead_code)]
|
||
|
||
mod econ;
|
||
|
||
use crate::{
|
||
config::CONFIG,
|
||
sim::WorldSim,
|
||
site::{namegen::NameGen, Castle, Settlement, Site as WorldSite, Tree},
|
||
site2,
|
||
util::{attempt, seed_expan, DHashMap, DHashSet, NEIGHBORS},
|
||
Index, Land,
|
||
};
|
||
use common::{
|
||
astar::Astar,
|
||
path::Path,
|
||
spiral::Spiral2d,
|
||
store::{Id, Store},
|
||
terrain::{uniform_idx_as_vec2, MapSizeLg, TerrainChunkSize, TERRAIN_CHUNK_BLOCKS_LG},
|
||
vol::RectVolSize,
|
||
};
|
||
use core::{fmt, hash::BuildHasherDefault, ops::Range};
|
||
use fxhash::FxHasher64;
|
||
use rand::prelude::*;
|
||
use rand_chacha::ChaChaRng;
|
||
use tracing::{debug, info, warn};
|
||
use vek::*;
|
||
|
||
const fn initial_civ_count(map_size_lg: MapSizeLg) -> u32 {
|
||
// NOTE: since map_size_lg's dimensions must fit in a u16, we can safely add
|
||
// them here.
|
||
//
|
||
// NOTE: 48 at "default" scale of 10 × 10 chunk bits (1024 × 1024 chunks).
|
||
(3 << (map_size_lg.vec().x + map_size_lg.vec().y)) >> 16
|
||
}
|
||
|
||
pub struct CaveInfo {
|
||
pub location: (Vec2<i32>, Vec2<i32>),
|
||
pub name: String,
|
||
}
|
||
|
||
#[derive(Default)]
|
||
pub struct Civs {
|
||
pub civs: Store<Civ>,
|
||
pub places: Store<Place>,
|
||
pub pois: Store<PointOfInterest>,
|
||
|
||
pub tracks: Store<Track>,
|
||
/// We use this hasher (FxHasher64) because
|
||
/// (1) we don't care about DDOS attacks (ruling out SipHash);
|
||
/// (2) we care about determinism across computers (ruling out AAHash);
|
||
/// (3) we have 8-byte keys (for which FxHash is fastest).
|
||
pub track_map: DHashMap<Id<Site>, DHashMap<Id<Site>, Id<Track>>>,
|
||
|
||
pub sites: Store<Site>,
|
||
pub caves: Store<CaveInfo>,
|
||
}
|
||
|
||
// Change this to get rid of particularly horrid seeds
|
||
const SEED_SKIP: u8 = 5;
|
||
const POI_THINNING_DIST_SQRD: i32 = 300;
|
||
|
||
pub struct GenCtx<'a, R: Rng> {
|
||
sim: &'a mut WorldSim,
|
||
rng: R,
|
||
}
|
||
|
||
impl<'a, R: Rng> GenCtx<'a, R> {
|
||
pub fn reseed(&mut self) -> GenCtx<'_, impl Rng> {
|
||
let mut entropy = self.rng.gen::<[u8; 32]>();
|
||
entropy[0] = entropy[0].wrapping_add(SEED_SKIP); // Skip bad seeds
|
||
GenCtx {
|
||
sim: self.sim,
|
||
rng: ChaChaRng::from_seed(entropy),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Civs {
|
||
pub fn generate(seed: u32, sim: &mut WorldSim, index: &mut Index) -> Self {
|
||
let mut this = Self::default();
|
||
let rng = ChaChaRng::from_seed(seed_expan::rng_state(seed));
|
||
let initial_civ_count = initial_civ_count(sim.map_size_lg());
|
||
let mut ctx = GenCtx { sim, rng };
|
||
info!("starting peak naming");
|
||
this.name_peaks(&mut ctx);
|
||
info!("starting biome naming");
|
||
this.name_biomes(&mut ctx);
|
||
|
||
for _ in 0..ctx.sim.get_size().product() / 10_000 {
|
||
this.generate_cave(&mut ctx);
|
||
}
|
||
|
||
let mut start_locations: Vec<Vec2<i32>> = Vec::new();
|
||
for _ in 0..initial_civ_count {
|
||
debug!("Creating civilisation...");
|
||
if this
|
||
.birth_civ(&mut ctx.reseed(), &mut start_locations)
|
||
.is_none()
|
||
{
|
||
warn!("Failed to find starting site for civilisation.");
|
||
}
|
||
}
|
||
info!(?initial_civ_count, "all civilisations created");
|
||
|
||
let mut gnarling_enemies: Vec<Vec2<i32>> = start_locations.clone();
|
||
let mut dungeon_enemies: Vec<Vec2<i32>> = start_locations.clone();
|
||
let mut tree_enemies: Vec<Vec2<i32>> = start_locations.clone();
|
||
let mut castle_enemies: Vec<Vec2<i32>> = Vec::new();
|
||
for _ in 0..initial_civ_count * 3 {
|
||
attempt(5, || {
|
||
let (kind, size, avoid) = match ctx.rng.gen_range(0..64) {
|
||
0..=5 => (SiteKind::Castle, 3, (&castle_enemies, 40)),
|
||
28..=31 => {
|
||
if index.features().site2_giant_trees {
|
||
(SiteKind::GiantTree, 4, (&tree_enemies, 40))
|
||
} else {
|
||
(SiteKind::Tree, 4, (&tree_enemies, 40))
|
||
}
|
||
},
|
||
32..=37 => (SiteKind::Gnarling, 5, (&gnarling_enemies, 40)),
|
||
_ => (SiteKind::Dungeon, 0, (&dungeon_enemies, 40)),
|
||
};
|
||
let loc = find_site_loc(&mut ctx, avoid, size, kind)?;
|
||
match kind {
|
||
SiteKind::Castle => {
|
||
gnarling_enemies.push(loc);
|
||
dungeon_enemies.push(loc);
|
||
tree_enemies.push(loc);
|
||
castle_enemies.push(loc);
|
||
},
|
||
SiteKind::Gnarling => {
|
||
castle_enemies.push(loc);
|
||
dungeon_enemies.push(loc);
|
||
gnarling_enemies.push(loc);
|
||
},
|
||
SiteKind::Dungeon => {
|
||
gnarling_enemies.push(loc);
|
||
dungeon_enemies.push(loc);
|
||
castle_enemies.push(loc);
|
||
},
|
||
_ => (),
|
||
}
|
||
Some(this.establish_site(&mut ctx.reseed(), loc, |place| Site {
|
||
kind,
|
||
center: loc,
|
||
place,
|
||
site_tmp: None,
|
||
}))
|
||
});
|
||
}
|
||
|
||
// Tick
|
||
//=== old economy is gone
|
||
|
||
// Flatten ground around sites
|
||
for site in this.sites.values() {
|
||
let wpos = site.center * TerrainChunkSize::RECT_SIZE.map(|e: u32| e as i32);
|
||
|
||
let (radius, flatten_radius) = match &site.kind {
|
||
SiteKind::Settlement => (32i32, 10.0),
|
||
SiteKind::Dungeon => (8i32, 3.0),
|
||
SiteKind::Castle => (16i32, 5.0),
|
||
SiteKind::Refactor => (32i32, 10.0),
|
||
SiteKind::CliffTown => (32i32, 10.0),
|
||
SiteKind::Tree => (12i32, 8.0),
|
||
SiteKind::GiantTree => (12i32, 8.0),
|
||
SiteKind::Gnarling => (16i32, 10.0),
|
||
};
|
||
|
||
let (raise, raise_dist, make_waypoint): (f32, i32, bool) = match &site.kind {
|
||
SiteKind::Settlement => (10.0, 6, true),
|
||
SiteKind::Castle => (0.0, 6, true),
|
||
_ => (0.0, 0, false),
|
||
};
|
||
|
||
// Flatten ground
|
||
if let Some(center_alt) = ctx.sim.get_alt_approx(wpos) {
|
||
for offs in Spiral2d::new().take(radius.pow(2) as usize) {
|
||
let center_alt = center_alt
|
||
+ if offs.magnitude_squared() <= raise_dist.pow(2) {
|
||
raise
|
||
} else {
|
||
0.0
|
||
}; // Raise the town centre up a little
|
||
let pos = site.center + offs;
|
||
let factor = ((1.0
|
||
- (site.center - pos).map(|e| e as f32).magnitude() / flatten_radius)
|
||
* 1.25)
|
||
.min(1.0);
|
||
let rng = &mut ctx.rng;
|
||
ctx.sim
|
||
.get_mut(pos)
|
||
// Don't disrupt chunks that are near water
|
||
.filter(|chunk| !chunk.river.near_water())
|
||
.map(|chunk| {
|
||
let diff = Lerp::lerp_precise(chunk.alt, center_alt, factor) - chunk.alt;
|
||
// Make sure we don't fall below sea level (fortunately, we don't have
|
||
// to worry about the case where water_alt is already set to a correct
|
||
// value higher than alt, since this chunk should have been filtered
|
||
// out in that case).
|
||
chunk.water_alt = CONFIG.sea_level.max(chunk.water_alt + diff);
|
||
chunk.alt += diff;
|
||
chunk.basement += diff;
|
||
chunk.rockiness = 0.0;
|
||
chunk.surface_veg *= 1.0 - factor * rng.gen_range(0.25..0.9);
|
||
|
||
if make_waypoint && offs == Vec2::zero() {
|
||
chunk.contains_waypoint = true;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// Place sites in world
|
||
let mut cnt = 0;
|
||
for sim_site in this.sites.values_mut() {
|
||
cnt += 1;
|
||
let wpos = sim_site
|
||
.center
|
||
.map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| {
|
||
e * sz as i32 + sz as i32 / 2
|
||
});
|
||
|
||
let mut rng = ctx.reseed().rng;
|
||
let site = index.sites.insert(match &sim_site.kind {
|
||
SiteKind::Settlement => {
|
||
WorldSite::settlement(Settlement::generate(wpos, Some(ctx.sim), &mut rng))
|
||
},
|
||
SiteKind::Dungeon => WorldSite::dungeon(site2::Site::generate_dungeon(
|
||
&Land::from_sim(ctx.sim),
|
||
&mut rng,
|
||
wpos,
|
||
)),
|
||
SiteKind::Castle => {
|
||
WorldSite::castle(Castle::generate(wpos, Some(ctx.sim), &mut rng))
|
||
},
|
||
SiteKind::Refactor => WorldSite::refactor(site2::Site::generate_city(
|
||
&Land::from_sim(ctx.sim),
|
||
&mut rng,
|
||
wpos,
|
||
)),
|
||
SiteKind::CliffTown => WorldSite::cliff_town(site2::Site::generate_cliff_town(
|
||
&Land::from_sim(ctx.sim),
|
||
&mut rng,
|
||
wpos,
|
||
)),
|
||
SiteKind::Tree => {
|
||
WorldSite::tree(Tree::generate(wpos, &Land::from_sim(ctx.sim), &mut rng))
|
||
},
|
||
SiteKind::GiantTree => WorldSite::giant_tree(site2::Site::generate_giant_tree(
|
||
&Land::from_sim(ctx.sim),
|
||
&mut rng,
|
||
wpos,
|
||
)),
|
||
SiteKind::Gnarling => WorldSite::gnarling(site2::Site::generate_gnarling(
|
||
&Land::from_sim(ctx.sim),
|
||
&mut rng,
|
||
wpos,
|
||
)),
|
||
});
|
||
sim_site.site_tmp = Some(site);
|
||
let site_ref = &index.sites[site];
|
||
|
||
let radius_chunks =
|
||
(site_ref.radius() / TerrainChunkSize::RECT_SIZE.x as f32).ceil() as usize;
|
||
for pos in Spiral2d::new()
|
||
.map(|offs| sim_site.center + offs)
|
||
.take((radius_chunks * 2).pow(2))
|
||
{
|
||
ctx.sim.get_mut(pos).map(|chunk| chunk.sites.push(site));
|
||
}
|
||
debug!(?sim_site.center, "Placed site at location");
|
||
}
|
||
info!(?cnt, "all sites placed");
|
||
|
||
//this.display_info();
|
||
|
||
// remember neighbor information in economy
|
||
for (s1, val) in this.track_map.iter() {
|
||
if let Some(index1) = this.sites.get(*s1).site_tmp {
|
||
for (s2, t) in val.iter() {
|
||
if let Some(index2) = this.sites.get(*s2).site_tmp {
|
||
if index.sites.get(index1).do_economic_simulation()
|
||
&& index.sites.get(index2).do_economic_simulation()
|
||
{
|
||
let cost = this.tracks.get(*t).path.len();
|
||
index
|
||
.sites
|
||
.get_mut(index1)
|
||
.economy
|
||
.add_neighbor(index2, cost);
|
||
index
|
||
.sites
|
||
.get_mut(index2)
|
||
.economy
|
||
.add_neighbor(index1, cost);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// collect natural resources
|
||
let sites = &mut index.sites;
|
||
(0..ctx.sim.map_size_lg().chunks_len())
|
||
.into_iter()
|
||
.for_each(|posi| {
|
||
let chpos = uniform_idx_as_vec2(ctx.sim.map_size_lg(), posi);
|
||
let wpos = chpos.map(|e| e as i64) * TerrainChunkSize::RECT_SIZE.map(|e| e as i64);
|
||
let closest_site = (*sites)
|
||
.iter_mut()
|
||
.filter(|s| !matches!(s.1.kind, crate::site::SiteKind::Dungeon(_)))
|
||
.min_by_key(|(_id, s)| s.get_origin().map(|e| e as i64).distance_squared(wpos));
|
||
if let Some((_id, s)) = closest_site {
|
||
let distance_squared = s.get_origin().map(|e| e as i64).distance_squared(wpos);
|
||
s.economy
|
||
.add_chunk(ctx.sim.get(chpos).unwrap(), distance_squared);
|
||
}
|
||
});
|
||
sites
|
||
.iter_mut()
|
||
.for_each(|(_, s)| s.economy.cache_economy());
|
||
|
||
this
|
||
}
|
||
|
||
// TODO: Move this
|
||
fn generate_cave(&mut self, ctx: &mut GenCtx<impl Rng>) {
|
||
let mut pos = ctx
|
||
.sim
|
||
.get_size()
|
||
.map(|sz| ctx.rng.gen_range(0..sz as i32) as f32);
|
||
let mut vel = pos
|
||
.map2(ctx.sim.get_size(), |pos, sz| sz as f32 / 2.0 - pos)
|
||
.try_normalized()
|
||
.unwrap_or_else(Vec2::unit_y);
|
||
|
||
let path = (-100..100)
|
||
.filter_map(|i: i32| {
|
||
let depth = (i.abs() as f32 / 100.0 * std::f32::consts::PI / 2.0).cos();
|
||
vel = (vel
|
||
+ Vec2::new(
|
||
ctx.rng.gen_range(-0.35..0.35),
|
||
ctx.rng.gen_range(-0.35..0.35),
|
||
))
|
||
.try_normalized()
|
||
.unwrap_or_else(Vec2::unit_y);
|
||
let old_pos = pos.map(|e| e as i32);
|
||
pos = (pos + vel * 0.5)
|
||
.clamped(Vec2::zero(), ctx.sim.get_size().map(|e| e as f32 - 1.0));
|
||
Some((pos.map(|e| e as i32), depth)).filter(|(pos, _)| *pos != old_pos)
|
||
})
|
||
.collect::<Vec<_>>();
|
||
|
||
for locs in path.windows(3) {
|
||
let to_prev_idx = NEIGHBORS
|
||
.iter()
|
||
.enumerate()
|
||
.find(|(_, dir)| **dir == locs[0].0 - locs[1].0)
|
||
.expect("Track locations must be neighbors")
|
||
.0;
|
||
let to_next_idx = NEIGHBORS
|
||
.iter()
|
||
.enumerate()
|
||
.find(|(_, dir)| **dir == locs[2].0 - locs[1].0)
|
||
.expect("Track locations must be neighbors")
|
||
.0;
|
||
|
||
ctx.sim.get_mut(locs[0].0).unwrap().cave.0.neighbors |=
|
||
1 << ((to_prev_idx as u8 + 4) % 8);
|
||
ctx.sim.get_mut(locs[1].0).unwrap().cave.0.neighbors |=
|
||
(1 << (to_prev_idx as u8)) | (1 << (to_next_idx as u8));
|
||
ctx.sim.get_mut(locs[2].0).unwrap().cave.0.neighbors |=
|
||
1 << ((to_next_idx as u8 + 4) % 8);
|
||
}
|
||
|
||
for loc in path.iter() {
|
||
let mut chunk = ctx.sim.get_mut(loc.0).unwrap();
|
||
let depth = loc.1 * 250.0 - 20.0;
|
||
chunk.cave.1.alt =
|
||
chunk.alt - depth + ctx.rng.gen_range(-4.0..4.0) * (depth > 10.0) as i32 as f32;
|
||
chunk.cave.1.width = ctx.rng.gen_range(6.0..32.0);
|
||
chunk.cave.0.offset = Vec2::new(ctx.rng.gen_range(-16..17), ctx.rng.gen_range(-16..17));
|
||
|
||
if chunk.cave.1.alt + chunk.cave.1.width + 5.0 > chunk.alt {
|
||
chunk.spawn_rate = 0.0;
|
||
}
|
||
}
|
||
self.caves.insert(CaveInfo {
|
||
location: (
|
||
path.first().unwrap().0 * TerrainChunkSize::RECT_SIZE.map(|e: u32| e as i32),
|
||
path.last().unwrap().0 * TerrainChunkSize::RECT_SIZE.map(|e: u32| e as i32),
|
||
),
|
||
name: {
|
||
let name = NameGen::location(&mut ctx.rng).generate();
|
||
match ctx.rng.gen_range(0..7) {
|
||
0 => format!("{} Hole", name),
|
||
1 => format!("{} Cavern", name),
|
||
2 => format!("{} Hollow", name),
|
||
3 => format!("{} Tunnel", name),
|
||
4 => format!("{} Mouth", name),
|
||
5 => format!("{} Grotto", name),
|
||
_ => format!("{} Den", name),
|
||
}
|
||
},
|
||
});
|
||
}
|
||
|
||
pub fn place(&self, id: Id<Place>) -> &Place { self.places.get(id) }
|
||
|
||
pub fn sites(&self) -> impl Iterator<Item = &Site> + '_ { self.sites.values() }
|
||
|
||
#[allow(dead_code)]
|
||
fn display_info(&self) {
|
||
for (id, civ) in self.civs.iter() {
|
||
println!("# Civilisation {:?}", id);
|
||
println!("Name: <unnamed>");
|
||
println!("Homeland: {:#?}", self.places.get(civ.homeland));
|
||
}
|
||
|
||
for (id, site) in self.sites.iter() {
|
||
println!("# Site {:?}", id);
|
||
println!("{:#?}", site);
|
||
}
|
||
}
|
||
|
||
/// Return the direct track between two places
|
||
pub fn track_between(&self, a: Id<Site>, b: Id<Site>) -> Option<Id<Track>> {
|
||
self.track_map
|
||
.get(&a)
|
||
.and_then(|dests| dests.get(&b))
|
||
.or_else(|| self.track_map.get(&b).and_then(|dests| dests.get(&a)))
|
||
.copied()
|
||
}
|
||
|
||
/// Return an iterator over a site's neighbors
|
||
pub fn neighbors(&self, site: Id<Site>) -> impl Iterator<Item = Id<Site>> + '_ {
|
||
let to = self
|
||
.track_map
|
||
.get(&site)
|
||
.map(|dests| dests.keys())
|
||
.into_iter()
|
||
.flatten();
|
||
let fro = self
|
||
.track_map
|
||
.iter()
|
||
.filter(move |(_, dests)| dests.contains_key(&site))
|
||
.map(|(p, _)| p);
|
||
to.chain(fro).filter(move |p| **p != site).copied()
|
||
}
|
||
|
||
/// Find the cheapest route between two places
|
||
fn route_between(&self, a: Id<Site>, b: Id<Site>) -> Option<(Path<Id<Site>>, f32)> {
|
||
let heuristic = move |p: &Id<Site>| {
|
||
(self
|
||
.sites
|
||
.get(*p)
|
||
.center
|
||
.distance_squared(self.sites.get(b).center) as f32)
|
||
.sqrt()
|
||
};
|
||
let neighbors = |p: &Id<Site>| self.neighbors(*p);
|
||
let transition =
|
||
|a: &Id<Site>, b: &Id<Site>| self.tracks.get(self.track_between(*a, *b).unwrap()).cost;
|
||
let satisfied = |p: &Id<Site>| *p == b;
|
||
// We use this hasher (FxHasher64) because
|
||
// (1) we don't care about DDOS attacks (ruling out SipHash);
|
||
// (2) we care about determinism across computers (ruling out AAHash);
|
||
// (3) we have 8-byte keys (for which FxHash is fastest).
|
||
let mut astar = Astar::new(
|
||
100,
|
||
a,
|
||
heuristic,
|
||
BuildHasherDefault::<FxHasher64>::default(),
|
||
);
|
||
astar
|
||
.poll(100, heuristic, neighbors, transition, satisfied)
|
||
.into_path()
|
||
.and_then(|path| astar.get_cheapest_cost().map(|cost| (path, cost)))
|
||
}
|
||
|
||
fn birth_civ(
|
||
&mut self,
|
||
ctx: &mut GenCtx<impl Rng>,
|
||
start_locations: &mut Vec<Vec2<i32>>,
|
||
) -> Option<Id<Civ>> {
|
||
// TODO: specify SiteKind based on where a suitable location is found
|
||
let kind = match ctx.rng.gen_range(0..64) {
|
||
0..=10 => SiteKind::CliffTown,
|
||
_ => SiteKind::Refactor,
|
||
};
|
||
let site = attempt(100, || {
|
||
let loc = find_site_loc(ctx, (start_locations, 60), 0, kind)?;
|
||
start_locations.push(loc);
|
||
Some(self.establish_site(ctx, loc, |place| Site {
|
||
kind,
|
||
site_tmp: None,
|
||
center: loc,
|
||
place,
|
||
/* most economic members have moved to site/Economy */
|
||
/* last_exports: Stocks::from_default(0.0),
|
||
* export_targets: Stocks::from_default(0.0),
|
||
* //trade_states: Stocks::default(), */
|
||
}))
|
||
})?;
|
||
|
||
let civ = self.civs.insert(Civ {
|
||
capital: site,
|
||
homeland: self.sites.get(site).place,
|
||
});
|
||
|
||
Some(civ)
|
||
}
|
||
|
||
fn establish_place(
|
||
&mut self,
|
||
_ctx: &mut GenCtx<impl Rng>,
|
||
loc: Vec2<i32>,
|
||
_area: Range<usize>,
|
||
) -> Id<Place> {
|
||
self.places.insert(Place { center: loc })
|
||
}
|
||
|
||
/// Adds lake POIs and names them
|
||
fn name_biomes(&mut self, ctx: &mut GenCtx<impl Rng>) {
|
||
let map_size_lg = ctx.sim.map_size_lg();
|
||
let mut biomes: Vec<(common::terrain::BiomeKind, Vec<usize>)> = Vec::new();
|
||
let mut explored = DHashSet::default();
|
||
let mut to_explore = DHashSet::default();
|
||
let mut to_floodfill = DHashSet::default();
|
||
// TODO: have start point in center and ignore ocean?
|
||
let start_point = 0;
|
||
to_explore.insert(start_point);
|
||
explored.insert(start_point);
|
||
|
||
while !to_explore.is_empty() {
|
||
let exploring = *to_explore.iter().next().unwrap();
|
||
to_explore.remove(&exploring);
|
||
to_floodfill.insert(exploring);
|
||
// Should always be a chunk on the map
|
||
let biome = ctx.sim.chunks[exploring].get_biome();
|
||
biomes.push((biome, Vec::new()));
|
||
while !to_floodfill.is_empty() {
|
||
let filling = *to_floodfill.iter().next().unwrap();
|
||
to_explore.remove(&filling);
|
||
to_floodfill.remove(&filling);
|
||
explored.insert(filling);
|
||
biomes.last_mut().unwrap().1.push(filling);
|
||
for neighbour in common::terrain::neighbors(map_size_lg, filling) {
|
||
if explored.contains(&neighbour) {
|
||
continue;
|
||
}
|
||
let n_biome = ctx.sim.chunks[neighbour].get_biome();
|
||
if n_biome == biome {
|
||
to_floodfill.insert(neighbour);
|
||
} else {
|
||
to_explore.insert(neighbour);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
let mut biome_count = 0;
|
||
for biome in biomes {
|
||
let name = match biome.0 {
|
||
common::terrain::BiomeKind::Lake if biome.1.len() as u32 > 200 => Some(format!(
|
||
"{} {}",
|
||
["Lake", "Loch"].choose(&mut ctx.rng).unwrap(),
|
||
NameGen::location(&mut ctx.rng).generate_lake_custom()
|
||
)),
|
||
common::terrain::BiomeKind::Lake if biome.1.len() as u32 > 10 => Some(format!(
|
||
"{} {}",
|
||
NameGen::location(&mut ctx.rng).generate_lake_custom(),
|
||
["Pool", "Well", "Pond"].choose(&mut ctx.rng).unwrap()
|
||
)),
|
||
common::terrain::BiomeKind::Grassland if biome.1.len() as u32 > 750 => {
|
||
Some(format!(
|
||
"{} {}",
|
||
[
|
||
NameGen::location(&mut ctx.rng).generate_grassland_engl(),
|
||
NameGen::location(&mut ctx.rng).generate_grassland_custom()
|
||
]
|
||
.choose(&mut ctx.rng)
|
||
.unwrap(),
|
||
[
|
||
"Grasslands",
|
||
"Plains",
|
||
"Meadows",
|
||
"Fields",
|
||
"Heath",
|
||
"Hills",
|
||
"Prairie",
|
||
"Lowlands",
|
||
"Steppe",
|
||
"Downs",
|
||
"Greens",
|
||
]
|
||
.choose(&mut ctx.rng)
|
||
.unwrap()
|
||
))
|
||
},
|
||
common::terrain::BiomeKind::Ocean if biome.1.len() as u32 > 750 => Some(format!(
|
||
"{} {}",
|
||
[
|
||
NameGen::location(&mut ctx.rng).generate_ocean_engl(),
|
||
NameGen::location(&mut ctx.rng).generate_ocean_custom()
|
||
]
|
||
.choose(&mut ctx.rng)
|
||
.unwrap(),
|
||
["Sea", "Bay", "Gulf", "Deep", "Depths", "Ocean", "Blue",]
|
||
.choose(&mut ctx.rng)
|
||
.unwrap()
|
||
)),
|
||
common::terrain::BiomeKind::Mountain if biome.1.len() as u32 > 750 => {
|
||
Some(format!(
|
||
"{} {}",
|
||
[
|
||
NameGen::location(&mut ctx.rng).generate_mountain_engl(),
|
||
NameGen::location(&mut ctx.rng).generate_mountain_custom()
|
||
]
|
||
.choose(&mut ctx.rng)
|
||
.unwrap(),
|
||
[
|
||
"Mountains",
|
||
"Range",
|
||
"Reach",
|
||
"Massif",
|
||
"Rocks",
|
||
"Cliffs",
|
||
"Peaks",
|
||
"Heights",
|
||
"Bluffs",
|
||
"Ridge",
|
||
"Canyon",
|
||
"Plateau",
|
||
]
|
||
.choose(&mut ctx.rng)
|
||
.unwrap()
|
||
))
|
||
},
|
||
common::terrain::BiomeKind::Snowland if biome.1.len() as u32 > 750 => {
|
||
Some(format!(
|
||
"{} {}",
|
||
[
|
||
NameGen::location(&mut ctx.rng).generate_snowland_engl(),
|
||
NameGen::location(&mut ctx.rng).generate_snowland_custom()
|
||
]
|
||
.choose(&mut ctx.rng)
|
||
.unwrap(),
|
||
[
|
||
"Snowlands",
|
||
"Glacier",
|
||
"Tundra",
|
||
"Drifts",
|
||
"Snowfields",
|
||
"Hills",
|
||
"Downs",
|
||
"Uplands",
|
||
"Highlands",
|
||
]
|
||
.choose(&mut ctx.rng)
|
||
.unwrap()
|
||
))
|
||
},
|
||
common::terrain::BiomeKind::Desert if biome.1.len() as u32 > 750 => Some(format!(
|
||
"{} {}",
|
||
[
|
||
NameGen::location(&mut ctx.rng).generate_desert_engl(),
|
||
NameGen::location(&mut ctx.rng).generate_desert_custom()
|
||
]
|
||
.choose(&mut ctx.rng)
|
||
.unwrap(),
|
||
[
|
||
"Desert", "Sands", "Sandsea", "Drifts", "Dunes", "Droughts", "Flats",
|
||
]
|
||
.choose(&mut ctx.rng)
|
||
.unwrap()
|
||
)),
|
||
common::terrain::BiomeKind::Swamp if biome.1.len() as u32 > 200 => Some(format!(
|
||
"{} {}",
|
||
NameGen::location(&mut ctx.rng).generate_swamp_engl(),
|
||
[
|
||
"Swamp",
|
||
"Swamps",
|
||
"Swamplands",
|
||
"Marsh",
|
||
"Marshlands",
|
||
"Morass",
|
||
"Mire",
|
||
"Bog",
|
||
"Wetlands",
|
||
"Fen",
|
||
"Moors",
|
||
]
|
||
.choose(&mut ctx.rng)
|
||
.unwrap()
|
||
)),
|
||
common::terrain::BiomeKind::Jungle if biome.1.len() as u32 > 85 => Some(format!(
|
||
"{} {}",
|
||
[
|
||
NameGen::location(&mut ctx.rng).generate_jungle_engl(),
|
||
NameGen::location(&mut ctx.rng).generate_jungle_custom()
|
||
]
|
||
.choose(&mut ctx.rng)
|
||
.unwrap(),
|
||
[
|
||
"Jungle",
|
||
"Rainforest",
|
||
"Greatwood",
|
||
"Wilds",
|
||
"Wildwood",
|
||
"Tangle",
|
||
"Tanglewood",
|
||
"Bush",
|
||
]
|
||
.choose(&mut ctx.rng)
|
||
.unwrap()
|
||
)),
|
||
common::terrain::BiomeKind::Forest if biome.1.len() as u32 > 750 => Some(format!(
|
||
"{} {}",
|
||
[
|
||
NameGen::location(&mut ctx.rng).generate_forest_engl(),
|
||
NameGen::location(&mut ctx.rng).generate_forest_custom()
|
||
]
|
||
.choose(&mut ctx.rng)
|
||
.unwrap(),
|
||
["Forest", "Woodlands", "Woods", "Glades", "Grove", "Weald",]
|
||
.choose(&mut ctx.rng)
|
||
.unwrap()
|
||
)),
|
||
common::terrain::BiomeKind::Savannah if biome.1.len() as u32 > 750 => {
|
||
Some(format!(
|
||
"{} {}",
|
||
[
|
||
NameGen::location(&mut ctx.rng).generate_savannah_engl(),
|
||
NameGen::location(&mut ctx.rng).generate_savannah_custom()
|
||
]
|
||
.choose(&mut ctx.rng)
|
||
.unwrap(),
|
||
[
|
||
"Savannah",
|
||
"Shrublands",
|
||
"Sierra",
|
||
"Prairie",
|
||
"Lowlands",
|
||
"Flats",
|
||
]
|
||
.choose(&mut ctx.rng)
|
||
.unwrap()
|
||
))
|
||
},
|
||
common::terrain::BiomeKind::Taiga if biome.1.len() as u32 > 750 => Some(format!(
|
||
"{} {}",
|
||
[
|
||
NameGen::location(&mut ctx.rng).generate_taiga_engl(),
|
||
NameGen::location(&mut ctx.rng).generate_taiga_custom()
|
||
]
|
||
.choose(&mut ctx.rng)
|
||
.unwrap(),
|
||
[
|
||
"Forest",
|
||
"Woodlands",
|
||
"Woods",
|
||
"Timberlands",
|
||
"Highlands",
|
||
"Uplands",
|
||
]
|
||
.choose(&mut ctx.rng)
|
||
.unwrap()
|
||
)),
|
||
_ => None,
|
||
};
|
||
if let Some(name) = name {
|
||
// find average center of the biome
|
||
let center = biome
|
||
.1
|
||
.iter()
|
||
.map(|b| uniform_idx_as_vec2(map_size_lg, *b))
|
||
.sum::<Vec2<i32>>()
|
||
/ biome.1.len() as i32;
|
||
// Select the point closest to the center
|
||
let idx = *biome
|
||
.1
|
||
.iter()
|
||
.min_by_key(|&b| center.distance_squared(uniform_idx_as_vec2(map_size_lg, *b)))
|
||
.unwrap();
|
||
let id = self.pois.insert(PointOfInterest {
|
||
name,
|
||
loc: uniform_idx_as_vec2(map_size_lg, idx),
|
||
kind: PoiKind::Biome(biome.1.len() as u32),
|
||
});
|
||
for chunk in biome.1 {
|
||
ctx.sim.chunks[chunk].poi = Some(id);
|
||
}
|
||
biome_count += 1;
|
||
}
|
||
}
|
||
|
||
info!(?biome_count, "all biomes named");
|
||
}
|
||
|
||
/// Adds mountain POIs and name them
|
||
fn name_peaks(&mut self, ctx: &mut GenCtx<impl Rng>) {
|
||
let map_size_lg = ctx.sim.map_size_lg();
|
||
const MIN_MOUNTAIN_ALT: f32 = 600.0;
|
||
const MIN_MOUNTAIN_CHAOS: f32 = 0.35;
|
||
let rng = &mut ctx.rng;
|
||
let sim_chunks = &ctx.sim.chunks;
|
||
let peaks = sim_chunks
|
||
.iter()
|
||
.enumerate()
|
||
.filter(|(posi, chunk)| {
|
||
let neighbor_alts_max = common::terrain::neighbors(map_size_lg, *posi)
|
||
.map(|i| sim_chunks[i].alt as u32)
|
||
.max();
|
||
chunk.alt > MIN_MOUNTAIN_ALT
|
||
&& chunk.chaos > MIN_MOUNTAIN_CHAOS
|
||
&& neighbor_alts_max.map_or(false, |n_alt| chunk.alt as u32 > n_alt)
|
||
})
|
||
.map(|(posi, chunk)| {
|
||
(
|
||
posi,
|
||
uniform_idx_as_vec2(map_size_lg, posi),
|
||
(chunk.alt - CONFIG.sea_level) as u32,
|
||
)
|
||
})
|
||
.collect::<Vec<(usize, Vec2<i32>, u32)>>();
|
||
let mut num_peaks = 0;
|
||
let mut removals = vec![false; peaks.len()];
|
||
for (i, peak) in peaks.iter().enumerate() {
|
||
for (k, n_peak) in peaks.iter().enumerate() {
|
||
// If the difference in position of this peak and another is
|
||
// below a threshold and this peak's altitude is lower, remove the
|
||
// peak from the list
|
||
if i != k
|
||
&& (peak.1).distance_squared(n_peak.1) < POI_THINNING_DIST_SQRD
|
||
&& peak.2 <= n_peak.2
|
||
{
|
||
// Remove this peak
|
||
// This cannot panic as `removals` is the same length as `peaks`
|
||
// i is the index in `peaks`
|
||
removals[i] = true;
|
||
}
|
||
}
|
||
}
|
||
peaks
|
||
.iter()
|
||
.enumerate()
|
||
.filter(|&(i, _)| !removals[i])
|
||
.for_each(|(_, (_, loc, alt))| {
|
||
num_peaks += 1;
|
||
self.pois.insert(PointOfInterest {
|
||
name: {
|
||
let name = NameGen::location(rng).generate();
|
||
if *alt < 1000 {
|
||
match rng.gen_range(0..6) {
|
||
0 => format!("{} Bluff", name),
|
||
1 => format!("{} Crag", name),
|
||
_ => format!("{} Hill", name),
|
||
}
|
||
} else {
|
||
match rng.gen_range(0..8) {
|
||
0 => format!("{}'s Peak", name),
|
||
1 => format!("{} Peak", name),
|
||
2 => format!("{} Summit", name),
|
||
_ => format!("Mount {}", name),
|
||
}
|
||
}
|
||
},
|
||
kind: PoiKind::Peak(*alt),
|
||
loc: *loc,
|
||
});
|
||
});
|
||
info!(?num_peaks, "all peaks named");
|
||
}
|
||
|
||
fn establish_site(
|
||
&mut self,
|
||
ctx: &mut GenCtx<impl Rng>,
|
||
loc: Vec2<i32>,
|
||
site_fn: impl FnOnce(Id<Place>) -> Site,
|
||
) -> Id<Site> {
|
||
const SITE_AREA: Range<usize> = 1..4; //64..256;
|
||
|
||
let place = match ctx.sim.get(loc).and_then(|site| site.place) {
|
||
Some(place) => place,
|
||
None => self.establish_place(ctx, loc, SITE_AREA),
|
||
};
|
||
|
||
let site = self.sites.insert(site_fn(place));
|
||
|
||
// Find neighbors
|
||
const MAX_NEIGHBOR_DISTANCE: f32 = 2000.0;
|
||
let mut nearby = self
|
||
.sites
|
||
.iter()
|
||
.filter(|(_, p)| {
|
||
matches!(
|
||
p.kind,
|
||
SiteKind::Refactor
|
||
| SiteKind::Settlement
|
||
| SiteKind::CliffTown
|
||
| SiteKind::Castle
|
||
)
|
||
})
|
||
.map(|(id, p)| (id, (p.center.distance_squared(loc) as f32).sqrt()))
|
||
.filter(|(_, dist)| *dist < MAX_NEIGHBOR_DISTANCE)
|
||
.collect::<Vec<_>>();
|
||
nearby.sort_by_key(|(_, dist)| *dist as i32);
|
||
|
||
if let SiteKind::Refactor | SiteKind::Settlement | SiteKind::CliffTown | SiteKind::Castle =
|
||
self.sites[site].kind
|
||
{
|
||
for (nearby, _) in nearby.into_iter().take(5) {
|
||
// Find a novel path
|
||
if let Some((path, cost)) = find_path(ctx, loc, self.sites.get(nearby).center) {
|
||
// Find a path using existing paths
|
||
if self
|
||
.route_between(site, nearby)
|
||
// If the novel path isn't efficient compared to existing routes, don't use it
|
||
.filter(|(_, route_cost)| *route_cost < cost * 3.0)
|
||
.is_none()
|
||
{
|
||
// Write the track to the world as a path
|
||
for locs in path.nodes().windows(3) {
|
||
let to_prev_idx = NEIGHBORS
|
||
.iter()
|
||
.enumerate()
|
||
.find(|(_, dir)| **dir == locs[0] - locs[1])
|
||
.expect("Track locations must be neighbors")
|
||
.0;
|
||
let to_next_idx = NEIGHBORS
|
||
.iter()
|
||
.enumerate()
|
||
.find(|(_, dir)| **dir == locs[2] - locs[1])
|
||
.expect("Track locations must be neighbors")
|
||
.0;
|
||
|
||
ctx.sim.get_mut(locs[0]).unwrap().path.0.neighbors |=
|
||
1 << ((to_prev_idx as u8 + 4) % 8);
|
||
ctx.sim.get_mut(locs[2]).unwrap().path.0.neighbors |=
|
||
1 << ((to_next_idx as u8 + 4) % 8);
|
||
let mut chunk = ctx.sim.get_mut(locs[1]).unwrap();
|
||
chunk.path.0.neighbors |=
|
||
(1 << (to_prev_idx as u8)) | (1 << (to_next_idx as u8));
|
||
chunk.path.0.offset =
|
||
Vec2::new(ctx.rng.gen_range(-16..17), ctx.rng.gen_range(-16..17));
|
||
}
|
||
|
||
// Take note of the track
|
||
let track = self.tracks.insert(Track { cost, path });
|
||
self.track_map
|
||
.entry(site)
|
||
.or_default()
|
||
.insert(nearby, track);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
site
|
||
}
|
||
}
|
||
|
||
/// Attempt to find a path between two locations
|
||
fn find_path(
|
||
ctx: &mut GenCtx<impl Rng>,
|
||
a: Vec2<i32>,
|
||
b: Vec2<i32>,
|
||
) -> Option<(Path<Vec2<i32>>, f32)> {
|
||
const MAX_PATH_ITERS: usize = 100_000;
|
||
let sim = &ctx.sim;
|
||
let heuristic = move |l: &Vec2<i32>| (l.distance_squared(b) as f32).sqrt();
|
||
let neighbors = |l: &Vec2<i32>| {
|
||
let l = *l;
|
||
NEIGHBORS
|
||
.iter()
|
||
.filter(move |dir| walk_in_dir(sim, l, **dir).is_some())
|
||
.map(move |dir| l + *dir)
|
||
};
|
||
let transition =
|
||
|a: &Vec2<i32>, b: &Vec2<i32>| 1.0 + walk_in_dir(sim, *a, *b - *a).unwrap_or(10000.0);
|
||
let satisfied = |l: &Vec2<i32>| *l == b;
|
||
// We use this hasher (FxHasher64) because
|
||
// (1) we don't care about DDOS attacks (ruling out SipHash);
|
||
// (2) we care about determinism across computers (ruling out AAHash);
|
||
// (3) we have 8-byte keys (for which FxHash is fastest).
|
||
let mut astar = Astar::new(
|
||
MAX_PATH_ITERS,
|
||
a,
|
||
heuristic,
|
||
BuildHasherDefault::<FxHasher64>::default(),
|
||
);
|
||
astar
|
||
.poll(MAX_PATH_ITERS, heuristic, neighbors, transition, satisfied)
|
||
.into_path()
|
||
.and_then(|path| astar.get_cheapest_cost().map(|cost| (path, cost)))
|
||
}
|
||
|
||
/// Return Some if travel between a location and a chunk next to it is permitted
|
||
/// If permitted, the approximate relative const of traversal is given
|
||
// (TODO: by whom?)
|
||
fn walk_in_dir(sim: &WorldSim, a: Vec2<i32>, dir: Vec2<i32>) -> Option<f32> {
|
||
if loc_suitable_for_walking(sim, a) && loc_suitable_for_walking(sim, a + dir) {
|
||
let a_chunk = sim.get(a)?;
|
||
let b_chunk = sim.get(a + dir)?;
|
||
|
||
let hill_cost = ((b_chunk.alt - a_chunk.alt).abs() / 5.0).powi(2);
|
||
let water_cost = if b_chunk.river.near_water() {
|
||
50.0
|
||
} else {
|
||
0.0
|
||
} + (b_chunk.water_alt - b_chunk.alt + 8.0).clamped(0.0, 8.0) * 3.0; // Try not to path swamps / tidal areas
|
||
let wild_cost = if b_chunk.path.0.is_way() {
|
||
0.0 // Traversing existing paths has no additional cost!
|
||
} else {
|
||
3.0 // + (1.0 - b_chunk.tree_density) * 20.0 // Prefer going through forests, for aesthetics
|
||
};
|
||
Some(1.0 + hill_cost + water_cost + wild_cost)
|
||
} else {
|
||
None
|
||
}
|
||
}
|
||
|
||
/// Return true if a position is suitable for walking on
|
||
fn loc_suitable_for_walking(sim: &WorldSim, loc: Vec2<i32>) -> bool {
|
||
if let Some(chunk) = sim.get(loc) {
|
||
!chunk.river.is_ocean() && !chunk.river.is_lake() && !chunk.near_cliffs()
|
||
} 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<i32>, dir: Vec2<i32>, site_kind: SiteKind) -> bool {
|
||
loc_suitable_for_site(sim, a, site_kind) && loc_suitable_for_site(sim, a + dir, site_kind)
|
||
}
|
||
|
||
/// Return true if a position is suitable for site construction (TODO:
|
||
/// criteria?)
|
||
fn loc_suitable_for_site(sim: &WorldSim, loc: Vec2<i32>, site_kind: SiteKind) -> bool {
|
||
fn check_chunk_occupation(sim: &WorldSim, loc: Vec2<i32>, radius: i32) -> bool {
|
||
for x in (-radius)..radius {
|
||
for y in (-radius)..radius {
|
||
let check_loc =
|
||
loc + Vec2::new(x, y).map2(TerrainChunkSize::RECT_SIZE, |e, sz| e * sz as i32);
|
||
if sim.get(check_loc).map_or(false, |c| !c.sites.is_empty()) {
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
true
|
||
}
|
||
let possible_terrain = if let Some(chunk) = sim.get(loc) {
|
||
!chunk.river.is_ocean()
|
||
&& !chunk.river.is_lake()
|
||
&& !chunk.river.is_river()
|
||
&& !chunk.is_underwater()
|
||
&& !matches!(
|
||
chunk.get_biome(),
|
||
common::terrain::BiomeKind::Lake | common::terrain::BiomeKind::Ocean
|
||
)
|
||
&& sim
|
||
.get_gradient_approx(loc)
|
||
.map(|grad| grad < 1.0)
|
||
.unwrap_or(false)
|
||
} else {
|
||
false
|
||
};
|
||
let not_occupied = check_chunk_occupation(sim, loc, site_kind.exclusion_radius());
|
||
possible_terrain && site_kind.is_suitable_loc(loc, sim) && not_occupied
|
||
}
|
||
|
||
/// Attempt to search for a location that's suitable for site construction
|
||
fn find_site_loc(
|
||
ctx: &mut GenCtx<impl Rng>,
|
||
avoid: (&Vec<Vec2<i32>>, i32),
|
||
size: i32,
|
||
site_kind: SiteKind,
|
||
) -> Option<Vec2<i32>> {
|
||
const MAX_ATTEMPTS: usize = 10000;
|
||
let mut loc = None;
|
||
let (avoid_locs, distance) = avoid;
|
||
for _ in 0..MAX_ATTEMPTS {
|
||
let test_loc = loc.unwrap_or_else(|| {
|
||
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 avoid_locs
|
||
.iter()
|
||
.any(|l| l.distance_squared(test_loc) < distance * distance)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
for offset in Spiral2d::new().take((size * 2 + 1).pow(2) as usize) {
|
||
if loc_suitable_for_site(ctx.sim, test_loc + offset, site_kind) {
|
||
return Some(test_loc);
|
||
}
|
||
}
|
||
|
||
loc = ctx.sim.get(test_loc).and_then(|c| {
|
||
site_kind.is_suitable_loc(test_loc, ctx.sim).then_some(
|
||
c.downhill?
|
||
.map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| e / (sz as i32)),
|
||
)
|
||
});
|
||
}
|
||
warn!("Failed to place site {:?}.", site_kind);
|
||
None
|
||
}
|
||
|
||
#[derive(Debug)]
|
||
pub struct Civ {
|
||
capital: Id<Site>,
|
||
homeland: Id<Place>,
|
||
}
|
||
|
||
#[derive(Debug)]
|
||
pub struct Place {
|
||
pub center: Vec2<i32>,
|
||
/* act sort of like territory with sites belonging to it
|
||
* nat_res/NaturalResources was moved to Economy
|
||
* nat_res: NaturalResources, */
|
||
}
|
||
|
||
pub struct Track {
|
||
/// Cost of using this track relative to other paths. This cost is an
|
||
/// arbitrary unit and doesn't make sense unless compared to other track
|
||
/// costs.
|
||
cost: f32,
|
||
path: Path<Vec2<i32>>,
|
||
}
|
||
|
||
impl Track {
|
||
pub fn path(&self) -> &Path<Vec2<i32>> { &self.path }
|
||
}
|
||
|
||
#[derive(Debug)]
|
||
pub struct Site {
|
||
pub kind: SiteKind,
|
||
// TODO: Remove this field when overhauling
|
||
pub site_tmp: Option<Id<crate::site::Site>>,
|
||
pub center: Vec2<i32>,
|
||
pub place: Id<Place>,
|
||
}
|
||
|
||
impl fmt::Display for Site {
|
||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||
writeln!(f, "{:?}", self.kind)?;
|
||
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||
pub enum SiteKind {
|
||
Settlement,
|
||
Dungeon,
|
||
Castle,
|
||
Refactor,
|
||
CliffTown,
|
||
Tree,
|
||
GiantTree,
|
||
Gnarling,
|
||
}
|
||
|
||
impl SiteKind {
|
||
pub fn is_suitable_loc(&self, loc: Vec2<i32>, sim: &WorldSim) -> bool {
|
||
sim.get(loc).map_or(false, |chunk| {
|
||
let suitable_for_town = |score_threshold: f32| -> bool {
|
||
const RESOURCE_RADIUS: i32 = 1;
|
||
let mut river_chunks = 0;
|
||
let mut lake_chunks = 0;
|
||
let mut ocean_chunks = 0;
|
||
let mut rock_chunks = 0;
|
||
let mut tree_chunks = 0;
|
||
let mut farmable_chunks = 0;
|
||
let mut farmable_needs_irrigation_chunks = 0;
|
||
let mut land_chunks = 0;
|
||
for x in (-RESOURCE_RADIUS)..RESOURCE_RADIUS {
|
||
for y in (-RESOURCE_RADIUS)..RESOURCE_RADIUS {
|
||
let check_loc = loc
|
||
+ Vec2::new(x, y)
|
||
.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e * sz as i32);
|
||
sim.get(check_loc).map(|c| {
|
||
if num::abs(chunk.alt - c.alt) < 200.0 {
|
||
if c.river.is_river() {
|
||
river_chunks += 1;
|
||
}
|
||
if c.river.is_lake() {
|
||
lake_chunks += 1;
|
||
}
|
||
if c.river.is_ocean() {
|
||
ocean_chunks += 1;
|
||
}
|
||
if c.tree_density > 0.7 {
|
||
tree_chunks += 1;
|
||
}
|
||
if c.rockiness < 0.3 && c.temp > CONFIG.snow_temp {
|
||
if c.surface_veg > 0.5 {
|
||
farmable_chunks += 1;
|
||
} else {
|
||
match c.get_biome() {
|
||
common::terrain::BiomeKind::Savannah => {
|
||
farmable_needs_irrigation_chunks += 1
|
||
},
|
||
common::terrain::BiomeKind::Desert => {
|
||
farmable_needs_irrigation_chunks += 1
|
||
},
|
||
_ => (),
|
||
}
|
||
}
|
||
}
|
||
if !c.river.is_river() && !c.river.is_lake() && !c.river.is_ocean()
|
||
{
|
||
land_chunks += 1;
|
||
}
|
||
}
|
||
// Mining is different since presumably you dig into the hillside
|
||
if c.rockiness > 0.7 && c.alt - chunk.alt > -10.0 {
|
||
rock_chunks += 1;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
let has_river = river_chunks > 1;
|
||
let has_lake = lake_chunks > 1;
|
||
let vegetation_implies_potable_water = chunk.tree_density > 0.4
|
||
&& !matches!(chunk.get_biome(), common::terrain::BiomeKind::Swamp);
|
||
let warm_or_firewood = chunk.temp > CONFIG.snow_temp || tree_chunks > 2;
|
||
let has_potable_water = {
|
||
has_river || (has_lake && chunk.alt > 100.0) || vegetation_implies_potable_water
|
||
};
|
||
let has_building_materials = tree_chunks > 0
|
||
|| rock_chunks > 0
|
||
|| chunk.temp > CONFIG.tropical_temp && (has_river || has_lake);
|
||
let water_rich = lake_chunks + river_chunks > 2;
|
||
let can_grow_rice = water_rich
|
||
&& chunk.humidity + 1.0 > CONFIG.jungle_hum
|
||
&& chunk.temp + 1.0 > CONFIG.tropical_temp;
|
||
let farming_score = if can_grow_rice {
|
||
farmable_chunks * 2
|
||
} else {
|
||
farmable_chunks
|
||
} + if water_rich {
|
||
farmable_needs_irrigation_chunks
|
||
} else {
|
||
0
|
||
};
|
||
let fish_score = lake_chunks + ocean_chunks;
|
||
let food_score = farming_score + fish_score;
|
||
let mining_score = if tree_chunks > 1 { rock_chunks } else { 0 };
|
||
let forestry_score = if has_river { tree_chunks } else { 0 };
|
||
let trading_score =
|
||
std::cmp::min(std::cmp::min(land_chunks, ocean_chunks), river_chunks);
|
||
let industry_score = 3.0 * (food_score as f32 + 1.0).log2()
|
||
+ 2.0 * (forestry_score as f32 + 1.0).log2()
|
||
+ (mining_score as f32 + 1.0).log2()
|
||
+ (trading_score as f32 + 1.0).log2();
|
||
has_potable_water
|
||
&& has_building_materials
|
||
&& industry_score > score_threshold
|
||
&& warm_or_firewood
|
||
};
|
||
match self {
|
||
SiteKind::Gnarling => {
|
||
(-0.3..0.4).contains(&chunk.temp) && chunk.tree_density > 0.75
|
||
},
|
||
SiteKind::GiantTree | SiteKind::Tree => chunk.tree_density > 0.4,
|
||
SiteKind::CliffTown => {
|
||
(-0.6..0.4).contains(&chunk.temp)
|
||
&& chunk.near_cliffs()
|
||
&& suitable_for_town(4.0)
|
||
},
|
||
SiteKind::Castle => {
|
||
if chunk.tree_density > 0.4 || chunk.river.near_water() || chunk.near_cliffs() {
|
||
return false;
|
||
}
|
||
const HILL_RADIUS: i32 = 3 * TERRAIN_CHUNK_BLOCKS_LG as i32;
|
||
for x in (-HILL_RADIUS)..HILL_RADIUS {
|
||
for y in (-HILL_RADIUS)..HILL_RADIUS {
|
||
let check_loc = loc + Vec2::new(x, y);
|
||
if let Some(true) = sim
|
||
.get_alt_approx(check_loc)
|
||
.map(|surrounding_alt| surrounding_alt > chunk.alt + 1.0)
|
||
{
|
||
return false;
|
||
}
|
||
// Castles are really big, so to avoid parts of them ending up
|
||
// underwater or in other awkward positions
|
||
// we have to do this
|
||
if sim
|
||
.get(check_loc)
|
||
.map_or(true, |c| c.is_underwater() || c.near_cliffs())
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
true
|
||
},
|
||
SiteKind::Refactor | SiteKind::Settlement => suitable_for_town(6.7),
|
||
_ => true,
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
impl SiteKind {
|
||
pub fn exclusion_radius(&self) -> i32 {
|
||
// FIXME: Provide specific values for each individual SiteKind
|
||
match self {
|
||
SiteKind::Dungeon => 4,
|
||
_ => 8, // This is just an arbitrary value
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Site {
|
||
pub fn is_dungeon(&self) -> bool { matches!(self.kind, SiteKind::Dungeon) }
|
||
|
||
pub fn is_settlement(&self) -> bool {
|
||
matches!(self.kind, SiteKind::Settlement | SiteKind::Refactor)
|
||
}
|
||
|
||
pub fn is_castle(&self) -> bool { matches!(self.kind, SiteKind::Castle) }
|
||
}
|
||
|
||
#[derive(PartialEq, Debug, Clone)]
|
||
pub struct PointOfInterest {
|
||
pub name: String,
|
||
pub kind: PoiKind,
|
||
pub loc: Vec2<i32>,
|
||
}
|
||
|
||
#[derive(PartialEq, Debug, Clone)]
|
||
pub enum PoiKind {
|
||
/// Peak stores the altitude
|
||
Peak(u32),
|
||
/// Lake stores a metric relating to size
|
||
Biome(u32),
|
||
}
|