mirror of
https://github.com/osmarks/ewo3.git
synced 2026-06-08 22:02:06 +00:00
Refactor and futilely retune plants
This commit is contained in:
+2
-1
@@ -1,4 +1,5 @@
|
||||
/target
|
||||
/node_modules
|
||||
out.png
|
||||
world.bin
|
||||
world.bin
|
||||
*.bin
|
||||
|
||||
Generated
+1
-2
@@ -805,8 +805,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "hecs"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3a2d85ff28b1e1a1c183b6ca9ec1a6b97e7937fb989988a74d9db39760a07b8"
|
||||
source = "git+https://github.com/osmarks/hecs-patch#ba24338a7c95f4333609f7d20676d684b49dfe0f"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
"hashbrown",
|
||||
|
||||
@@ -45,3 +45,6 @@ path = "src/render.rs"
|
||||
|
||||
[profile.release]
|
||||
debug = true
|
||||
|
||||
[patch.crates-io]
|
||||
hecs = { git = "https://github.com/osmarks/hecs-patch" }
|
||||
|
||||
+6
-3
@@ -27,7 +27,8 @@ pub enum HealthChangeType {
|
||||
BluntForce,
|
||||
Magic,
|
||||
NaturalRegeneration,
|
||||
Starvation
|
||||
Starvation,
|
||||
Senescence
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -389,6 +390,7 @@ pub struct Plant {
|
||||
pub growth_ticks: u64,
|
||||
pub children: u64,
|
||||
pub ready_for_reproduce_ticks: u64,
|
||||
pub age: u64,
|
||||
pub total_growth: f32 // currently a bit redundant, given nutrient consumption counter
|
||||
}
|
||||
|
||||
@@ -403,12 +405,13 @@ impl Plant {
|
||||
growth_ticks: 0,
|
||||
children: 0,
|
||||
ready_for_reproduce_ticks: 0,
|
||||
total_growth: 0.0
|
||||
total_growth: 0.0,
|
||||
age: 0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_reproduce(&self) -> bool {
|
||||
self.current_size >= self.genome.max_size() * self.genome.reproductive_size_fraction()
|
||||
self.current_size >= self.genome.mature_size()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,5 +4,6 @@ pub mod plant;
|
||||
pub mod save;
|
||||
pub mod world_serde;
|
||||
pub mod worldgen;
|
||||
pub mod util;
|
||||
|
||||
pub use components::*;
|
||||
|
||||
+31
-39
@@ -20,6 +20,7 @@ use ewo3::plant;
|
||||
use ewo3::save::{SavedGame, GameMetrics};
|
||||
use ewo3::world_serde;
|
||||
use ewo3::worldgen;
|
||||
use ewo3::util::config::*;
|
||||
|
||||
#[derive(FromArgs)]
|
||||
/// Run the game server.
|
||||
@@ -112,9 +113,6 @@ struct Client {
|
||||
entity: Entity
|
||||
}
|
||||
|
||||
const VIEW: i32 = 15;
|
||||
const RANDOM_DESPAWN_INV_RATE: u64 = 4000;
|
||||
|
||||
struct EnemySpec {
|
||||
symbol: char,
|
||||
min_damage: f32,
|
||||
@@ -289,22 +287,6 @@ fn game_state_from_saved(saved: SavedGame) -> Result<GameState> {
|
||||
})
|
||||
}
|
||||
|
||||
const PLANT_TICK_DELAY: u64 = 128;
|
||||
const FIELD_DECAY_DELAY: u64 = 100;
|
||||
const PLANT_GROWTH_SCALE: f32 = 0.01;
|
||||
const SOIL_NUTRIENT_CONSUMPTION_RATE: f32 = 0.5;
|
||||
const SOIL_NUTRIENT_FIXATION_RATE: f32 = 0.0002;
|
||||
const WATER_CONSUMPTION_RATE: f32 = 0.03;
|
||||
const PLANT_IDLE_WATER_CONSUMPTION_OFFSET: f32 = 0.05;
|
||||
const PLANT_DIEOFF_THRESHOLD: f32 = 0.3;
|
||||
const PLANT_DIEOFF_RATE: f32 = 0.2;
|
||||
const AUTOSAVE_INTERVAL_TICKS: u64 = 1024;
|
||||
const PLANT_POLLINATION_RADIUS: i32 = 10; // TODO: should be directional (wind, insects, etc) and vary by plant.
|
||||
const PLANT_POLLINATION_SCAN_FRACTION: f32 = 0.1;
|
||||
const PLANT_SEEDING_RADIUS: i32 = 5; // TODO: as above
|
||||
const PLANT_SEEDING_ATTEMPTS: usize = 10;
|
||||
const SOIL_NUTRIENT_DECOMP_RETURN_RATE: f32 = 1.0 / SOIL_NUTRIENT_CONSUMPTION_RATE * 0.6;
|
||||
|
||||
async fn game_tick(state: &mut GameState) -> Result<()> {
|
||||
let mut buffer = Buffer::new();
|
||||
|
||||
@@ -318,7 +300,7 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
|
||||
buffer.apply(state);
|
||||
|
||||
if state.ticks % FIELD_DECAY_DELAY == 0 {
|
||||
state.dynamic_soil_nutrients.for_each_mut(|nutrients| *nutrients *= 0.9995);
|
||||
state.dynamic_soil_nutrients.for_each_mut(|nutrients| *nutrients *= 0.9999);
|
||||
} else if state.ticks % FIELD_DECAY_DELAY == 1 {
|
||||
state.dynamic_groundwater.for_each_mut(|water| *water *= 0.999);
|
||||
} else if state.ticks % FIELD_DECAY_DELAY == 2 {
|
||||
@@ -408,21 +390,21 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
|
||||
if health.current <= 0.0 {
|
||||
buffer.kill(&state.world, entity, &mut rng, None);
|
||||
state.metrics.plants_died_starvation += 1;
|
||||
// return nutrients to soil upon death; TODO implement decomposition
|
||||
// return nutrients to soil upon death; TODO implement decomposition modelling
|
||||
state.dynamic_soil_nutrients[pos] += plant.current_size * SOIL_NUTRIENT_DECOMP_RETURN_RATE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let original_size = plant.current_size;
|
||||
plant.current_size += plant.genome.effective_growth_rate(soil_nutrients, water, temperature, salt, terrain) * PLANT_GROWTH_SCALE * plant.current_size.powf(-0.25); // allometric scaling law
|
||||
plant.current_size = plant.current_size.min(plant.genome.max_size());
|
||||
plant.current_size += plant.genome.base_growth_rate(soil_nutrients, water, temperature, salt, terrain) * PLANT_GROWTH_SCALE * plant.current_size.powf(-0.25); // allometric scaling law
|
||||
let difference = (plant.current_size - original_size).max(0.0);
|
||||
|
||||
if plant.can_reproduce() {
|
||||
plant.ready_for_reproduce_ticks += 1;
|
||||
if rng.f32() < plant.genome.reproduction_rate() {
|
||||
plants_to_reproduce.push(entity);
|
||||
plant.current_size -= PLANT_REPRODUCTION_ATTEMPT_COST;
|
||||
}
|
||||
} else {
|
||||
plant.ready_for_reproduce_ticks = 0;
|
||||
@@ -431,7 +413,7 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
|
||||
state.dynamic_soil_nutrients[pos] -= difference * SOIL_NUTRIENT_CONSUMPTION_RATE;
|
||||
plant.nutrients_consumed += difference * SOIL_NUTRIENT_CONSUMPTION_RATE;
|
||||
plant.total_growth += difference;
|
||||
if plant.current_size / plant.genome.max_size() > 0.5 {
|
||||
if plant.current_size > plant.genome.mature_size() {
|
||||
state.dynamic_soil_nutrients[pos] += plant.genome.nutrient_addition_rate() * SOIL_NUTRIENT_FIXATION_RATE;
|
||||
plant.nutrients_added += plant.genome.nutrient_addition_rate() * SOIL_NUTRIENT_FIXATION_RATE;
|
||||
}
|
||||
@@ -447,10 +429,18 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: fix
|
||||
if plant.total_growth > plant.genome.max_size() * 2.0 {
|
||||
buffer.kill(&state.world, entity, &mut rng, None);
|
||||
state.metrics.plants_died_old_age += 1;
|
||||
plant.age += 1;
|
||||
|
||||
if plant.age as f32 > plant.genome.lifespan() * PLANT_LIFESPAN_SCALE {
|
||||
// TODO refactor
|
||||
if let Ok(mut health) = state.world.get::<&mut Health>(entity) {
|
||||
health.apply(HealthChangeType::Senescence, -PLANT_DIEOFF_RATE);
|
||||
if health.current <= 0.0 {
|
||||
buffer.kill(&state.world, entity, &mut rng, None);
|
||||
state.metrics.plants_died_old_age += 1;
|
||||
state.dynamic_soil_nutrients[pos] += plant.current_size * SOIL_NUTRIENT_DECOMP_RETURN_RATE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -462,7 +452,7 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
|
||||
|
||||
let count = count_hexes(PLANT_POLLINATION_RADIUS);
|
||||
let reproduction_tries = (count as f32 * PLANT_POLLINATION_SCAN_FRACTION).ceil() as usize;
|
||||
let mut hybrid_genome = None;
|
||||
let mut maybe_other = None;
|
||||
for _ in 0..reproduction_tries {
|
||||
let newpos = pos + sample_range_rng(PLANT_POLLINATION_RADIUS, &mut rng);
|
||||
if !state.map.heightmap.in_range(newpos) || newpos == pos {
|
||||
@@ -471,8 +461,8 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
|
||||
if let Some(other) = state.positions.entities[newpos] {
|
||||
if let Ok(other_plant) = state.world.get::<&mut Plant>(other) {
|
||||
if other_plant.can_reproduce() {
|
||||
if let Some(hybrid) = other_plant.genome.hybridize(&own_genome) {
|
||||
hybrid_genome = Some((hybrid, other));
|
||||
if let Some(_hybrid) = other_plant.genome.hybridize(&mut rng, &own_genome) {
|
||||
maybe_other = Some(other);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -480,10 +470,12 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((hybrid_genome, other)) = hybrid_genome {
|
||||
if let Some(other) = maybe_other {
|
||||
// TODO: multiple seeds from one plant with different genomes\
|
||||
let [plant, other_plant] = state.world.query_disjoint_mut::<&mut Plant, 2>([entity, other]);
|
||||
let plant = plant?;
|
||||
let other_plant = other_plant?;
|
||||
let hybrid_genome = other_plant.genome.hybridize(&mut rng, &own_genome).unwrap();
|
||||
|
||||
for _ in 0..PLANT_SEEDING_ATTEMPTS {
|
||||
let newpos = pos + sample_range_rng(PLANT_SEEDING_RADIUS, &mut rng);
|
||||
@@ -499,8 +491,9 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
|
||||
));
|
||||
plant.children += 1;
|
||||
other_plant.children += 1;
|
||||
plant.current_size *= plant.genome.reproductive_size_fraction(); // TODO: explicit cooldown?
|
||||
other_plant.current_size *= other_plant.genome.reproductive_size_fraction(); // TODO: vary this by plant component gender?
|
||||
// TODO: can this go negative?
|
||||
plant.current_size -= PLANT_CHILD_COST;
|
||||
other_plant.current_size -= PLANT_CHILD_COST;
|
||||
state.metrics.plants_reproduced += 1;
|
||||
break;
|
||||
}
|
||||
@@ -859,8 +852,6 @@ async fn save_game(state: &GameState) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const INITIAL_PLANTS: usize = 262144;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let mut loaded_save = false;
|
||||
@@ -908,15 +899,16 @@ async fn main() -> Result<()> {
|
||||
let mut batch = Vec::with_capacity(INITIAL_PLANTS);
|
||||
let mut used = HashSet::new();
|
||||
while batch.len() < INITIAL_PLANTS {
|
||||
let genome = plant::Genome::random();
|
||||
let pos = Coord::origin() + sample_range(state.map.radius());
|
||||
let genome = plant::Genome::random(&mut state.rng);
|
||||
let radius = state.map.radius();
|
||||
let pos = Coord::origin() + sample_range(&mut state.rng, radius);
|
||||
if genome.base_growth_rate(state.actual_soil_nutrients(pos), state.actual_groundwater(pos), state.baseline_temperature[pos], state.baseline_salt[pos], &state.map.get_terrain(pos)) > 0.2 && !used.contains(&pos) {
|
||||
batch.push((
|
||||
Position::single_tile(pos, MapLayer::Entities),
|
||||
Render('+'),
|
||||
Health::new(10.0, 10.0),
|
||||
//ShrinkOnDeath,
|
||||
Plant::new(plant::Genome::random()),
|
||||
Plant::new(genome),
|
||||
NewlyAdded
|
||||
));
|
||||
used.insert(pos);
|
||||
|
||||
+3
-3
@@ -40,9 +40,9 @@ pub fn rotate_60(p0: CoordVec) -> CoordVec {
|
||||
|
||||
pub const DIRECTIONS: &[CoordVec] = &[CoordVec::new(0, -1), CoordVec::new(1, -1), CoordVec::new(-1, 0), CoordVec::new(1, 0), CoordVec::new(0, 1), CoordVec::new(-1, 1)];
|
||||
|
||||
pub fn sample_range(range: i32) -> CoordVec {
|
||||
let q = fastrand::i32(-range..=range);
|
||||
let r = fastrand::i32((-range).max(-q-range)..=range.min(-q+range));
|
||||
pub fn sample_range(rng: &mut fastrand::Rng, range: i32) -> CoordVec {
|
||||
let q = rng.i32(-range..=range);
|
||||
let r = rng.i32((-range).max(-q-range)..=range.min(-q+range));
|
||||
CoordVec::new(q, r)
|
||||
}
|
||||
|
||||
|
||||
+50
-56
@@ -1,5 +1,6 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::worldgen::TerrainType;
|
||||
use crate::util::*;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum CropType {
|
||||
@@ -15,40 +16,20 @@ pub struct Genome {
|
||||
// polygenic traits; parameterized as N(0,1) (allegedly)
|
||||
// groundwater is [0,1] so this is sort of questionable
|
||||
// TODO: reparameterize or something
|
||||
growth_rate: f32,
|
||||
nutrient_addition_rate: f32,
|
||||
optimal_water_level: f32,
|
||||
optimal_water_level: f32, // no longer absolute level; sigmoided
|
||||
optimal_temperature: f32,
|
||||
reproduction_rate: f32,
|
||||
reproductive_size_fraction_param: f32,
|
||||
temperature_tolerance: f32,
|
||||
water_tolerance: f32,
|
||||
salt_tolerance: f32,
|
||||
max_size: f32
|
||||
mature_size: f32,
|
||||
lifespan_multiplier: f32,
|
||||
reproduction_rate: f32,
|
||||
// TODO number of seeds produced?
|
||||
// TODO color trait
|
||||
}
|
||||
|
||||
fn sigmoid(x: f32) -> f32 {
|
||||
1.0 / (1.0 + (-x).exp())
|
||||
}
|
||||
|
||||
// Box-Muller transform
|
||||
fn normal() -> f32 {
|
||||
let u = fastrand::f32();
|
||||
let v = fastrand::f32();
|
||||
(v * std::f32::consts::TAU).cos() * (-2.0 * u.ln()).sqrt()
|
||||
}
|
||||
|
||||
fn normal_scaled(mu: f32, sigma: f32) -> f32 {
|
||||
normal() * sigma + mu
|
||||
}
|
||||
|
||||
impl Genome {
|
||||
pub fn reproductive_size_fraction(&self) -> f32 {
|
||||
sigmoid(self.reproductive_size_fraction_param) * 0.49 + 0.5
|
||||
}
|
||||
|
||||
pub fn terrain_valid(&self, terrain: &TerrainType) -> bool {
|
||||
match terrain {
|
||||
TerrainType::Empty => true,
|
||||
@@ -66,7 +47,6 @@ impl Genome {
|
||||
}
|
||||
let temperature_diff = (temperature - sigmoid(self.optimal_temperature)).powf(2.0);
|
||||
let base = 1.0
|
||||
- self.reproduction_rate * 0.1 // faster reproduction trades off slightly against growth
|
||||
- self.nutrient_addition_rate() * 0.16 // nutrient enrichment has a growth tradeoff
|
||||
- self.water_tolerance * 0.08
|
||||
- self.temperature_tolerance * 0.07
|
||||
@@ -77,19 +57,32 @@ impl Genome {
|
||||
let temperature_tolerance_coefficient = 3.0 * (1.0 + (-self.temperature_tolerance).exp());
|
||||
let salt_tolerance_coefficient = 8.0 * (-self.salt_tolerance).exp();
|
||||
|
||||
base
|
||||
let raw = base
|
||||
* (2.0 * nutrients - 1.5).min(0.0).exp()
|
||||
* (-water_tolerance_coefficient * water_diff).exp()
|
||||
* (-temperature_tolerance_coefficient * temperature_diff).exp()
|
||||
* (-salt.abs() * salt_tolerance_coefficient).exp()
|
||||
* (-salt.abs() * salt_tolerance_coefficient).exp();
|
||||
|
||||
raw.min(1.0)
|
||||
}
|
||||
|
||||
pub fn max_size(&self) -> f32 {
|
||||
self.max_size.min(0.0).exp().max(self.max_size + 1.0) + 0.5
|
||||
pub fn mature_size(&self) -> f32 {
|
||||
self.mature_size.exp()
|
||||
}
|
||||
|
||||
pub fn effective_growth_rate(&self, nutrients: f32, water: f32, temperature: f32, salt: f32, terrain: &TerrainType) -> f32 {
|
||||
1.5f32.powf(self.growth_rate) * 0.5 * self.base_growth_rate(nutrients, water, temperature, salt, terrain)
|
||||
// This is not directly used in computations, as plants mature when they have gained enough size rather than at a specifc time.
|
||||
// However, old age death is directly time-based.
|
||||
pub fn age_at_maturity(&self) -> f32 {
|
||||
// allometric scaling law
|
||||
self.mature_size().powf(0.25)
|
||||
}
|
||||
|
||||
pub fn lifespan_multiplier(&self) -> f32 {
|
||||
self.lifespan_multiplier.exp() + 1.0
|
||||
}
|
||||
|
||||
pub fn lifespan(&self) -> f32 {
|
||||
self.age_at_maturity() * self.lifespan_multiplier()
|
||||
}
|
||||
|
||||
pub fn nutrient_addition_rate(&self) -> f32 {
|
||||
@@ -100,8 +93,8 @@ impl Genome {
|
||||
sigmoid(self.reproduction_rate)
|
||||
}
|
||||
|
||||
pub fn random() -> Genome {
|
||||
let crop_type = match fastrand::usize(0..4) {
|
||||
pub fn random(rng: &mut fastrand::Rng) -> Genome {
|
||||
let crop_type = match rng.usize(0..4) {
|
||||
0 => CropType::Grass,
|
||||
1 => CropType::EucalyptusTree,
|
||||
2 => CropType::BushTomato,
|
||||
@@ -109,26 +102,28 @@ impl Genome {
|
||||
_ => unreachable!()
|
||||
};
|
||||
|
||||
let (nutrient_addition_rate, optimal_water_level, optimal_temperature, reproductive_size_fraction_param, salt_tolerance, max_size) = match crop_type {
|
||||
CropType::Grass => (-10.0,-1.0, 0.0, -1.0, 0.0, 1.0),
|
||||
CropType::EucalyptusTree => (-10.0, 1.0, 0.5, 1.0, 0.1, 5.0),
|
||||
CropType::BushTomato => (-10.0, 0.0, 1.0, -0.3, 0.2, 1.5),
|
||||
CropType::GoldenWattleTree => ( 2.0, 0.5, 0.5, 0.5, 0.7, 3.0),
|
||||
// Mature sizes aren't fully realistic for performance reasons: modelling individual blades of grass at EWO3 speeds is unfortunately not currently feasible. The sizes may represent an aggregation of several plants.
|
||||
// Size is something like total mass of a 1m^2 collection of this.
|
||||
// TODO pick these more precisely.
|
||||
let (nutrient_addition_rate, optimal_water_level, optimal_temperature, salt_tolerance, lifespan_multiplier, mature_size, reproduction_rate) = match crop_type {
|
||||
CropType::Grass => (-10.0,-1.0,-0.5, 0.0, 0.5, -1.0, 0.0), // TODO: tie reproduction rate to something else?
|
||||
CropType::EucalyptusTree => (-10.0, 1.0, 0.0, 0.1, 5.0, 6.0, -4.0),
|
||||
CropType::BushTomato => (-10.0, 0.0, 1.0, 0.2, 1.5, 1.0, -3.0),
|
||||
CropType::GoldenWattleTree => ( 2.0, 0.5, 0.2, 0.7, 3.0, 4.0, -4.0),
|
||||
|
||||
};
|
||||
|
||||
Genome {
|
||||
crop_type: crop_type,
|
||||
growth_rate: normal(),
|
||||
nutrient_addition_rate,
|
||||
optimal_water_level,
|
||||
optimal_temperature,
|
||||
reproduction_rate: normal(),
|
||||
reproductive_size_fraction_param: normal() + reproductive_size_fraction_param,
|
||||
temperature_tolerance: normal(),
|
||||
water_tolerance: normal(),
|
||||
salt_tolerance: normal() + salt_tolerance,
|
||||
max_size
|
||||
temperature_tolerance: normal(rng),
|
||||
water_tolerance: normal(rng),
|
||||
salt_tolerance: normal(rng) + salt_tolerance,
|
||||
mature_size,
|
||||
lifespan_multiplier,
|
||||
reproduction_rate
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,20 +132,19 @@ impl Genome {
|
||||
sigmoid(self.optimal_water_level * 3.0 - 1.0)
|
||||
}
|
||||
|
||||
pub fn hybridize(&self, other: &Genome) -> Option<Genome> {
|
||||
pub fn hybridize(&self, rng: &mut fastrand::Rng, other: &Genome) -> Option<Genome> {
|
||||
if self.crop_type != other.crop_type { return None }
|
||||
Some(Genome {
|
||||
crop_type: self.crop_type,
|
||||
growth_rate: (self.growth_rate + other.growth_rate) / 2.0 + normal_scaled(0.0, 0.1),
|
||||
nutrient_addition_rate: (self.nutrient_addition_rate + other.nutrient_addition_rate) / 2.0 + normal_scaled(0.0, 0.03),
|
||||
optimal_water_level: (self.optimal_water_level + other.optimal_water_level) / 2.0 + normal_scaled(0.0, 0.03),
|
||||
optimal_temperature: (self.optimal_temperature + other.optimal_temperature) / 2.0 + normal_scaled(0.0, 0.03),
|
||||
reproduction_rate: (self.reproduction_rate + other.reproduction_rate) / 2.0 + normal_scaled(0.0, 0.5),
|
||||
reproductive_size_fraction_param: (self.reproductive_size_fraction_param + other.reproductive_size_fraction_param) / 2.0 + normal_scaled(0.0, 0.2),
|
||||
temperature_tolerance: (self.temperature_tolerance + other.temperature_tolerance) / 2.0 + normal_scaled(0.0, 0.2),
|
||||
water_tolerance: (self.water_tolerance + other.water_tolerance) / 2.0 + normal_scaled(0.0, 0.2),
|
||||
salt_tolerance: (self.salt_tolerance + other.salt_tolerance) / 2.0 + normal_scaled(0.0, 0.2),
|
||||
max_size: (self.max_size + other.max_size) / 2.0 + normal_scaled(0.0, 0.3)
|
||||
nutrient_addition_rate: (self.nutrient_addition_rate + other.nutrient_addition_rate) / 2.0 + normal_scaled(rng, 0.0, 0.03),
|
||||
optimal_water_level: (self.optimal_water_level + other.optimal_water_level) / 2.0 + normal_scaled(rng, 0.0, 0.03),
|
||||
optimal_temperature: (self.optimal_temperature + other.optimal_temperature) / 2.0 + normal_scaled(rng, 0.0, 0.03),
|
||||
temperature_tolerance: (self.temperature_tolerance + other.temperature_tolerance) / 2.0 + normal_scaled(rng, 0.0, 0.2),
|
||||
water_tolerance: (self.water_tolerance + other.water_tolerance) / 2.0 + normal_scaled(rng, 0.0, 0.2),
|
||||
salt_tolerance: (self.salt_tolerance + other.salt_tolerance) / 2.0 + normal_scaled(rng, 0.0, 0.2),
|
||||
mature_size: (self.mature_size + other.mature_size) / 2.0 + normal_scaled(rng, 0.0, 0.1),
|
||||
lifespan_multiplier: (self.lifespan_multiplier + other.lifespan_multiplier) / 2.0 + normal_scaled(rng, 0.0, 0.1),
|
||||
reproduction_rate: (self.reproduction_rate + other.reproduction_rate) / 2.0 + normal_scaled(rng, 0.0, 0.05),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+106
-99
@@ -8,6 +8,7 @@ use ewo3::map::*;
|
||||
use ewo3::save::SavedGame;
|
||||
use ewo3::world_serde;
|
||||
use ewo3::worldgen::*;
|
||||
use ewo3::util::config::*;
|
||||
use glow::HasContext;
|
||||
use glutin::config::ConfigTemplateBuilder;
|
||||
use glutin::context::{ContextAttributesBuilder, NotCurrentGlContext, PossiblyCurrentContext};
|
||||
@@ -62,67 +63,71 @@ struct Args {
|
||||
window: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Field {
|
||||
Height,
|
||||
Rain,
|
||||
Water,
|
||||
Groundwater,
|
||||
Salt,
|
||||
Temperature,
|
||||
Humidity,
|
||||
Soil,
|
||||
Contour,
|
||||
SeaDistance,
|
||||
Plants,
|
||||
macro_rules! define_fields {
|
||||
(
|
||||
$vis:vis enum $name:ident {
|
||||
$(
|
||||
$variant:ident => $str:literal
|
||||
),+ $(,)?
|
||||
}
|
||||
) => {
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
$vis enum $name {
|
||||
$(
|
||||
$variant,
|
||||
)+
|
||||
}
|
||||
|
||||
const ALL_FIELDS: [$name; define_fields!(@count $($variant),+)] = [
|
||||
$(
|
||||
$name::$variant,
|
||||
)+
|
||||
];
|
||||
|
||||
impl $name {
|
||||
fn parse(s: &str) -> Result<Self> {
|
||||
match s {
|
||||
$(
|
||||
$str => Ok(Self::$variant),
|
||||
)+
|
||||
_ => anyhow::bail!("unknown field: {s}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn name(self) -> &'static str {
|
||||
match self {
|
||||
$(
|
||||
Self::$variant => $str,
|
||||
)+
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
(@count $($variant:ident),+) => {
|
||||
<[()]>::len(&[$(define_fields!(@unit $variant)),+])
|
||||
};
|
||||
|
||||
(@unit $variant:ident) => { () };
|
||||
}
|
||||
|
||||
const ALL_FIELDS: [Field; 11] = [
|
||||
Field::Height,
|
||||
Field::Rain,
|
||||
Field::Water,
|
||||
Field::Groundwater,
|
||||
Field::Salt,
|
||||
Field::Temperature,
|
||||
Field::Humidity,
|
||||
Field::Soil,
|
||||
Field::Contour,
|
||||
Field::SeaDistance,
|
||||
Field::Plants,
|
||||
];
|
||||
|
||||
impl Field {
|
||||
fn parse(s: &str) -> Result<Self> {
|
||||
match s {
|
||||
"height" => Ok(Self::Height),
|
||||
"rain" => Ok(Self::Rain),
|
||||
"water" => Ok(Self::Water),
|
||||
"groundwater" => Ok(Self::Groundwater),
|
||||
"salt" => Ok(Self::Salt),
|
||||
"temperature" => Ok(Self::Temperature),
|
||||
"humidity" => Ok(Self::Humidity),
|
||||
"soil" => Ok(Self::Soil),
|
||||
"contour" => Ok(Self::Contour),
|
||||
"sea_distance" => Ok(Self::SeaDistance),
|
||||
"plants" => Ok(Self::Plants),
|
||||
_ => anyhow::bail!("unknown field: {s}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn name(self) -> &'static str {
|
||||
match self {
|
||||
Field::Height => "height",
|
||||
Field::Rain => "rain",
|
||||
Field::Water => "water",
|
||||
Field::Groundwater => "groundwater",
|
||||
Field::Salt => "salt",
|
||||
Field::Temperature => "temperature",
|
||||
Field::Humidity => "humidity",
|
||||
Field::Soil => "soil",
|
||||
Field::Contour => "contour",
|
||||
Field::SeaDistance => "sea_distance",
|
||||
Field::Plants => "plants",
|
||||
}
|
||||
define_fields! {
|
||||
enum Field {
|
||||
Height => "height",
|
||||
Rain => "rain",
|
||||
Water => "water",
|
||||
Groundwater => "groundwater",
|
||||
Salt => "salt",
|
||||
Temperature => "temperature",
|
||||
Humidity => "humidity",
|
||||
Soil => "soil",
|
||||
Contour => "contour",
|
||||
SeaDistance => "sea_distance",
|
||||
Plants => "plant_growth",
|
||||
PlantsAge => "plant_age",
|
||||
PlantsLifespanFraction => "plant_lifespan_fraction",
|
||||
DynamicGroundwater => "dynamic_groundwater",
|
||||
DynamicSoil => "dynamic_soil",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,6 +172,10 @@ struct RenderData {
|
||||
contour_points: HashMap<Coord, u8>,
|
||||
sea_distance: Map<f32>,
|
||||
plants: Map<f32>,
|
||||
plants_age: Map<f32>,
|
||||
plants_lifespan_fraction: Map<f32>,
|
||||
dynamic_groundwater: Map<f32>,
|
||||
dynamic_soil: Map<f32>,
|
||||
}
|
||||
|
||||
struct RenderedImage {
|
||||
@@ -203,6 +212,10 @@ fn sample_field(field: Field, position: Coord, data: &RenderData) -> f32 {
|
||||
Field::Contour => data.contour_points.get(&position).copied().unwrap_or_default() as f32 / 255.0,
|
||||
Field::SeaDistance => data.sea_distance[position].clamp(0.0, 1.0),
|
||||
Field::Plants => data.plants[position],
|
||||
Field::PlantsAge => data.plants_age[position],
|
||||
Field::PlantsLifespanFraction => data.plants_lifespan_fraction[position],
|
||||
Field::DynamicGroundwater => data.dynamic_groundwater[position],
|
||||
Field::DynamicSoil => data.dynamic_soil[position],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,11 +247,12 @@ fn to_rgb(c1: f32, c2: f32, c3: f32, color_space: ColorSpace) -> [u8; 3] {
|
||||
}
|
||||
|
||||
fn build_derived_data(
|
||||
world: &GeneratedWorld,
|
||||
ecs_world: &World,
|
||||
dynamic_groundwater: &Map<f32>,
|
||||
dynamic_soil_nutrients: &Map<f32>,
|
||||
) -> (Map<f32>, Map<f32>, Map<f32>, Map<f32>, HashMap<Coord, u8>) {
|
||||
world: GeneratedWorld,
|
||||
ecs_world: World,
|
||||
dynamic_groundwater: Map<f32>,
|
||||
dynamic_soil_nutrients: Map<f32>,
|
||||
positions: PositionIndex,
|
||||
) -> RenderData {
|
||||
let (sinks, sea) = get_sea(&world.heightmap);
|
||||
|
||||
let mut sea_distance = distance_map(world.radius, sea.iter().copied());
|
||||
@@ -265,6 +279,8 @@ fn build_derived_data(
|
||||
);
|
||||
|
||||
let mut plants = Map::new(world.radius, 0.0f32);
|
||||
let mut plants_age = Map::new(world.radius, 0.0f32);
|
||||
let mut plants_lifespan_fraction = Map::new(world.radius, 0.0f32);
|
||||
for (position, plant) in ecs_world.query::<(&Position, &Plant)>().iter() {
|
||||
let pos = position.head();
|
||||
if !plants.in_range(pos) {
|
||||
@@ -280,15 +296,24 @@ fn build_derived_data(
|
||||
if g > plants[pos] {
|
||||
plants[pos] = g;
|
||||
}
|
||||
plants_age[pos] = plant.age as f32;
|
||||
plants_lifespan_fraction[pos] = plant.age as f32 / (plant.genome.lifespan() * PLANT_LIFESPAN_SCALE);
|
||||
}
|
||||
|
||||
(
|
||||
sea_distance,
|
||||
RenderData {
|
||||
world,
|
||||
ecs_world,
|
||||
positions,
|
||||
groundwater,
|
||||
soil,
|
||||
contour_points,
|
||||
sea_distance,
|
||||
plants,
|
||||
contour_points
|
||||
)
|
||||
plants_age,
|
||||
plants_lifespan_fraction,
|
||||
dynamic_groundwater,
|
||||
dynamic_soil: dynamic_soil_nutrients
|
||||
}
|
||||
}
|
||||
|
||||
fn rebuild_position_index(world: &World, radius: i32) -> PositionIndex {
|
||||
@@ -307,22 +332,14 @@ fn load_render_data(args: &Args) -> Result<RenderData> {
|
||||
let ecs_world = world_serde::deserialize_world_from_bytes(&save.world)?;
|
||||
let positions = rebuild_position_index(&ecs_world, save.map.radius);
|
||||
let world = save.map;
|
||||
let (sea_distance, groundwater, soil, plants, contour_points) = build_derived_data(
|
||||
&world,
|
||||
&ecs_world,
|
||||
&save.dynamic_groundwater,
|
||||
&save.dynamic_soil_nutrients,
|
||||
);
|
||||
Ok(RenderData {
|
||||
|
||||
Ok(build_derived_data(
|
||||
world,
|
||||
ecs_world,
|
||||
positions,
|
||||
groundwater,
|
||||
soil,
|
||||
contour_points,
|
||||
sea_distance,
|
||||
plants,
|
||||
})
|
||||
save.dynamic_groundwater,
|
||||
save.dynamic_soil_nutrients,
|
||||
positions
|
||||
))
|
||||
} else {
|
||||
let radius = args.radius.max(1);
|
||||
let mut heightmap = generate_heights_with_radius(radius);
|
||||
@@ -361,17 +378,7 @@ fn load_render_data(args: &Args) -> Result<RenderData> {
|
||||
let ecs_world = World::new();
|
||||
let positions = PositionIndex::new(world.radius);
|
||||
let zero = Map::new(world.radius, 0.0f32);
|
||||
let (sea_distance, groundwater, soil, plants, contour_points) = build_derived_data(&world, &ecs_world, &zero, &zero);
|
||||
Ok(RenderData {
|
||||
world,
|
||||
ecs_world,
|
||||
positions,
|
||||
groundwater,
|
||||
soil,
|
||||
contour_points,
|
||||
sea_distance,
|
||||
plants,
|
||||
})
|
||||
Ok(build_derived_data(world, ecs_world, zero.clone(), zero, positions))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -622,8 +629,8 @@ fn inspect_entity(data: &RenderData, entity: hecs::Entity, layer: MapLayer, ui:
|
||||
}
|
||||
if let Ok(plant) = data.ecs_world.get::<&Plant>(entity) {
|
||||
ui.text(format!(
|
||||
" Plant: size={:.4} growth_ticks={} children={} ready_ticks={}",
|
||||
plant.current_size, plant.growth_ticks, plant.children, plant.ready_for_reproduce_ticks
|
||||
" Plant: size={:.4} growth_ticks={} children={} ready_ticks={} age={}",
|
||||
plant.current_size, plant.growth_ticks, plant.children, plant.ready_for_reproduce_ticks, plant.age
|
||||
));
|
||||
ui.text(format!(
|
||||
" Plant: nutrients_consumed={:.4} nutrients_added={:.4} water_consumed={:.4}",
|
||||
@@ -741,7 +748,7 @@ fn run_window(data: RenderData, mut settings: RenderSettings) -> Result<()> {
|
||||
|
||||
ui.window("Controls")
|
||||
.position([20.0, 20.0], Condition::FirstUseEver)
|
||||
.size([420.0, 420.0], Condition::FirstUseEver)
|
||||
.size([720.0, 420.0], Condition::FirstUseEver)
|
||||
.build(|| {
|
||||
ui.text(format!("radius: {}", data.world.radius));
|
||||
|
||||
@@ -855,7 +862,7 @@ fn run_window(data: RenderData, mut settings: RenderSettings) -> Result<()> {
|
||||
let tid = map_texture_id.expect("texture id");
|
||||
|
||||
ui.window("Map")
|
||||
.position([460.0, 20.0], Condition::FirstUseEver)
|
||||
.position([760.0, 20.0], Condition::FirstUseEver)
|
||||
.size(
|
||||
[frame.width as f32 + 24.0, frame.height as f32 + 42.0],
|
||||
Condition::FirstUseEver,
|
||||
@@ -893,7 +900,7 @@ fn run_window(data: RenderData, mut settings: RenderSettings) -> Result<()> {
|
||||
|
||||
ui.window("Inspector")
|
||||
.position([20.0, 460.0], Condition::FirstUseEver)
|
||||
.size([420.0, 460.0], Condition::FirstUseEver)
|
||||
.size([720.0, 660.0], Condition::FirstUseEver)
|
||||
.build(|| {
|
||||
ui.text("Field ranges:");
|
||||
if settings.pca_fields.len() >= 2 {
|
||||
@@ -947,7 +954,7 @@ fn run_window(data: RenderData, mut settings: RenderSettings) -> Result<()> {
|
||||
}
|
||||
for f in ALL_FIELDS {
|
||||
ui.text(format!(
|
||||
"{:<12} {:>10.6}",
|
||||
"{:<21} {:>10.6}",
|
||||
f.name(),
|
||||
sample_field(f, coord, &data)
|
||||
));
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
pub fn sigmoid(x: f32)-> f32 {
|
||||
1.0 / (1.0 + (-x).exp())
|
||||
}
|
||||
|
||||
// Box-Muller transform
|
||||
pub fn normal(rng: &mut fastrand::Rng) -> f32 {
|
||||
let u = rng.f32();
|
||||
let v = rng.f32();
|
||||
((v * std::f32::consts::TAU).cos() * (-2.0 * u.ln()).sqrt()).clamp(-6.0, 6.0)
|
||||
}
|
||||
|
||||
pub fn normal_scaled(rng: &mut fastrand::Rng, mu: f32, sigma: f32) -> f32 {
|
||||
normal(rng) * sigma + mu
|
||||
}
|
||||
|
||||
pub mod config {
|
||||
// Runtime game logic (plants)
|
||||
pub const PLANT_TICK_DELAY: u64 = 128;
|
||||
pub const FIELD_DECAY_DELAY: u64 = 100;
|
||||
pub const PLANT_GROWTH_SCALE: f32 = 0.01;
|
||||
pub const SOIL_NUTRIENT_CONSUMPTION_RATE: f32 = 0.5;
|
||||
pub const SOIL_NUTRIENT_FIXATION_RATE: f32 = 0.002;
|
||||
pub const WATER_CONSUMPTION_RATE: f32 = 0.06;
|
||||
pub const PLANT_IDLE_WATER_CONSUMPTION_OFFSET: f32 = 0.05;
|
||||
pub const PLANT_DIEOFF_THRESHOLD: f32 = 0.3;
|
||||
pub const PLANT_DIEOFF_RATE: f32 = 0.2;
|
||||
pub const AUTOSAVE_INTERVAL_TICKS: u64 = 1024;
|
||||
pub const PLANT_POLLINATION_RADIUS: i32 = 12; // TODO: should be directional (wind, insects, etc) and vary by plant.
|
||||
pub const PLANT_POLLINATION_SCAN_FRACTION: f32 = 0.1;
|
||||
pub const PLANT_SEEDING_RADIUS: i32 = 5; // TODO: as above
|
||||
pub const PLANT_SEEDING_ATTEMPTS: usize = 10;
|
||||
pub const SOIL_NUTRIENT_DECOMP_RETURN_RATE: f32 = 1.0 / SOIL_NUTRIENT_CONSUMPTION_RATE * 0.6;
|
||||
pub const PLANT_LIFESPAN_SCALE: f32 = 1.0 / PLANT_GROWTH_SCALE;
|
||||
pub const PLANT_REPRODUCTION_ATTEMPT_COST: f32 = 0.01;
|
||||
pub const PLANT_CHILD_COST: f32 = 0.05; // TODO genome parameter, tie to initial size
|
||||
pub const INITIAL_PLANTS: usize = 262144;
|
||||
// Runtime game logic (misc)
|
||||
pub const VIEW: i32 = 15;
|
||||
pub const RANDOM_DESPAWN_INV_RATE: u64 = 4000;
|
||||
// Worldgen constants
|
||||
pub const WORLD_RADIUS: i32 = 1024;
|
||||
pub const NOISE_SCALE: f32 = 0.0005;
|
||||
pub const HEIGHT_EXPONENT: f32 = 0.3;
|
||||
pub const WATER_SOURCES: usize = 40;
|
||||
pub const CONTOUR_INTERVAL: f32 = 0.1;
|
||||
pub const SEA_LEVEL: f32 = -0.8;
|
||||
pub const EROSION: f32 = 0.09;
|
||||
pub const EROSION_EXPONENT: f32 = 1.5;
|
||||
pub const SALT_REMOVAL: f32 = 0.13;
|
||||
pub const SALT_RANGE: f32 = 0.2;
|
||||
pub const BASE_TEMPERATURE: f32 = 30.0; // degrees
|
||||
pub const HEIGHT_SCALE: f32 = 1e3; // unrealistic but makes world more interesting; m
|
||||
//const SEA_LEVEL_AIR_PRESSURE: f32 = 1013.0; // hPa
|
||||
//const PRESSURE_DROP_PER_METER: f32 = 0.001; // hPa m^-1
|
||||
pub const AIR_SPECIFIC_HEAT_CAPACITY: f32 = 1012.0; // J kg^-1 K^-1
|
||||
pub const EARTH_GRAVITY: f32 = 9.81; // m s^-2
|
||||
pub const NUTRIENT_NOISE_SCALE: f32 = 0.0015;
|
||||
}
|
||||
+2
-23
@@ -3,14 +3,9 @@ use std::{cmp::Ordering, collections::{hash_map::Entry, BinaryHeap, HashMap, Has
|
||||
use noise_functions::Sample3;
|
||||
use rayon::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::util::config::*;
|
||||
use crate::map::*;
|
||||
|
||||
pub const WORLD_RADIUS: i32 = 1024;
|
||||
const NOISE_SCALE: f32 = 0.0005;
|
||||
const HEIGHT_EXPONENT: f32 = 0.3;
|
||||
const WATER_SOURCES: usize = 40;
|
||||
const CONTOUR_INTERVAL: f32 = 0.1;
|
||||
|
||||
pub fn height_baseline_with_radius(pos: Coord, radius: i32) -> f32 {
|
||||
let w_frac = (hex_distance(pos, Coord::origin()) as f32 / radius as f32).powf(3.0);
|
||||
let pos = to_cubic(pos);
|
||||
@@ -33,7 +28,7 @@ fn percentilize<F: Fn(f32) -> f32>(raw: &mut Map<f32>, postprocess: F) {
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize<F: Fn(f32) -> f32 + Sync>(raw: &mut Map<f32>, postprocess: F) {
|
||||
pub fn normalize<F: Fn(f32) -> f32 + Sync>(raw: &mut Map<f32>, postprocess: F) {
|
||||
let mut min = raw.iter_data().copied().min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();
|
||||
let mut max = raw.iter_data().copied().max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();
|
||||
if min == max {
|
||||
@@ -220,10 +215,6 @@ pub fn compute_groundwater(water: &Map<f32>, rain: &Map<f32>, heightmap: &Map<f3
|
||||
groundwater
|
||||
}
|
||||
|
||||
const SEA_LEVEL: f32 = -0.8;
|
||||
const EROSION: f32 = 0.09;
|
||||
const EROSION_EXPONENT: f32 = 1.5;
|
||||
|
||||
fn floodfill(src: Coord, all: &HashSet<Coord>) -> HashSet<Coord> {
|
||||
let mut out = HashSet::new();
|
||||
let mut queue = VecDeque::new();
|
||||
@@ -247,9 +238,6 @@ pub fn get_sea(heightmap: &Map<f32>) -> (HashSet<Coord>, HashSet<Coord>) {
|
||||
(sinks, sea)
|
||||
}
|
||||
|
||||
const SALT_REMOVAL: f32 = 0.13;
|
||||
const SALT_RANGE: f32 = 0.33;
|
||||
|
||||
pub fn simulate_water(heightmap: &mut Map<f32>, rain_map: &Map<f32>, sea: &HashSet<Coord>, sinks: &HashSet<Coord>) -> (Map<f32>, Map<f32>) {
|
||||
let mut watermap = Map::<f32>::new(heightmap.radius, 0.0);
|
||||
|
||||
@@ -346,8 +334,6 @@ pub fn simulate_water(heightmap: &mut Map<f32>, rain_map: &Map<f32>, sea: &HashS
|
||||
(watermap, salt)
|
||||
}
|
||||
|
||||
const NUTRIENT_NOISE_SCALE: f32 = 0.0015;
|
||||
|
||||
// As a handwave, define soil nutrients to be partly randomized and partly based on water.
|
||||
// This kind of sort of makes sense because nitrogen is partly fixed by plants, which would have grown in water-having areas.
|
||||
// Update: they covaried way too much so reducing this.
|
||||
@@ -378,13 +364,6 @@ fn august_roche_magnus(temperature: f32) -> f32 {
|
||||
6.1094 * f32::exp((17.625 * temperature) / (243.04 + temperature))
|
||||
}
|
||||
|
||||
const BASE_TEMPERATURE: f32 = 30.0; // degrees
|
||||
const HEIGHT_SCALE: f32 = 1e3; // unrealistic but makes world more interesting; m
|
||||
//const SEA_LEVEL_AIR_PRESSURE: f32 = 1013.0; // hPa
|
||||
//const PRESSURE_DROP_PER_METER: f32 = 0.001; // hPa m^-1
|
||||
const AIR_SPECIFIC_HEAT_CAPACITY: f32 = 1012.0; // J kg^-1 K^-1
|
||||
const EARTH_GRAVITY: f32 = 9.81; // m s^-2
|
||||
|
||||
pub fn simulate_air(heightmap: &Map<f32>, sea: &HashSet<Coord>, scan_dir: CoordVec, perpendicular_dir: CoordVec) -> (Map<f32>, Map<f32>, Map<f32>) {
|
||||
let radius = heightmap.radius;
|
||||
let start_pos = Coord::origin() + -scan_dir * radius;
|
||||
|
||||
Reference in New Issue
Block a user