veloren/world/src/civ/mod.rs
2022-03-29 12:52:08 +00:00

1348 lines
52 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#![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),
}