mirror of
https://github.com/osmarks/ewo3.git
synced 2026-03-14 14:49:42 +00:00
Restructure data structures
This commit is contained in:
266
Cargo.lock
generated
266
Cargo.lock
generated
@@ -1,6 +1,6 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
@@ -35,6 +35,37 @@ version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
||||
|
||||
[[package]]
|
||||
name = "argh"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f384d96bfd3c0b3c41f24dae69ee9602c091d64fc432225cf5295b5abbe0036"
|
||||
dependencies = [
|
||||
"argh_derive",
|
||||
"argh_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "argh_derive"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "938e5f66269c1f168035e29ed3fb437b084e476465e9314a0328f4005d7be599"
|
||||
dependencies = [
|
||||
"argh_shared",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "argh_shared"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5127f8a5bc1cfb0faf1f6248491452b8a5b6901068d8da2d47cbb285986ae683"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.3.0"
|
||||
@@ -114,6 +145,15 @@ version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
|
||||
|
||||
[[package]]
|
||||
name = "castaway"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.98"
|
||||
@@ -222,6 +262,7 @@ name = "ewo3"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argh",
|
||||
"bincode",
|
||||
"euclid",
|
||||
"fastrand",
|
||||
@@ -230,6 +271,8 @@ dependencies = [
|
||||
"image",
|
||||
"indexmap",
|
||||
"lazy_static",
|
||||
"ndarray",
|
||||
"ndarray-conv",
|
||||
"noise-functions",
|
||||
"rayon",
|
||||
"seahash",
|
||||
@@ -287,7 +330,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -443,6 +486,16 @@ version = "0.4.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
|
||||
|
||||
[[package]]
|
||||
name = "matrixmultiply"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"rawpointer",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.2"
|
||||
@@ -470,12 +523,106 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndarray"
|
||||
version = "0.17.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d"
|
||||
dependencies = [
|
||||
"matrixmultiply",
|
||||
"num-complex",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"rawpointer",
|
||||
"rayon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndarray-conv"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a502892118d1e27f9c514f61c44dd6fe46821ef53cb12b5af9043e8ca97a786b"
|
||||
dependencies = [
|
||||
"castaway",
|
||||
"ndarray",
|
||||
"num",
|
||||
"realfft",
|
||||
"rustfft",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "noise-functions"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "822a69eedf004ac2f492119af7a8203790b1c9115b9a9ef6bcd0cde5d6783565"
|
||||
|
||||
[[package]]
|
||||
name = "num"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-complex",
|
||||
"num-integer",
|
||||
"num-iter",
|
||||
"num-rational",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-complex"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-iter"
|
||||
version = "0.1.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@@ -558,12 +705,36 @@ dependencies = [
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic-util"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||
|
||||
[[package]]
|
||||
name = "primal-check"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.85"
|
||||
@@ -575,9 +746,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.36"
|
||||
version = "1.0.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
|
||||
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -612,6 +783,12 @@ dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rawpointer"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.10.0"
|
||||
@@ -632,6 +809,15 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "realfft"
|
||||
version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f821338fddb99d089116342c46e9f1fbf3828dba077674613e734e01d6ea8677"
|
||||
dependencies = [
|
||||
"rustfft",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.1"
|
||||
@@ -647,6 +833,26 @@ version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||
|
||||
[[package]]
|
||||
name = "rustfft"
|
||||
version = "6.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21db5f9893e91f41798c88680037dba611ca6674703c1a18601b01a72c8adb89"
|
||||
dependencies = [
|
||||
"num-complex",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"primal-check",
|
||||
"strength_reduce",
|
||||
"transpose",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.18"
|
||||
@@ -682,7 +888,7 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -753,6 +959,12 @@ version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
|
||||
[[package]]
|
||||
name = "strength_reduce"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
@@ -766,9 +978,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.66"
|
||||
version = "2.0.87"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
|
||||
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -781,7 +993,16 @@ version = "1.0.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
"thiserror-impl 1.0.61",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -792,7 +1013,18 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -833,7 +1065,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -848,6 +1080,16 @@ dependencies = [
|
||||
"tungstenite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "transpose"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"strength_reduce",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.23.0"
|
||||
@@ -862,7 +1104,7 @@ dependencies = [
|
||||
"log",
|
||||
"rand",
|
||||
"sha1",
|
||||
"thiserror",
|
||||
"thiserror 1.0.61",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
@@ -1058,5 +1300,5 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
@@ -23,10 +23,13 @@ image = { version = "0.25", default-features = false, features = ["png"] }
|
||||
rayon = "1"
|
||||
bincode = { version = "2.0.0-rc.3", features = ["serde"] }
|
||||
smallvec = "1"
|
||||
argh = "0.1"
|
||||
ndarray-conv = "0.6.0"
|
||||
ndarray = "0.17.2"
|
||||
|
||||
[[bin]]
|
||||
name = "worldgen"
|
||||
path = "src/worldgen_test.rs"
|
||||
|
||||
[profile.release]
|
||||
debug = true
|
||||
debug = true
|
||||
|
||||
73
src/main.rs
73
src/main.rs
@@ -1,3 +1,6 @@
|
||||
#![feature(test)]
|
||||
extern crate test;
|
||||
|
||||
use hecs::{CommandBuffer, Entity, With, World};
|
||||
use futures_util::{stream::TryStreamExt, SinkExt, StreamExt};
|
||||
use indexmap::IndexMap;
|
||||
@@ -88,9 +91,25 @@ struct GameState {
|
||||
clients: Slab<Client>,
|
||||
ticks: u64,
|
||||
map: worldgen::GeneratedWorld,
|
||||
baseline_soil_nutrients: Map<f32>,
|
||||
baseline_water: Map<f32>,
|
||||
baseline_salt: Map<f32>,
|
||||
baseline_temperature: Map<f32>,
|
||||
dynamic_soil_nutrients: Map<f32>,
|
||||
dynamic_water: Map<f32>,
|
||||
positions: PositionIndex
|
||||
}
|
||||
|
||||
impl GameState {
|
||||
fn actual_water(&self, pos: Coord) -> f32 {
|
||||
self.baseline_water[pos] + self.dynamic_water[pos]
|
||||
}
|
||||
|
||||
fn actual_soil_nutrients(&self, pos: Coord) -> f32 {
|
||||
self.baseline_soil_nutrients[pos] + self.dynamic_soil_nutrients[pos]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
enum Item {
|
||||
Dirt,
|
||||
@@ -264,7 +283,10 @@ struct DespawnOnImpact;
|
||||
struct Inventory(indexmap::IndexMap<Item, u64>);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Plant(plant::Genome);
|
||||
struct Plant {
|
||||
genome: plant::Genome,
|
||||
current_size: f32
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct NewlyAdded; // ugly hack to work around ECS deficiencies
|
||||
@@ -381,6 +403,10 @@ impl StochasticNumber {
|
||||
}
|
||||
}
|
||||
|
||||
const PLANT_TICK_DELAY: u64 = 128;
|
||||
const FIELD_DECAY_DELAY: u64 = 100;
|
||||
const PLANT_GROWTH_SCALE: f32 = 0.01;
|
||||
|
||||
async fn game_tick(state: &mut GameState) -> Result<()> {
|
||||
let mut buffer = hecs::CommandBuffer::new();
|
||||
|
||||
@@ -391,6 +417,16 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
|
||||
|
||||
buffer.run_on(&mut state.world);
|
||||
|
||||
if state.ticks % FIELD_DECAY_DELAY == 0 {
|
||||
state.baseline_soil_nutrients.for_each_mut(|nutrients| *nutrients *= 0.999);
|
||||
} else if state.ticks % FIELD_DECAY_DELAY == 1 {
|
||||
state.baseline_water.for_each_mut(|water| *water *= 0.999);
|
||||
} else if state.ticks % FIELD_DECAY_DELAY == 2 {
|
||||
state.dynamic_soil_nutrients = smooth(&state.dynamic_soil_nutrients, 3);
|
||||
} else if state.ticks % FIELD_DECAY_DELAY == 3 {
|
||||
state.dynamic_water = smooth(&state.dynamic_water, 3);
|
||||
}
|
||||
|
||||
// Spawn enemies
|
||||
for (_entity, (pos, EnemyTarget { spawn_range, spawn_density, spawn_rate_inv, .. })) in state.world.query::<(&Position, &EnemyTarget)>().iter() {
|
||||
let pos = pos.head();
|
||||
@@ -445,6 +481,22 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
// Run plant simulations.
|
||||
for (entity, (pos, plant)) in state.world.query::<(&Position, &mut Plant)>().iter() {
|
||||
if (entity.id() as u64) % PLANT_TICK_DELAY == state.ticks % PLANT_TICK_DELAY {
|
||||
let pos = pos.head();
|
||||
let water = state.actual_soil_nutrients(pos);
|
||||
let soil_nutrients = state.actual_soil_nutrients(pos);
|
||||
let salt = state.baseline_salt[pos];
|
||||
let temperature = state.baseline_temperature[pos];
|
||||
let base_growth_rate = plant.genome.effective_growth_rate(soil_nutrients, water, temperature, salt);
|
||||
plant.current_size += base_growth_rate * PLANT_GROWTH_SCALE * plant.current_size.powf(-0.25); // allometric scaling law
|
||||
plant.current_size = plant.current_size.min(plant.genome.max_size);
|
||||
let can_reproduce = plant.current_size >= plant.genome.max_size * plant.genome.reproductive_size_fraction();
|
||||
//state.dynamic_soil_nutrients;
|
||||
}
|
||||
}
|
||||
|
||||
// Process enemy motion and ranged attacks
|
||||
for (entity, (pos, ranged, energy, jump)) in state.world.query::<hecs::With<(&Position, Option<&mut RangedAttack>, Option<&mut Energy>, Option<&Jump>), &Enemy>>().iter() {
|
||||
let pos = pos.head();
|
||||
@@ -826,12 +878,25 @@ async fn main() -> Result<()> {
|
||||
}
|
||||
};
|
||||
|
||||
let baseline_soil_nutrients = world.soil_nutrients.clone();
|
||||
let baseline_water = world.groundwater.clone();
|
||||
let baseline_salt = world.salt.clone();
|
||||
let baseline_temperature = world.temperature.clone();
|
||||
let dynamic_soil_nutrients = baseline_soil_nutrients.clone();
|
||||
let dynamic_water = baseline_water.clone();
|
||||
|
||||
let state = Arc::new(Mutex::new(GameState {
|
||||
world: World::new(),
|
||||
clients: Slab::new(),
|
||||
ticks: 0,
|
||||
positions: PositionIndex::new(world.radius),
|
||||
map: world
|
||||
map: world,
|
||||
baseline_soil_nutrients,
|
||||
baseline_water,
|
||||
baseline_salt,
|
||||
baseline_temperature,
|
||||
dynamic_soil_nutrients,
|
||||
dynamic_water
|
||||
}));
|
||||
|
||||
{
|
||||
@@ -844,7 +909,7 @@ async fn main() -> Result<()> {
|
||||
Render('+'),
|
||||
Health(100.0, 100.0),
|
||||
//ShrinkOnDeath,
|
||||
Plant(plant::Genome::random()),
|
||||
Plant { genome: plant::Genome::random(), current_size: 0.0 },
|
||||
NewlyAdded
|
||||
));
|
||||
}
|
||||
@@ -862,9 +927,11 @@ async fn main() -> Result<()> {
|
||||
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
|
||||
loop {
|
||||
let mut state = state.lock().await;
|
||||
let time = std::time::Instant::now();
|
||||
if let Err(e) = game_tick(&mut state).await {
|
||||
println!("{:?}", e);
|
||||
}
|
||||
println!("Tick time: {:?}", time.elapsed());
|
||||
interval.tick().await;
|
||||
}
|
||||
});
|
||||
|
||||
155
src/map.rs
155
src/map.rs
@@ -1,6 +1,9 @@
|
||||
use euclid::{Point3D, Point2D, Vector2D};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::{Index, IndexMut};
|
||||
use std::marker::PhantomData;
|
||||
use ndarray::prelude::*;
|
||||
use ndarray_conv::{ConvExt, ConvMode, PaddingMode};
|
||||
|
||||
pub struct AxialWorldSpace;
|
||||
pub struct CubicWorldSpace;
|
||||
@@ -60,22 +63,30 @@ pub fn count_hexes(x: i32) -> i32 {
|
||||
x*(x+1)*3+1
|
||||
}
|
||||
|
||||
struct CoordsIndexIterator {
|
||||
struct CoordIterator {
|
||||
radius: i32,
|
||||
index: usize,
|
||||
count: usize,
|
||||
r: i32,
|
||||
q: i32,
|
||||
max: usize
|
||||
}
|
||||
|
||||
impl Iterator for CoordsIndexIterator {
|
||||
type Item = (Coord, usize);
|
||||
struct CoordIterMut<'a, T> {
|
||||
coords: CoordIterator,
|
||||
data: *mut T,
|
||||
radius: i32,
|
||||
side_length: usize,
|
||||
_marker: PhantomData<&'a mut T>
|
||||
}
|
||||
|
||||
impl Iterator for CoordIterator {
|
||||
type Item = Coord;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.index == self.max {
|
||||
if self.count == self.max {
|
||||
return None;
|
||||
}
|
||||
let result = (Coord::new(self.q, self.r), self.index);
|
||||
self.index += 1;
|
||||
let result = Coord::new(self.q, self.r);
|
||||
self.count += 1;
|
||||
self.q += 1;
|
||||
if self.r < 0 && self.q == self.radius + 1 {
|
||||
self.r += 1;
|
||||
@@ -89,81 +100,143 @@ impl Iterator for CoordsIndexIterator {
|
||||
}
|
||||
}
|
||||
|
||||
// blame OpenAI for this, and also Mozilla
|
||||
impl<'a, T> Iterator for CoordIterMut<'a, T> {
|
||||
type Item = (Coord, &'a mut T);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let coord = self.coords.next()?;
|
||||
let q = (coord.x + self.radius) as usize;
|
||||
let r = (coord.y + self.radius) as usize;
|
||||
let i = q + r * self.side_length;
|
||||
unsafe {
|
||||
// CoordIterator yields each valid map coordinate once, so each backing index is yielded once.
|
||||
Some((coord, &mut *self.data.add(i)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Map<T> {
|
||||
pub data: Vec<T>,
|
||||
pub radius: i32
|
||||
data: Vec<T>,
|
||||
pub radius: i32,
|
||||
side_length: usize
|
||||
}
|
||||
|
||||
impl<T> Map<T> {
|
||||
pub fn new(radius: i32, fill: T) -> Map<T> where T: Clone {
|
||||
let size = count_hexes(radius) as usize;
|
||||
// We represent worlds using axial coordinates (+q is right, +r is down-right).
|
||||
// This results in a parallelogram shape.
|
||||
// However, the map is a hexagon for the purposes of gameplay, so the top-left and bottom-right corners are invalid.
|
||||
let side_length = (radius * 2 + 1) as usize;
|
||||
let size = side_length.pow(2);
|
||||
Map {
|
||||
data: vec![fill; size],
|
||||
radius
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_fn<S, F: FnMut(Coord) -> S>(mut f: F, radius: i32) -> Map<S> {
|
||||
let size = count_hexes(radius) as usize;
|
||||
Map {
|
||||
radius,
|
||||
data: Vec::from_iter(CoordsIndexIterator {
|
||||
radius,
|
||||
index: 0,
|
||||
max: size,
|
||||
r: -radius,
|
||||
q: 0
|
||||
}.map(|(c, _i)| f(c)))
|
||||
side_length
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map<S, F: FnMut(&T) -> S>(mut f: F, other: &Self) -> Map<S> {
|
||||
pub fn size(&self) -> usize {
|
||||
count_hexes(self.radius) as usize
|
||||
}
|
||||
|
||||
pub fn from_fn<S: Default + Clone, F: FnMut(Coord) -> S>(mut f: F, radius: i32) -> Map<S> {
|
||||
let mut map = Map::new(radius, S::default());
|
||||
for coord in map.iter_coords() {
|
||||
map[coord] = f(coord);
|
||||
}
|
||||
map
|
||||
}
|
||||
|
||||
pub fn map<S: Default + Clone, F: FnMut(&T) -> S>(mut f: F, other: &Self) -> Map<S> {
|
||||
Map::<S>::from_fn(|c| f(&other[c]), other.radius)
|
||||
}
|
||||
|
||||
pub fn coord_to_index(&self, c: Coord) -> usize {
|
||||
let r = c.y + self.radius;
|
||||
let fh = r.min(self.radius);
|
||||
let mut coords_above = fh*(self.radius+1) + fh*(fh-1)/2;
|
||||
if fh < r {
|
||||
let d = r - fh;
|
||||
coords_above += d*(2*self.radius+1) - d*(d-1)/2;
|
||||
}
|
||||
let q_start = if r < self.radius { -r } else { -self.radius };
|
||||
(coords_above + (c.x - q_start)) as usize
|
||||
let q = (c.x + self.radius) as usize;
|
||||
let r = (c.y + self.radius) as usize;
|
||||
q * self.side_length + r
|
||||
}
|
||||
|
||||
pub fn in_range(&self, coord: Coord) -> bool {
|
||||
hex_distance(coord, Coord::origin()) <= self.radius
|
||||
}
|
||||
|
||||
pub fn iter_coords(&self) -> impl Iterator<Item=(Coord, usize)> {
|
||||
CoordsIndexIterator {
|
||||
pub fn iter_coords(&self) -> impl Iterator<Item=Coord> {
|
||||
CoordIterator {
|
||||
radius: self.radius,
|
||||
index: 0,
|
||||
max: self.data.len(),
|
||||
count: 0,
|
||||
max: self.size(),
|
||||
r: -self.radius,
|
||||
q: 0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item=(Coord, &T)> {
|
||||
self.iter_coords().map(|(c, i)| (c, &self.data[i]))
|
||||
self.iter_coords().map(|c| (c, &self[c]))
|
||||
}
|
||||
|
||||
pub fn iter_data(&self) -> impl Iterator<Item=&T> {
|
||||
self.iter_coords().map(|c| &self[c])
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item=(Coord, &mut T)> {
|
||||
CoordIterMut {
|
||||
coords: CoordIterator {
|
||||
radius: self.radius,
|
||||
count: 0,
|
||||
max: self.size(),
|
||||
r: -self.radius,
|
||||
q: 0
|
||||
},
|
||||
data: self.data.as_mut_ptr(),
|
||||
radius: self.radius,
|
||||
side_length: self.side_length,
|
||||
_marker: PhantomData
|
||||
}
|
||||
}
|
||||
|
||||
pub fn for_each_mut(&mut self, mut f: impl FnMut(&mut T)) {
|
||||
for (_, value) in self.iter_mut() {
|
||||
f(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2D hex convolution
|
||||
pub fn smooth(map: &Map<f32>, radius: i32) -> Map<f32> {
|
||||
//let mut map = map.clone();
|
||||
//map[Coord::new(1, 0)] = 3.0;
|
||||
let data: ArrayBase<ndarray::ViewRepr<&f32>, Dim<[usize; 2]>, f32> = ArrayView2::from_shape((map.side_length, map.side_length), map.data.as_slice()).unwrap();
|
||||
//println!("{:?}", data[(map.radius as usize + 1, map.radius as usize)]);
|
||||
let mut kernel = Array2::from_elem((radius as usize * 2 + 1, radius as usize * 2 + 1), 0.0);
|
||||
|
||||
for (_, offset) in hex_range(radius) {
|
||||
kernel[(offset.x as usize + radius as usize, offset.y as usize + radius as usize)] = 1.0 / count_hexes(radius) as f32;
|
||||
}
|
||||
|
||||
let result = ConvExt::conv(&data, &kernel, ConvMode::Same, PaddingMode::Zeros).unwrap();
|
||||
|
||||
Map {
|
||||
radius: map.radius,
|
||||
side_length: map.side_length,
|
||||
data: result.into_raw_vec(), // TODO fix
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Index<Coord> for Map<T> {
|
||||
type Output = T;
|
||||
fn index(&self, index: Coord) -> &Self::Output {
|
||||
//println!("{:?}", index);
|
||||
debug_assert!(self.in_range(index), "invalid coord: {:?}", index);
|
||||
&self.data[self.coord_to_index(index)]
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IndexMut<Coord> for Map<T> {
|
||||
fn index_mut(&mut self, index: Coord) -> &mut Self::Output {
|
||||
debug_assert!(self.in_range(index), "invalid coord: {:?}", index);
|
||||
let i = self.coord_to_index(index);
|
||||
&mut self.data[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
40
src/plant.rs
40
src/plant.rs
@@ -9,15 +9,17 @@ pub enum CropType {
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Genome {
|
||||
crop_type: CropType,
|
||||
// polygenic traits; parameterized as N(0,1)
|
||||
// polygenic traits; parameterized as N(0,1) (allegedly)
|
||||
growth_rate: f32,
|
||||
nitrogen_fixation_rate: f32,
|
||||
nutrient_addition_rate: f32,
|
||||
optimal_water_level: f32,
|
||||
optimal_temperature: f32,
|
||||
reproduction_rate: f32,
|
||||
reproductive_size_fraction_param: f32,
|
||||
temperature_tolerance: f32,
|
||||
water_tolerance: f32,
|
||||
max_size: f32
|
||||
salt_tolerance: f32,
|
||||
pub max_size: f32
|
||||
// TODO color trait
|
||||
}
|
||||
|
||||
@@ -37,16 +39,24 @@ fn normal_scaled(mu: f32, sigma: f32) -> f32 {
|
||||
}
|
||||
|
||||
impl Genome {
|
||||
pub fn effective_growth_rate(&self, water: f32, temperature: f32) -> f32 {
|
||||
pub fn reproductive_size_fraction(&self) -> f32 {
|
||||
sigmoid(self.reproductive_size_fraction_param)
|
||||
}
|
||||
|
||||
pub fn effective_growth_rate(&self, nutrients: f32, water: f32, temperature: f32, salt: f32) -> f32 {
|
||||
let water_diff = (water - self.optimal_water_level).abs();
|
||||
let temperature_diff = (temperature - self.optimal_temperature).abs();
|
||||
1.5f32.powf(self.growth_rate)
|
||||
let salt_excess = (salt - sigmoid(self.salt_tolerance)).max(0.0);
|
||||
let base = 1.5f32.powf(self.growth_rate)
|
||||
- self.reproduction_rate * 0.1 // faster reproduction trades off slightly against growth
|
||||
- self.nitrogen_fixation_rate.max(0.0) * 0.16 // same for nitrogen fixation
|
||||
- self.nutrient_addition_rate.max(0.0) * 0.16 // nutrient enrichment has a growth tradeoff
|
||||
- (water_diff - sigmoid(self.water_tolerance)).max(0.0) // penalize plants when far from optimal environmental range
|
||||
- (temperature_diff - sigmoid(self.temperature_tolerance)).max(0.0) // same for temperature
|
||||
- salt_excess
|
||||
- self.water_tolerance * 0.2
|
||||
- self.temperature_tolerance * 0.2
|
||||
- self.salt_tolerance * 0.2;
|
||||
base * (-nutrients.min(0.0)).exp()
|
||||
}
|
||||
|
||||
pub fn random() -> Genome {
|
||||
@@ -58,23 +68,25 @@ impl Genome {
|
||||
_ => unreachable!()
|
||||
};
|
||||
|
||||
let (nitrogen_fixation_rate, optimal_water_level, optimal_temperature, max_size) = match crop_type {
|
||||
CropType::Grass => (-10.0, 0.0, 0.0, 0.0),
|
||||
CropType::EucalyptusTree => (-10.0, 2.0, 1.0, 5.0),
|
||||
CropType::BushTomato => (-10.0, -1.0, 1.5, 1.0),
|
||||
CropType::GoldenWattleTree => (2.0, 1.5, 1.0, 3.0),
|
||||
let (nutrient_addition_rate, optimal_water_level, optimal_temperature, reproductive_size_fraction_param, salt_tolerance, max_size) = match crop_type {
|
||||
CropType::Grass => (-10.0, 0.0, 0.0, -1.0, 0.2, 0.0),
|
||||
CropType::EucalyptusTree => (-10.0, 2.0, 1.0, 1.0, 1.3, 5.0),
|
||||
CropType::BushTomato => (-10.0, -1.0, 1.5, -0.3, 1.6, 1.0),
|
||||
CropType::GoldenWattleTree => (2.0, 1.5, 1.0, 0.5, 0.9, 3.0),
|
||||
|
||||
};
|
||||
|
||||
Genome {
|
||||
crop_type: crop_type,
|
||||
growth_rate: normal(),
|
||||
nitrogen_fixation_rate,
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -84,12 +96,14 @@ impl Genome {
|
||||
Some(Genome {
|
||||
crop_type: self.crop_type,
|
||||
growth_rate: (self.growth_rate + other.growth_rate) / 2.0 + normal_scaled(0.0, 0.1),
|
||||
nitrogen_fixation_rate: (self.nitrogen_fixation_rate + other.nitrogen_fixation_rate) / 2.0 + normal_scaled(0.0, 0.03),
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
202
src/worldgen.rs
202
src/worldgen.rs
@@ -1,6 +1,7 @@
|
||||
use std::{cmp::Ordering, collections::{hash_map::Entry, BinaryHeap, HashMap, HashSet, VecDeque}, hash::{Hash, Hasher}};
|
||||
|
||||
use noise_functions::Sample3;
|
||||
use rayon::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::map::*;
|
||||
|
||||
@@ -9,38 +10,46 @@ const NOISE_SCALE: f32 = 0.0005;
|
||||
const HEIGHT_EXPONENT: f32 = 0.3;
|
||||
const WATER_SOURCES: usize = 40;
|
||||
|
||||
pub fn height_baseline(pos: Coord) -> f32 {
|
||||
let w_frac = (hex_distance(pos, Coord::origin()) as f32 / WORLD_RADIUS as f32).powf(3.0);
|
||||
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);
|
||||
let noise =
|
||||
let noise =
|
||||
noise_functions::OpenSimplex2s.ridged(6, 0.7, 1.55).seed(406).sample3([10.0 + pos.x as f32 * NOISE_SCALE, pos.y as f32 * NOISE_SCALE, pos.z as f32 * NOISE_SCALE]);
|
||||
let range = 1.0 - 2.0 * (w_frac - 0.5).powf(2.0);
|
||||
noise * range - w_frac
|
||||
}
|
||||
|
||||
pub fn height_baseline(pos: Coord) -> f32 {
|
||||
height_baseline_with_radius(pos, WORLD_RADIUS)
|
||||
}
|
||||
|
||||
fn percentilize<F: Fn(f32) -> f32>(raw: &mut Map<f32>, postprocess: F) {
|
||||
let mut xs: Vec<(usize, f32)> = raw.data.iter().copied().enumerate().collect();
|
||||
let mut xs: Vec<(Coord, f32)> = raw.iter().map(|(coord, value)| (coord, *value)).collect();
|
||||
xs.sort_unstable_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
|
||||
for (j, (i, _x)) in xs.into_iter().enumerate() {
|
||||
let percentile = j as f32 / raw.data.len() as f32;
|
||||
raw.data[i] = postprocess(percentile);
|
||||
let percentile = j as f32 / raw.size() as f32;
|
||||
raw[i] = postprocess(percentile);
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize<F: Fn(f32) -> f32>(raw: &mut Map<f32>, postprocess: F) {
|
||||
let mut min = raw.data.iter().copied().min_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();
|
||||
let mut max = raw.data.iter().copied().max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap();
|
||||
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 {
|
||||
min = 0.0;
|
||||
max = 1.0;
|
||||
}
|
||||
for x in raw.data.iter_mut() {
|
||||
for (_c, x) in raw.iter_mut() {
|
||||
*x = postprocess((*x - min) / (max - min));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_heights() -> Map<f32> {
|
||||
let mut raw = Map::<f32>::from_fn(height_baseline, WORLD_RADIUS);
|
||||
generate_heights_with_radius(WORLD_RADIUS)
|
||||
}
|
||||
|
||||
pub fn generate_heights_with_radius(radius: i32) -> Map<f32> {
|
||||
let mut raw = Map::<f32>::from_fn(|pos| height_baseline_with_radius(pos, radius), radius);
|
||||
percentilize(&mut raw, |x| {
|
||||
let s = 1.0 - (1.0 - x).powf(HEIGHT_EXPONENT);
|
||||
s * 2.0 - 1.0
|
||||
@@ -49,30 +58,35 @@ pub fn generate_heights() -> Map<f32> {
|
||||
}
|
||||
|
||||
pub fn generate_contours(field: &Map<f32>, interval: f32) -> Vec<(Coord, f32, f32, CoordVec)> {
|
||||
let mut v = vec![];
|
||||
// Starting at the origin, we want to detect contour lines in any of the six directions.
|
||||
// Go in one of the perpendicular directions to generate base directions then scan until the edge is reached starting from any of those points.
|
||||
for scan_direction in DIRECTIONS {
|
||||
// "perpendicular" doesn't really work in axial coordinates, but this apparently does so whatever
|
||||
let slit_direction = rotate_60(*scan_direction);
|
||||
for x in 0..field.radius {
|
||||
let base = Coord::zero() + slit_direction * x;
|
||||
let mut last: Option<f32> = None;
|
||||
for y in 0..field.radius {
|
||||
let point = base + *scan_direction * y;
|
||||
if hex_distance(point, Coord::zero()) <= field.radius {
|
||||
let sample = field[point] / interval;
|
||||
if let Some(last) = last {
|
||||
if last.trunc() != sample.trunc() {
|
||||
v.push((point, last, sample, *scan_direction));
|
||||
DIRECTIONS.par_iter()
|
||||
.map(|scan_direction| {
|
||||
let mut out = vec![];
|
||||
// "perpendicular" doesn't really work in axial coordinates, but this apparently does so whatever
|
||||
let slit_direction = rotate_60(*scan_direction);
|
||||
for x in 0..field.radius {
|
||||
let base = Coord::zero() + slit_direction * x;
|
||||
let mut last: Option<f32> = None;
|
||||
for y in 0..field.radius {
|
||||
let point = base + *scan_direction * y;
|
||||
if hex_distance(point, Coord::zero()) <= field.radius {
|
||||
let sample = field[point] / interval;
|
||||
if let Some(last) = last {
|
||||
if last.trunc() != sample.trunc() {
|
||||
out.push((point, last, sample, *scan_direction));
|
||||
}
|
||||
}
|
||||
last = Some(sample);
|
||||
}
|
||||
last = Some(sample);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
v
|
||||
out
|
||||
})
|
||||
.reduce(Vec::new, |mut acc, mut chunk| {
|
||||
acc.append(&mut chunk);
|
||||
acc
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@@ -109,7 +123,7 @@ impl<T: Hash + Eq + PartialEq> PartialOrd for PointWrapper<T> {
|
||||
|
||||
pub fn generate_separated_high_points(n: usize, sep: i32, map: &Map<f32>) -> Vec<Coord> {
|
||||
let mut points = vec![];
|
||||
let mut priority = BinaryHeap::with_capacity(map.data.len());
|
||||
let mut priority = BinaryHeap::with_capacity(map.size());
|
||||
for (coord, height) in map.iter() {
|
||||
priority.push(PointWrapper(-*height, coord));
|
||||
}
|
||||
@@ -192,12 +206,13 @@ pub fn distance_map<I: Iterator<Item=Coord>>(radius: i32, sources: I) -> Map<f32
|
||||
|
||||
pub fn compute_groundwater(water: &Map<f32>, rain: &Map<f32>, heightmap: &Map<f32>) -> Map<f32> {
|
||||
let mut groundwater = distance_map(
|
||||
water.radius,
|
||||
water.radius,
|
||||
water.iter().filter_map(|(c, i)| if *i > 0.0 { Some(c) } else { None }));
|
||||
percentilize(&mut groundwater, |x| (1.0 - x).powf(0.3));
|
||||
for (coord, h) in heightmap.iter() {
|
||||
groundwater[coord] -= *h * 0.05;
|
||||
groundwater[coord] += rain[coord] * 0.15;
|
||||
|
||||
for (coord, gw) in groundwater.iter_mut() {
|
||||
*gw -= heightmap[coord] * 0.05;
|
||||
*gw += rain[coord] * 0.15;
|
||||
}
|
||||
percentilize(&mut groundwater, |x| x.powf(0.7));
|
||||
groundwater
|
||||
@@ -225,18 +240,18 @@ fn floodfill(src: Coord, all: &HashSet<Coord>) -> HashSet<Coord> {
|
||||
}
|
||||
|
||||
pub fn get_sea(heightmap: &Map<f32>) -> (HashSet<Coord>, HashSet<Coord>) {
|
||||
let sinks = heightmap.iter_coords().filter(|(c, _)| heightmap[*c] <= SEA_LEVEL).map(|(c, _)| c).collect::<HashSet<_>>();
|
||||
let sea = floodfill(Coord::new(0, WORLD_RADIUS), &sinks);
|
||||
let sinks = heightmap.iter_coords().filter(|c| heightmap[*c] <= SEA_LEVEL).collect::<HashSet<_>>();
|
||||
let sea = floodfill(Coord::new(0, heightmap.radius), &sinks);
|
||||
(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>) {
|
||||
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);
|
||||
|
||||
let sources = generate_separated_high_points(WATER_SOURCES, WORLD_RADIUS / 10, &rain_map);
|
||||
let sources = generate_separated_high_points(WATER_SOURCES, (heightmap.radius / 10).max(1), rain_map);
|
||||
let mut remainder = sinks.clone();
|
||||
|
||||
for s in sea.iter() {
|
||||
@@ -258,13 +273,12 @@ pub fn simulate_water(heightmap: &mut Map<f32>, rain_map: &Map<f32>, sea: &HashS
|
||||
}
|
||||
}
|
||||
|
||||
let mut salt = distance_map(watermap.radius, sea.iter().copied());
|
||||
let mut salt = distance_map(heightmap.radius, sea.iter().copied());
|
||||
normalize(&mut salt, |x| (SALT_RANGE - x).max(0.0) / SALT_RANGE);
|
||||
|
||||
for (coord, rain) in rain_map.iter() {
|
||||
if *rain > 0.0 {
|
||||
salt[coord] -= *rain * 0.3;
|
||||
salt[coord] = salt[coord].max(0.0);
|
||||
for (coord, salt) in salt.iter_mut() {
|
||||
let rain = rain_map[coord];
|
||||
if rain > 0.0 {
|
||||
*salt = (*salt - rain * 0.3).max(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,9 +348,13 @@ 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.
|
||||
pub fn soil_nutrients(groundwater: &Map<f32>) -> Map<f32> {
|
||||
let mut soil_nutrients = Map::<f32>::from_fn(|cr| {
|
||||
let c = to_cubic(cr);
|
||||
noise_functions::OpenSimplex2s.seed(406).sample3([10.0 + c.x as f32 * NUTRIENT_NOISE_SCALE, c.y as f32 * NUTRIENT_NOISE_SCALE, c.z as f32 * NUTRIENT_NOISE_SCALE]) + groundwater[cr]
|
||||
let mut soil_nutrients = Map::<f32>::from_fn(|d| {
|
||||
let c = to_cubic(d);
|
||||
noise_functions::OpenSimplex2s.seed(406).sample3([
|
||||
10.0 + c.x as f32 * NUTRIENT_NOISE_SCALE,
|
||||
c.y as f32 * NUTRIENT_NOISE_SCALE,
|
||||
c.z as f32 * NUTRIENT_NOISE_SCALE
|
||||
]) + groundwater[d]
|
||||
}, groundwater.radius);
|
||||
percentilize(&mut soil_nutrients, |x| x.powf(0.4));
|
||||
soil_nutrients
|
||||
@@ -356,22 +374,6 @@ fn august_roche_magnus(temperature: f32) -> f32 {
|
||||
6.1094 * f32::exp((17.625 * temperature) / (243.04 + temperature))
|
||||
}
|
||||
|
||||
// 2D hex convolution
|
||||
fn smooth(map: &Map<f32>, radius: i32) -> Map<f32> {
|
||||
let mut out = Map::<f32>::new(map.radius, 0.0);
|
||||
for (coord, index) in map.iter_coords() {
|
||||
let mut sum = map.data[index];
|
||||
for (_, offset) in hex_range(radius) {
|
||||
let neighbor = coord + offset;
|
||||
if map.in_range(neighbor) {
|
||||
sum += map[neighbor];
|
||||
}
|
||||
}
|
||||
out.data[index] = sum / (count_hexes(radius) as f32);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
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
|
||||
@@ -379,12 +381,13 @@ const HEIGHT_SCALE: f32 = 1e3; // unrealistic but makes world more interesting;
|
||||
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 start_pos = Coord::origin() + -scan_dir * WORLD_RADIUS;
|
||||
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;
|
||||
let mut rain_map = Map::<f32>::new(heightmap.radius, 0.0);
|
||||
let mut temperature_map = Map::<f32>::new(heightmap.radius, 0.0);
|
||||
let mut atmo_humidity = Map::<f32>::new(heightmap.radius, 0.0); // relative humidity
|
||||
let mut frontier = (-WORLD_RADIUS..=WORLD_RADIUS).map(|x| WindSlice {
|
||||
let mut frontier = (-radius..=radius).map(|x| WindSlice {
|
||||
coord: start_pos + perpendicular_dir * x,
|
||||
humidity: 0.0,
|
||||
temperature: 0.0,
|
||||
@@ -396,6 +399,8 @@ pub fn simulate_air(heightmap: &Map<f32>, sea: &HashSet<Coord>, scan_dir: CoordV
|
||||
// We treat it as a line advancing and gaining/losing water and temperature.
|
||||
// Water is lost when the partial pressure of water in the air is greater than the saturation vapour pressure.
|
||||
// Temperature changes with height based on a slightly dubious equation I derived.
|
||||
// TODO: recalculate this; Wikipedia says temperature change is actually
|
||||
// because of air pressure changes and not directly derived from GPE.
|
||||
for slice in frontier.iter_mut() {
|
||||
if heightmap.in_range(slice.coord) {
|
||||
any_in_range = true;
|
||||
@@ -408,7 +413,7 @@ pub fn simulate_air(heightmap: &Map<f32>, sea: &HashSet<Coord>, scan_dir: CoordV
|
||||
slice.temperature = BASE_TEMPERATURE;
|
||||
slice.humidity = max_water * 0.9;
|
||||
slice.last_height = SEA_LEVEL;
|
||||
|
||||
|
||||
} else {
|
||||
let excess = (slice.humidity - max_water).max(0.0);
|
||||
slice.humidity -= excess;
|
||||
@@ -431,16 +436,26 @@ pub fn simulate_air(heightmap: &Map<f32>, sea: &HashSet<Coord>, scan_dir: CoordV
|
||||
let mut next_temperature = vec![0.0; frontier.len()];
|
||||
let mut next_humidity = vec![0.0; frontier.len()];
|
||||
// Smooth out temperature and humidity.
|
||||
for i in 1..(frontier.len()-1) {
|
||||
next_temperature[i] = 1.0/3.0 * (frontier[i-1].temperature + frontier[i+1].temperature + frontier[i].temperature);
|
||||
next_humidity[i] = 1.0/3.0 * (frontier[i-1].humidity + frontier[i+1].humidity + frontier[i].humidity);
|
||||
}
|
||||
// TODO at some point: replace with generalized convolution
|
||||
let frontier_len = frontier.len();
|
||||
next_temperature.par_iter_mut().enumerate().for_each(|(i, out)| {
|
||||
if i == 0 || i + 1 >= frontier_len {
|
||||
return;
|
||||
}
|
||||
*out = 1.0/3.0 * (frontier[i-1].temperature + frontier[i+1].temperature + frontier[i].temperature);
|
||||
});
|
||||
next_humidity.par_iter_mut().enumerate().for_each(|(i, out)| {
|
||||
if i == 0 || i + 1 >= frontier_len {
|
||||
return;
|
||||
}
|
||||
*out = 1.0/3.0 * (frontier[i-1].humidity + frontier[i+1].humidity + frontier[i].humidity);
|
||||
});
|
||||
|
||||
for (i, slice) in frontier.iter_mut().enumerate() {
|
||||
frontier.par_iter_mut().enumerate().for_each(|(i, slice)| {
|
||||
slice.temperature = next_temperature[i];
|
||||
slice.humidity = next_humidity[i];
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
if !any_in_range { break; }
|
||||
}
|
||||
|
||||
@@ -463,13 +478,13 @@ pub enum TerrainType {
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GeneratedWorld {
|
||||
heightmap: Map<f32>,
|
||||
terrain: Map<TerrainType>,
|
||||
groundwater: Map<f32>,
|
||||
salt: Map<f32>,
|
||||
atmo_humidity: Map<f32>,
|
||||
temperature: Map<f32>,
|
||||
soil_nutrients: Map<f32>,
|
||||
pub heightmap: Map<f32>,
|
||||
pub terrain: Map<TerrainType>,
|
||||
pub groundwater: Map<f32>,
|
||||
pub salt: Map<f32>,
|
||||
pub atmo_humidity: Map<f32>,
|
||||
pub temperature: Map<f32>,
|
||||
pub soil_nutrients: Map<f32>,
|
||||
pub radius: i32
|
||||
}
|
||||
|
||||
@@ -514,11 +529,11 @@ pub fn generate_world() -> GeneratedWorld {
|
||||
}
|
||||
|
||||
impl TerrainType {
|
||||
pub fn entry_cost(&self) -> Option<i64> {
|
||||
pub fn entry_cost(&self) -> Option<i64> {
|
||||
match *self {
|
||||
Self::Empty => Some(0),
|
||||
Self::Wall => None,
|
||||
Self::ShallowWater => Some(10),
|
||||
Self::ShallowWater => Some(10),
|
||||
Self::DeepWater => None,
|
||||
Self::Contour => Some(1)
|
||||
}
|
||||
@@ -538,15 +553,30 @@ impl TerrainType {
|
||||
impl GeneratedWorld {
|
||||
pub fn get_terrain(&self, pos: Coord) -> TerrainType {
|
||||
let distance = hex_distance(pos, Coord::origin());
|
||||
|
||||
|
||||
if distance >= self.heightmap.radius {
|
||||
return TerrainType::Wall
|
||||
}
|
||||
|
||||
|
||||
self.terrain[pos].clone()
|
||||
}
|
||||
|
||||
pub fn radius(&self) -> i32 {
|
||||
self.heightmap.radius
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{Map, smooth, WORLD_RADIUS};
|
||||
use test::bench::Bencher;
|
||||
|
||||
#[bench]
|
||||
fn bench_smooth_map(b: &mut Bencher) {
|
||||
use std::hint::black_box;
|
||||
b.iter(|| {
|
||||
let map = black_box(Map::new(WORLD_RADIUS, 0.0f32));
|
||||
smooth(&map, 3);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,204 @@
|
||||
#![feature(test)]
|
||||
extern crate test;
|
||||
|
||||
use anyhow::Result;
|
||||
use argh::FromArgs;
|
||||
use image::{ImageBuffer, Rgb};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::HashMap;
|
||||
use std::time::Instant;
|
||||
|
||||
mod worldgen;
|
||||
mod map;
|
||||
|
||||
use worldgen::*;
|
||||
use map::*;
|
||||
use worldgen::*;
|
||||
|
||||
#[derive(FromArgs)]
|
||||
/// Render worldgen debug fields to a PNG.
|
||||
struct Args {
|
||||
/// world radius
|
||||
#[argh(option, default = "WORLD_RADIUS")]
|
||||
radius: i32,
|
||||
/// output path
|
||||
#[argh(option, default = "String::from(\"./out.png\")")]
|
||||
output: String,
|
||||
/// first channel field (defaults to 0 if omitted)
|
||||
#[argh(option)]
|
||||
c1: Option<String>,
|
||||
/// second channel field (defaults to 0 if omitted)
|
||||
#[argh(option)]
|
||||
c2: Option<String>,
|
||||
/// third channel field (defaults to 0 if omitted)
|
||||
#[argh(option)]
|
||||
c3: Option<String>,
|
||||
/// color space: rgb or oklab
|
||||
#[argh(option, default = "String::from(\"oklab\")")]
|
||||
color_space: String,
|
||||
/// percentile-normalize each selected channel
|
||||
#[argh(switch)]
|
||||
normalize: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Field {
|
||||
Height,
|
||||
Rain,
|
||||
Water,
|
||||
Groundwater,
|
||||
Salt,
|
||||
Temperature,
|
||||
Humidity,
|
||||
Soil,
|
||||
Contour,
|
||||
SeaDistance,
|
||||
}
|
||||
|
||||
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),
|
||||
_ => anyhow::bail!("unknown field: {s}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RenderData<'a> {
|
||||
heightmap: &'a Map<f32>,
|
||||
rain: &'a Map<f32>,
|
||||
water: &'a Map<f32>,
|
||||
groundwater: &'a Map<f32>,
|
||||
salt: &'a Map<f32>,
|
||||
temperature: &'a Map<f32>,
|
||||
humidity: &'a Map<f32>,
|
||||
soil: &'a Map<f32>,
|
||||
contour_points: &'a HashMap<Coord, u8>,
|
||||
sea_distance: &'a Map<f32>,
|
||||
}
|
||||
|
||||
fn sample_field(field: Option<Field>, position: Coord, data: &RenderData) -> f32 {
|
||||
match field {
|
||||
None => 0.0,
|
||||
Some(field) => match field {
|
||||
Field::Height => ((data.heightmap[position] + 1.0) * 0.5).clamp(0.0, 1.0),
|
||||
Field::Rain => data.rain[position].clamp(0.0, 1.0),
|
||||
Field::Water => data.water[position].min(1.0),
|
||||
Field::Groundwater => data.groundwater[position].clamp(0.0, 1.0),
|
||||
Field::Salt => data.salt[position].clamp(0.0, 1.0),
|
||||
Field::Temperature => data.temperature[position].clamp(0.0, 1.0),
|
||||
Field::Humidity => data.humidity[position].clamp(0.0, 1.0),
|
||||
Field::Soil => data.soil[position].clamp(0.0, 1.0),
|
||||
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),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn field_range(field: Option<Field>, data: &RenderData) -> (f32, f32) {
|
||||
if field.is_none() {
|
||||
return (0.0, 1.0);
|
||||
}
|
||||
let mut min = f32::INFINITY;
|
||||
let mut max = f32::NEG_INFINITY;
|
||||
for (position, _) in data.heightmap.iter() {
|
||||
let v = sample_field(field, position, data);
|
||||
min = min.min(v);
|
||||
max = max.max(v);
|
||||
}
|
||||
if (max - min).abs() < f32::EPSILON {
|
||||
(0.0, 1.0)
|
||||
} else {
|
||||
(min, max)
|
||||
}
|
||||
}
|
||||
|
||||
fn to_rgb(c1: f32, c2: f32, c3: f32, color_space: &str) -> [u8; 3] {
|
||||
fn linear_to_srgb(x: f32) -> f32 {
|
||||
if x <= 0.0031308 {
|
||||
12.92 * x
|
||||
} else {
|
||||
1.055 * x.powf(1.0 / 2.4) - 0.055
|
||||
}
|
||||
}
|
||||
|
||||
let (r, g, b) = match color_space {
|
||||
"rgb" => (c1, c2, c3),
|
||||
"oklab" => {
|
||||
let l = c1.clamp(0.0, 1.0);
|
||||
let a = c2 * 2.0 - 1.0;
|
||||
let b = c3 * 2.0 - 1.0;
|
||||
|
||||
let l_ = l + 0.3963377774 * a + 0.2158037573 * b;
|
||||
let m_ = l - 0.1055613458 * a - 0.0638541728 * b;
|
||||
let s_ = l - 0.0894841775 * a - 1.2914855480 * b;
|
||||
|
||||
let l3 = l_ * l_ * l_;
|
||||
let m3 = m_ * m_ * m_;
|
||||
let s3 = s_ * s_ * s_;
|
||||
|
||||
let r_lin = 4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3;
|
||||
let g_lin = -1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3;
|
||||
let b_lin = -0.0041960863 * l3 - 0.7034186147 * m3 + 1.7076147010 * s3;
|
||||
|
||||
(
|
||||
linear_to_srgb(r_lin).clamp(0.0, 1.0),
|
||||
linear_to_srgb(g_lin).clamp(0.0, 1.0),
|
||||
linear_to_srgb(b_lin).clamp(0.0, 1.0),
|
||||
)
|
||||
}
|
||||
_ => (c1, c2, c3),
|
||||
};
|
||||
[(r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8]
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut heightmap = generate_heights();
|
||||
let (sinks, sea) = get_sea(&heightmap);
|
||||
let total_start = Instant::now();
|
||||
let args: Args = argh::from_env();
|
||||
let f1 = args.c1.as_deref().map(Field::parse).transpose()?;
|
||||
let f2 = args.c2.as_deref().map(Field::parse).transpose()?;
|
||||
let f3 = args.c3.as_deref().map(Field::parse).transpose()?;
|
||||
let radius = args.radius.max(1);
|
||||
|
||||
println!("wind...");
|
||||
let (rain, temperature, atmo_humidity) = simulate_air(&heightmap, &sea, CoordVec::new(0, -1), CoordVec::new(1, 0));
|
||||
let t = Instant::now();
|
||||
let mut heightmap = generate_heights_with_radius(radius);
|
||||
println!("heights: {:.3}s", t.elapsed().as_secs_f32());
|
||||
|
||||
println!("hydro...");
|
||||
let hydro_start = Instant::now();
|
||||
let t = Instant::now();
|
||||
let (sinks, sea) = get_sea(&heightmap);
|
||||
println!(" sea/sinks: {:.3}s", t.elapsed().as_secs_f32());
|
||||
let t = Instant::now();
|
||||
let (rain, temperature, atmo_humidity) =
|
||||
simulate_air(&heightmap, &sea, CoordVec::new(0, -1), CoordVec::new(1, 0));
|
||||
println!(" air: {:.3}s", t.elapsed().as_secs_f32());
|
||||
let t = Instant::now();
|
||||
let (water, salt) = simulate_water(&mut heightmap, &rain, &sea, &sinks);
|
||||
println!(" water: {:.3}s", t.elapsed().as_secs_f32());
|
||||
let t = Instant::now();
|
||||
let groundwater = compute_groundwater(&water, &rain, &heightmap);
|
||||
println!(" groundwater: {:.3}s", t.elapsed().as_secs_f32());
|
||||
let t = Instant::now();
|
||||
let mut sea_distance = distance_map(heightmap.radius, sea.iter().copied());
|
||||
let radius = heightmap.radius as f32;
|
||||
for (_, value) in sea_distance.iter_mut() {
|
||||
*value = (*value / radius).clamp(0.0, 1.0);
|
||||
}
|
||||
println!(" sea distance: {:.3}s", t.elapsed().as_secs_f32());
|
||||
println!("hydro total: {:.3}s", hydro_start.elapsed().as_secs_f32());
|
||||
|
||||
println!("contours...");
|
||||
let t = Instant::now();
|
||||
let contours = generate_contours(&heightmap, 0.15);
|
||||
println!("contours: {:.3}s", t.elapsed().as_secs_f32());
|
||||
let mut contour_points = HashMap::new();
|
||||
|
||||
for (point, x1, x2, _) in contours {
|
||||
@@ -28,26 +207,51 @@ fn main() -> Result<()> {
|
||||
*entry = std::cmp::max(*entry, (steepness * 4000.0).abs() as u8);
|
||||
}
|
||||
|
||||
println!("groundwater...");
|
||||
let groundwater = compute_groundwater(&water, &rain, &heightmap);
|
||||
|
||||
println!("soil...");
|
||||
let t = Instant::now();
|
||||
let soil_nutrients = soil_nutrients(&groundwater);
|
||||
println!("soil: {:.3}s", t.elapsed().as_secs_f32());
|
||||
|
||||
println!("rendering...");
|
||||
let mut image = ImageBuffer::from_pixel((WORLD_RADIUS * 2 + 1) as u32, (WORLD_RADIUS * 2 + 1) as u32, Rgb::from([0u8, 0, 0]));
|
||||
let t = Instant::now();
|
||||
let image_radius = heightmap.radius;
|
||||
let mut image = ImageBuffer::from_pixel((image_radius * 2 + 1) as u32, (image_radius * 2 + 1) as u32, Rgb::from([0u8, 0, 0]));
|
||||
let render_data = RenderData {
|
||||
heightmap: &heightmap,
|
||||
rain: &rain,
|
||||
water: &water,
|
||||
groundwater: &groundwater,
|
||||
salt: &salt,
|
||||
temperature: &temperature,
|
||||
humidity: &atmo_humidity,
|
||||
soil: &soil_nutrients,
|
||||
contour_points: &contour_points,
|
||||
sea_distance: &sea_distance,
|
||||
};
|
||||
let r1 = field_range(f1, &render_data);
|
||||
let r2 = field_range(f2, &render_data);
|
||||
let r3 = field_range(f3, &render_data);
|
||||
|
||||
for (position, height) in heightmap.iter() {
|
||||
let col = position.x + (position.y - (position.y & 1)) / 2 + WORLD_RADIUS;
|
||||
let row = position.y + WORLD_RADIUS;
|
||||
let height = *height * 0.5 + 1.0;
|
||||
let green_channel = height;
|
||||
let red_channel = soil_nutrients[position];
|
||||
let blue_channel = water[position].min(1.0);
|
||||
image.put_pixel(col as u32, row as u32, Rgb::from([(red_channel * 255.0) as u8, (green_channel * 255.0) as u8, (blue_channel * 255.0) as u8]));
|
||||
for (position, _) in heightmap.iter() {
|
||||
let col = position.x + (position.y - (position.y & 1)) / 2 + image_radius;
|
||||
let row = position.y + image_radius;
|
||||
let mut c1 = sample_field(f1, position, &render_data);
|
||||
let mut c2 = sample_field(f2, position, &render_data);
|
||||
let mut c3 = sample_field(f3, position, &render_data);
|
||||
if args.normalize {
|
||||
c1 = ((c1 - r1.0) / (r1.1 - r1.0)).clamp(0.0, 1.0);
|
||||
c2 = ((c2 - r2.0) / (r2.1 - r2.0)).clamp(0.0, 1.0);
|
||||
c3 = ((c3 - r3.0) / (r3.1 - r3.0)).clamp(0.0, 1.0);
|
||||
}
|
||||
let rgb = to_rgb(c1, c2, c3, &args.color_space);
|
||||
image.put_pixel(col as u32, row as u32, Rgb::from(rgb));
|
||||
}
|
||||
println!("render: {:.3}s", t.elapsed().as_secs_f32());
|
||||
|
||||
image.save("./out.png")?;
|
||||
let t = Instant::now();
|
||||
image.save(args.output)?;
|
||||
println!("save: {:.3}s", t.elapsed().as_secs_f32());
|
||||
println!("total: {:.3}s", total_start.elapsed().as_secs_f32());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user