mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'tormod/worldgen-refactorings' into 'master'
Refactor various stuff in worldgen See merge request veloren/veloren!3939
This commit is contained in:
commit
b1321849bb
3
.gitignore
vendored
3
.gitignore
vendored
@ -72,3 +72,6 @@ img-export/**/*.png
|
|||||||
nix/result*
|
nix/result*
|
||||||
/result*
|
/result*
|
||||||
/shell.nix
|
/shell.nix
|
||||||
|
|
||||||
|
# Bash
|
||||||
|
.history
|
||||||
|
@ -120,6 +120,47 @@ impl ProximityRequirements {
|
|||||||
all_of_compliance && any_of_compliance
|
all_of_compliance && any_of_compliance
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn location_hint(&self, world_dims: &Aabr<i32>) -> Aabr<i32> {
|
||||||
|
let bounding_box_of_point = |point: Vec2<i32>, max_distance: i32| Aabr {
|
||||||
|
min: Vec2 {
|
||||||
|
x: point.x - max_distance,
|
||||||
|
y: point.y - max_distance,
|
||||||
|
},
|
||||||
|
max: Vec2 {
|
||||||
|
x: point.x + max_distance,
|
||||||
|
y: point.y + max_distance,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let any_of_hint = self
|
||||||
|
.any_of
|
||||||
|
.iter()
|
||||||
|
.fold(None, |acc, spec| match spec.max_distance {
|
||||||
|
None => acc,
|
||||||
|
Some(max_distance) => {
|
||||||
|
let bounding_box_of_new_point =
|
||||||
|
bounding_box_of_point(spec.location, max_distance);
|
||||||
|
match acc {
|
||||||
|
None => Some(bounding_box_of_new_point),
|
||||||
|
Some(acc) => Some(acc.union(bounding_box_of_new_point)),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.map(|hint| hint.intersection(*world_dims))
|
||||||
|
.unwrap_or_else(|| world_dims.to_owned());
|
||||||
|
let hint = self
|
||||||
|
.all_of
|
||||||
|
.iter()
|
||||||
|
.fold(any_of_hint, |acc, spec| match spec.max_distance {
|
||||||
|
None => acc,
|
||||||
|
Some(max_distance) => {
|
||||||
|
let bounding_box_of_new_point =
|
||||||
|
bounding_box_of_point(spec.location, max_distance);
|
||||||
|
acc.intersection(bounding_box_of_new_point)
|
||||||
|
},
|
||||||
|
});
|
||||||
|
hint
|
||||||
|
}
|
||||||
|
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
ProximityRequirements {
|
ProximityRequirements {
|
||||||
all_of: Vec::new(),
|
all_of: Vec::new(),
|
||||||
@ -1452,30 +1493,6 @@ fn loc_suitable_for_walking(sim: &WorldSim, loc: Vec2<i32>) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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,
|
|
||||||
is_suitable_loc: bool,
|
|
||||||
) -> 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);
|
|
||||||
if sim.get(check_loc).map_or(false, |c| !c.sites.is_empty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
let not_occupied = || check_chunk_occupation(sim, loc, site_kind.exclusion_radius());
|
|
||||||
// only check occupation if the location is suitable
|
|
||||||
is_suitable_loc && not_occupied()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempt to search for a location that's suitable for site construction
|
/// Attempt to search for a location that's suitable for site construction
|
||||||
fn find_site_loc(
|
fn find_site_loc(
|
||||||
ctx: &mut GenCtx<impl Rng>,
|
ctx: &mut GenCtx<impl Rng>,
|
||||||
@ -1486,16 +1503,24 @@ fn find_site_loc(
|
|||||||
const MAX_ATTEMPTS: usize = 10000;
|
const MAX_ATTEMPTS: usize = 10000;
|
||||||
let mut loc = None;
|
let mut loc = None;
|
||||||
for _ in 0..MAX_ATTEMPTS {
|
for _ in 0..MAX_ATTEMPTS {
|
||||||
|
let world_dims = Aabr {
|
||||||
|
min: Vec2 { x: 0, y: 0 },
|
||||||
|
max: Vec2 {
|
||||||
|
x: ctx.sim.get_size().x as i32,
|
||||||
|
y: ctx.sim.get_size().y as i32,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let location_hint = proximity_reqs.location_hint(&world_dims);
|
||||||
let test_loc = loc.unwrap_or_else(|| {
|
let test_loc = loc.unwrap_or_else(|| {
|
||||||
Vec2::new(
|
Vec2::new(
|
||||||
ctx.rng.gen_range(0..ctx.sim.get_size().x as i32),
|
ctx.rng.gen_range(location_hint.min.x..location_hint.max.x),
|
||||||
ctx.rng.gen_range(0..ctx.sim.get_size().y as i32),
|
ctx.rng.gen_range(location_hint.min.y..location_hint.max.y),
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
let is_suitable_loc = site_kind.is_suitable_loc(test_loc, ctx.sim);
|
let is_suitable_loc = site_kind.is_suitable_loc(test_loc, ctx.sim);
|
||||||
if is_suitable_loc && proximity_reqs.satisfied_by(test_loc) {
|
if is_suitable_loc && proximity_reqs.satisfied_by(test_loc) {
|
||||||
if loc_suitable_for_site(ctx.sim, test_loc, site_kind, is_suitable_loc) {
|
if site_kind.exclusion_radius_clear(ctx.sim, test_loc) {
|
||||||
return Some(test_loc);
|
return Some(test_loc);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1509,6 +1534,110 @@ fn find_site_loc(
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn town_attributes_of_site(loc: Vec2<i32>, sim: &WorldSim) -> Option<TownSiteAttributes> {
|
||||||
|
sim.get(loc).map(|chunk| {
|
||||||
|
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).cpos_to_wpos();
|
||||||
|
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);
|
||||||
|
TownSiteAttributes {
|
||||||
|
food_score,
|
||||||
|
mining_score,
|
||||||
|
forestry_score,
|
||||||
|
trading_score,
|
||||||
|
heating: warm_or_firewood,
|
||||||
|
potable_water: has_potable_water,
|
||||||
|
building_materials: has_building_materials,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TownSiteAttributes {
|
||||||
|
food_score: i32,
|
||||||
|
mining_score: i32,
|
||||||
|
forestry_score: i32,
|
||||||
|
trading_score: i32,
|
||||||
|
heating: bool,
|
||||||
|
potable_water: bool,
|
||||||
|
building_materials: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Civ {
|
pub struct Civ {
|
||||||
capital: Id<Site>,
|
capital: Id<Site>,
|
||||||
@ -1594,99 +1723,19 @@ impl SiteKind {
|
|||||||
|
|
||||||
sim.get(loc).map_or(false, |chunk| {
|
sim.get(loc).map_or(false, |chunk| {
|
||||||
let suitable_for_town = |score_threshold: f32| -> bool {
|
let suitable_for_town = |score_threshold: f32| -> bool {
|
||||||
const RESOURCE_RADIUS: i32 = 1;
|
let attributes = town_attributes_of_site(loc, sim);
|
||||||
let mut river_chunks = 0;
|
attributes.map_or(false, |attr| {
|
||||||
let mut lake_chunks = 0;
|
let industry_score = 3.0 * (attr.food_score as f32 + 1.0).log2()
|
||||||
let mut ocean_chunks = 0;
|
+ 2.0 * (attr.forestry_score as f32 + 1.0).log2()
|
||||||
let mut rock_chunks = 0;
|
+ (attr.mining_score as f32 + 1.0).log2()
|
||||||
let mut tree_chunks = 0;
|
+ (attr.trading_score as f32 + 1.0).log2();
|
||||||
let mut farmable_chunks = 0;
|
attr.potable_water
|
||||||
let mut farmable_needs_irrigation_chunks = 0;
|
&& attr.building_materials
|
||||||
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).cpos_to_wpos();
|
|
||||||
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
|
&& industry_score > score_threshold
|
||||||
&& warm_or_firewood
|
&& attr.heating
|
||||||
// Because of how the algorithm for site2 towns work, they have to start on land.
|
// Because of how the algorithm for site2 towns work, they have to start on land.
|
||||||
&& on_land()
|
&& on_land()
|
||||||
|
})
|
||||||
};
|
};
|
||||||
match self {
|
match self {
|
||||||
SiteKind::Gnarling => {
|
SiteKind::Gnarling => {
|
||||||
@ -1756,9 +1805,7 @@ impl SiteKind {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl SiteKind {
|
|
||||||
pub fn exclusion_radius(&self) -> i32 {
|
pub fn exclusion_radius(&self) -> i32 {
|
||||||
// FIXME: Provide specific values for each individual SiteKind
|
// FIXME: Provide specific values for each individual SiteKind
|
||||||
match self {
|
match self {
|
||||||
@ -1766,6 +1813,19 @@ impl SiteKind {
|
|||||||
_ => 8, // This is just an arbitrary value
|
_ => 8, // This is just an arbitrary value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn exclusion_radius_clear(&self, sim: &WorldSim, loc: Vec2<i32>) -> bool {
|
||||||
|
let radius = self.exclusion_radius();
|
||||||
|
for x in (-radius)..radius {
|
||||||
|
for y in (-radius)..radius {
|
||||||
|
let check_loc = loc + Vec2::new(x, y);
|
||||||
|
if sim.get(check_loc).map_or(false, |c| !c.sites.is_empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Site {
|
impl Site {
|
||||||
@ -1827,4 +1887,31 @@ mod tests {
|
|||||||
assert!(reqs.satisfied_by(Vec2 { x: 1, y: -1 }));
|
assert!(reqs.satisfied_by(Vec2 { x: 1, y: -1 }));
|
||||||
assert!(!reqs.satisfied_by(Vec2 { x: -8, y: 8 }));
|
assert!(!reqs.satisfied_by(Vec2 { x: -8, y: 8 }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn complex_proximity_requirements() {
|
||||||
|
let a_site = Vec2 { x: 572, y: 724 };
|
||||||
|
let reqs = ProximityRequirements::new()
|
||||||
|
.close_to_one_of(vec![a_site].into_iter(), 60)
|
||||||
|
.avoid_all_of(vec![a_site].into_iter(), 40);
|
||||||
|
assert!(reqs.satisfied_by(Vec2 { x: 572, y: 774 }));
|
||||||
|
assert!(!reqs.satisfied_by(a_site));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn location_hint() {
|
||||||
|
let reqs = ProximityRequirements::new().close_to_one_of(
|
||||||
|
vec![Vec2 { x: 1, y: 0 }, Vec2 { x: 13, y: 12 }].into_iter(),
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
let expected = Aabr {
|
||||||
|
min: Vec2 { x: 0, y: 0 },
|
||||||
|
max: Vec2 { x: 23, y: 22 },
|
||||||
|
};
|
||||||
|
let map_dims = Aabr {
|
||||||
|
min: Vec2 { x: 0, y: 0 },
|
||||||
|
max: Vec2 { x: 200, y: 300 },
|
||||||
|
};
|
||||||
|
assert_eq!(expected, reqs.location_hint(&map_dims));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user