1
0
mirror of https://github.com/osmarks/ewo3.git synced 2026-03-23 10:19:42 +00:00

further retuning, slightly reduce questionable decisions

This commit is contained in:
osmarks
2026-03-06 13:39:19 +00:00
parent 599107f2ed
commit dd91ebd8de
7 changed files with 147 additions and 145 deletions

23
Cargo.lock generated
View File

@@ -527,6 +527,7 @@ dependencies = [
"ndarray",
"ndarray-conv",
"noise-functions",
"oklab",
"raw-window-handle 0.5.2",
"rayon",
"seahash",
@@ -541,6 +542,12 @@ dependencies = [
"winit",
]
[[package]]
name = "fast-srgb8"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1"
[[package]]
name = "fastrand"
version = "2.1.0"
@@ -1272,6 +1279,16 @@ version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d079845b37af429bfe5dfa76e6d087d788031045b25cfc6fd898486fd9847666"
[[package]]
name = "oklab"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1e35ab3c8efa6bc97d651abe7fb051aebb30c925a450ff6722cf9c797a938cc"
dependencies = [
"fast-srgb8",
"rgb",
]
[[package]]
name = "once_cell"
version = "1.21.3"
@@ -1553,6 +1570,12 @@ dependencies = [
"bitflags 2.11.0",
]
[[package]]
name = "rgb"
version = "0.8.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
[[package]]
name = "rustfft"
version = "6.4.1"

View File

@@ -37,6 +37,7 @@ glutin = "0.31"
glutin-winit = "0.4"
raw-window-handle = "0.5"
winit = "0.29"
oklab = "1.1.2"
[[bin]]
name = "render"

View File

@@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::f32;
use indexmap::IndexMap;
@@ -9,12 +10,18 @@ use enum_map::{Enum, EnumMap};
use crate::map::*;
use crate::plant;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Item {
// TODO: complicate this by making things nonfungible more
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
pub enum FungibleItem {
Dirt,
Bone,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum NonFungibleItem {
Seed(plant::Genome)
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Enum, Copy)]
pub enum HealthChangeType {
BluntForce,
@@ -23,18 +30,35 @@ pub enum HealthChangeType {
Starvation
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Item {
Fungible(FungibleItem),
NonFungible(NonFungibleItem)
}
impl Item {
pub fn name(&self) -> &'static str {
pub fn name(&self) -> Cow<'static, str> {
use Item::*;
use NonFungibleItem::*;
use FungibleItem::*;
match self {
Item::Dirt => "Dirt",
Item::Bone => "Bone",
Fungible(Dirt) => Cow::Borrowed("Dirt"),
Fungible(Bone) => Cow::Borrowed("Bone"),
NonFungible(Seed(genome)) => Cow::Owned(format!("Seed ({:?}", genome.crop_type))
}
}
pub fn description(&self) -> &'static str {
use Item::*;
use NonFungibleItem::*;
use FungibleItem::*;
match self {
Item::Dirt => "It's from the ground. You're carrying it for some reason.",
Item::Bone => "Disassembling your enemies for resources is probably ethical.",
Fungible(Dirt) => "It's from the ground. You're carrying it for some reason.",
Fungible(Bone) => "Disassembling your enemies for resources is probably ethical.",
NonFungible(Seed(_genome)) => "Grows into a plant, given appropriate conditions."
}
}
}
@@ -292,7 +316,8 @@ pub struct DespawnOnImpact;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Inventory {
pub contents: IndexMap<Item, u64>,
pub fungible: IndexMap<FungibleItem, u64>,
pub non_fungible: Vec<NonFungibleItem>,
pub added_items: u64,
pub additions: u64,
pub taken_items: u64,
@@ -301,17 +326,26 @@ pub struct Inventory {
impl Inventory {
pub fn add(&mut self, item: Item, qty: u64) {
*self.contents.entry(item).or_default() += qty;
match item {
Item::Fungible(x) => *self.fungible.entry(x).or_default() += qty,
Item::NonFungible(x) => for _ in 0..qty {
self.non_fungible.push(x.clone())
}
}
self.added_items += qty;
self.additions += 1;
}
pub fn extend(&mut self, other: &Inventory) {
for (item, count) in other.contents.iter() {
self.add(item.clone(), *count);
for (item, count) in other.fungible.iter() {
self.add(Item::Fungible(item.clone()), *count);
}
for item in other.non_fungible.iter() {
self.non_fungible.push(item.clone());
}
}
/*
pub fn take(&mut self, item: Item, qty: u64) -> bool {
match self.contents.entry(item) {
indexmap::map::Entry::Occupied(mut o) => {
@@ -327,14 +361,16 @@ impl Inventory {
indexmap::map::Entry::Vacant(_) => false,
}
}
*/
pub fn is_empty(&self) -> bool {
!self.contents.iter().any(|(_, c)| *c > 0)
!self.fungible.iter().any(|(_, c)| *c > 0) && self.non_fungible.is_empty()
}
pub fn empty() -> Self {
Self {
contents: IndexMap::new(),
fungible: IndexMap::new(),
non_fungible: Vec::new(),
added_items: 0,
additions: 0,
taken_items: 0,
@@ -353,6 +389,7 @@ pub struct Plant {
pub growth_ticks: u64,
pub children: u64,
pub ready_for_reproduce_ticks: u64,
pub total_growth: f32 // currently a bit redundant, given nutrient consumption counter
}
impl Plant {
@@ -366,6 +403,7 @@ impl Plant {
growth_ticks: 0,
children: 0,
ready_for_reproduce_ticks: 0,
total_growth: 0.0
}
}

View File

@@ -9,7 +9,7 @@ use tokio_tungstenite::tungstenite::protocol::Message;
use tokio::sync::{mpsc, Mutex};
use anyhow::{Result, Context, anyhow};
use argh::FromArgs;
use std::{collections::{HashMap, HashSet}, hash::{Hash, Hasher}, net::SocketAddr, ops::DerefMut, sync::Arc, time::Duration};
use std::{collections::HashSet, hash::{Hash, Hasher}, net::SocketAddr, ops::DerefMut, sync::Arc, time::Duration};
use slab::Slab;
use serde::{Serialize, Deserialize};
use smallvec::smallvec;
@@ -292,7 +292,7 @@ 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.07;
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;
@@ -302,6 +302,8 @@ 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();
@@ -316,7 +318,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.9999);
state.dynamic_soil_nutrients.for_each_mut(|nutrients| *nutrients *= 0.9995);
} 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 {
@@ -348,7 +350,7 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
}
if !occupied && state.map.get_terrain(newpos).entry_cost().is_some() && hex_distance(newpos, pos) >= *spawn_range.start() {
let mut spec = EnemySpec::random(&mut rng);
spec.drops.push((Item::Bone, StochasticNumber::Triangle { min: 0.7 * spec.initial_health / 40.0, max: 1.3 * spec.initial_health / 40.0, mode: spec.initial_health / 40.0 }));
spec.drops.push((Item::Fungible(FungibleItem::Bone), StochasticNumber::Triangle { min: 0.7 * spec.initial_health / 40.0, max: 1.3 * spec.initial_health / 40.0, mode: spec.initial_health / 40.0 }));
if spec.ranged {
buffer.cmd.spawn((
Render(spec.symbol),
@@ -404,11 +406,10 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
if let Ok(mut health) = state.world.get::<&mut Health>(entity) {
health.apply(HealthChangeType::Starvation, -PLANT_DIEOFF_RATE);
if health.current <= 0.0 {
// TODO: this is inelegant and should be shared with the other death code
// also, it might break the position tracker
buffer.kill(&state.world, entity, &mut rng, None);
state.metrics.plants_died += 1;
// TODO: death should provide decomposition nutrients to soil
state.metrics.plants_died_starvation += 1;
// return nutrients to soil upon death; TODO implement decomposition
state.dynamic_soil_nutrients[pos] += plant.current_size * SOIL_NUTRIENT_DECOMP_RETURN_RATE;
}
}
}
@@ -429,16 +430,16 @@ 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 {
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;
}
// TODO: water consumption should depend on atmospheric humidity
let water_consumed = (difference + PLANT_IDLE_WATER_CONSUMPTION_OFFSET) * WATER_CONSUMPTION_RATE * plant.genome.water_efficiency();
state.dynamic_groundwater[pos] -= water_consumed;
plant.water_consumed += water_consumed;
//println!("water={} nutrient={} diff={} consume={}", water, soil_nutrients, difference, water_consumed);
if difference > 0.0 {
plant.growth_ticks += 1;
if let Ok(mut health) = state.world.get::<&mut Health>(entity) {
@@ -446,7 +447,11 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
}
}
// TODO: plants should die of old age
// 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;
}
}
}
@@ -480,9 +485,8 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
let plant = plant?;
let other_plant = other_plant?;
for _ in 0..count_hexes(PLANT_SEEDING_RADIUS) {
for _ in 0..PLANT_SEEDING_ATTEMPTS {
let newpos = pos + sample_range_rng(PLANT_SEEDING_RADIUS, &mut rng);
// TODO: maybe just discard seed here
if !state.map.heightmap.in_range(newpos) || state.positions.entities[newpos].is_some() || !hybrid_genome.terrain_valid(&state.map.get_terrain(newpos)) {
continue;
}
@@ -625,7 +629,7 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
Position::single_tile(position, MapLayer::Terrain),
NewlyAdded
));
inventory.add(Item::Dirt, StochasticNumber::triangle_from_min_range(1.0, 3.0).sample_rounded(&mut rng));
inventory.add(Item::Fungible(FungibleItem::Dirt), StochasticNumber::triangle_from_min_range(1.0, 3.0).sample_rounded(&mut rng));
}
}
},
@@ -775,8 +779,9 @@ async fn game_tick(state: &mut GameState) -> Result<()> {
}
}
let health = state.world.get::<&Health>(client.entity)?.current;
let inventory = state.world.get::<&Inventory>(client.entity)?.contents
.iter().map(|(i, q)| (i.name().to_string(), i.description().to_string(), *q)).filter(|(_, _, q)| *q > 0).collect();
// TODO do this properly
let inventory = state.world.get::<&Inventory>(client.entity)?.fungible
.iter().map(|(i, q)| (Item::Fungible(i.clone()).name().to_string(), Item::Fungible(i.clone()).description().to_string(), *q)).filter(|(_, _, q)| *q > 0).collect();
client.frames_tx.send(Frame::Display { nearby, health, inventory }).await?;
} else {
client.frames_tx.send(Frame::Dead).await?;

View File

@@ -11,7 +11,7 @@ pub enum CropType {
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct Genome {
crop_type: CropType,
pub crop_type: CropType,
// polygenic traits; parameterized as N(0,1) (allegedly)
// groundwater is [0,1] so this is sort of questionable
// TODO: reparameterize or something
@@ -60,7 +60,10 @@ impl Genome {
if !self.terrain_valid(terrain) {
return 0.0;
}
let water_diff = (water - sigmoid(self.optimal_water_level)).powf(2.0);
let mut water_diff = (water - sigmoid(self.optimal_water_level)).powf(2.0);
if water_diff >= 0.0 {
water_diff *= 0.5; // ugly hack for asymmetry
}
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
@@ -68,6 +71,7 @@ impl Genome {
- self.water_tolerance * 0.08
- self.temperature_tolerance * 0.07
- self.salt_tolerance.max(0.0) * 0.05;
let base = base.max(0.0);
let water_tolerance_coefficient = 13.0 * (1.0 + (-self.water_tolerance).exp());
let temperature_tolerance_coefficient = 3.0 * (1.0 + (-self.temperature_tolerance).exp());
@@ -109,7 +113,7 @@ impl Genome {
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.4, 3.0),
CropType::GoldenWattleTree => ( 2.0, 0.5, 0.5, 0.5, 0.7, 3.0),
};

View File

@@ -16,6 +16,7 @@ use glutin::surface::{GlSurface, Surface, SurfaceAttributesBuilder, WindowSurfac
use imgui::Condition;
use imgui_glow_renderer::{AutoRenderer as ImguiRenderer, TextureMap};
use imgui_winit_support::winit::window::WindowBuilder;
use ndarray::{Array1, Array2, Axis};
use hecs::World;
use image::{ImageBuffer, Rgb};
use imgui_winit_support::{HiDpiMode, WinitPlatform};
@@ -222,38 +223,11 @@ fn field_range(field: Option<Field>, data: &RenderData) -> (f32, f32) {
}
fn to_rgb(c1: f32, c2: f32, c3: f32, color_space: ColorSpace) -> [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 {
ColorSpace::Rgb => (c1, c2, c3),
ColorSpace::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),
)
let rgb = oklab::oklab_to_srgb_f32(oklab::Oklab { l: c1.clamp(0.0, 1.0), a: c2 * 1.0 - 0.5, b: c3 * 1.0 - 0.5 });
(rgb.r, rgb.g, rgb.b)
}
};
[(r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8]
@@ -401,63 +375,39 @@ fn load_render_data(args: &Args) -> Result<RenderData> {
}
}
fn mat_vec_mul(mat: &[f32], n: usize, v: &[f32]) -> Vec<f32> {
let mut out = vec![0.0; n];
for i in 0..n {
let mut acc = 0.0;
for j in 0..n {
acc += mat[i * n + j] * v[j];
}
out[i] = acc;
}
out
}
fn vec_norm(v: &[f32]) -> f32 {
v.iter().map(|x| x * x).sum::<f32>().sqrt()
}
fn normalize_vec(v: &mut [f32]) {
let n = vec_norm(v);
fn normalize_vec(v: &mut Array1<f32>) {
let n = v.dot(v).sqrt();
if n > 1e-12 {
for x in v.iter_mut() {
*x /= n;
}
*v /= n;
}
}
fn dot(a: &[f32], b: &[f32]) -> f32 {
a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
}
fn top_pca_vectors(cov: &[f32], dim: usize, k: usize) -> Vec<Vec<f32>> {
let mut a = cov.to_vec();
fn top_pca_vectors(mut a: Array2<f32>, k: usize) -> Vec<Array1<f32>> {
let dim = a.nrows();
let mut out = Vec::new();
let components = k.min(dim);
for comp in 0..components {
let mut v = (0..dim)
.map(|i| 1.0 + (i + comp) as f32 * 0.013)
.collect::<Vec<_>>();
let mut v = Array1::from_iter((0..dim).map(|i| 1.0 + (i + comp) as f32 * 0.013));
normalize_vec(&mut v);
for _ in 0..64 {
let mut next = mat_vec_mul(&a, dim, &v);
let mut next = a.dot(&v);
normalize_vec(&mut next);
v = next;
}
let av = mat_vec_mul(&a, dim, &v);
let lambda = dot(&v, &av);
let av = a.dot(&v);
let lambda = v.dot(&av);
if !lambda.is_finite() || lambda.abs() < 1e-8 {
break;
}
for i in 0..dim {
for j in 0..dim {
a[i * dim + j] -= lambda * v[i] * v[j];
}
}
let outer = v
.view()
.insert_axis(Axis(1))
.dot(&v.view().insert_axis(Axis(0)));
a = &a - &(outer * lambda);
out.push(v);
}
@@ -474,51 +424,31 @@ fn render_pca(data: &RenderData, fields: &[Field], color_space: ColorSpace) -> R
let n = coords.len();
let m = fields.len();
let mut samples = vec![0.0f32; n * m];
let mut samples = Array2::<f32>::zeros((n, m));
for (i, coord) in coords.iter().copied().enumerate() {
for (j, field) in fields.iter().copied().enumerate() {
samples[i * m + j] = sample_field(field, coord, data);
}
}
let mut means = vec![0.0f32; m];
for j in 0..m {
let mut acc = 0.0;
for i in 0..n {
acc += samples[i * m + j];
}
means[j] = acc / n as f32;
}
let mut stds = vec![0.0f32; m];
for j in 0..m {
let mut acc = 0.0;
for i in 0..n {
let d = samples[i * m + j] - means[j];
acc += d * d;
}
stds[j] = (acc / (n as f32 - 1.0).max(1.0)).sqrt().max(1e-6);
}
for i in 0..n {
for j in 0..m {
samples[i * m + j] = (samples[i * m + j] - means[j]) / stds[j];
samples[[i, j]] = sample_field(field, coord, data);
}
}
let mut cov = vec![0.0f32; m * m];
let denom = (n as f32 - 1.0).max(1.0);
for a in 0..m {
for b in 0..m {
let mut acc = 0.0;
for i in 0..n {
acc += samples[i * m + a] * samples[i * m + b];
}
cov[a * m + b] = acc / denom;
let means = samples
.mean_axis(Axis(0))
.ok_or_else(|| anyhow!("no samples for PCA"))?;
let centered = &samples - &means;
let variances = centered.mapv(|x| x * x).sum_axis(Axis(0)) / denom;
let stds = variances.mapv(|x| x.sqrt().max(1e-6));
let standardized = centered / &stds;
let cov_nd = standardized.t().dot(&standardized) / denom;
for i in 0..m {
for j in 0..m {
cov[i * m + j] = cov_nd[[i, j]];
}
}
let pcs = top_pca_vectors(&cov, m, 3);
let pcs = top_pca_vectors(cov_nd, 3);
if pcs.is_empty() {
anyhow::bail!("PCA failed to produce principal components")
}
@@ -532,11 +462,11 @@ fn render_pca(data: &RenderData, fields: &[Field], color_space: ColorSpace) -> R
coeffs.push(channel_coeffs);
}
let mut chans = vec![vec![0.0f32; n]; 3];
for i in 0..n {
let row = &samples[i * m..(i + 1) * m];
for k in 0..3 {
chans[k][i] = if k < pcs.len() { dot(row, &pcs[k]) } else { 0.0 };
let mut chans = Array2::<f32>::zeros((n, 3));
for k in 0..3 {
if k < pcs.len() {
let projected = standardized.dot(&pcs[k]);
chans.column_mut(k).assign(&projected);
}
}
@@ -544,7 +474,7 @@ fn render_pca(data: &RenderData, fields: &[Field], color_space: ColorSpace) -> R
for k in 0..3 {
let mut min = f32::INFINITY;
let mut max = f32::NEG_INFINITY;
for v in chans[k].iter().copied() {
for v in chans.column(k).iter().copied() {
min = min.min(v);
max = max.max(v);
}
@@ -559,9 +489,9 @@ fn render_pca(data: &RenderData, fields: &[Field], color_space: ColorSpace) -> R
let mut pixels = vec![0u8; (side * side * 3) as usize];
for (i, coord) in coords.iter().copied().enumerate() {
let (x, y) = hex_to_image_coords(coord, data.world.radius);
let c1 = normalize(chans[0][i], ranges[0].0, ranges[0].1);
let c2 = normalize(chans[1][i], ranges[1].0, ranges[1].1);
let c3 = normalize(chans[2][i], ranges[2].0, ranges[2].1);
let c1 = normalize(chans[[i, 0]], ranges[0].0, ranges[0].1);
let c2 = normalize(chans[[i, 1]], ranges[1].0, ranges[1].1);
let c3 = normalize(chans[[i, 2]], ranges[2].0, ranges[2].1);
let rgb = to_rgb(c1, c2, c3, color_space);
let idx = ((y * side + x) * 3) as usize;
pixels[idx] = rgb[0];

View File

@@ -6,14 +6,15 @@ use crate::worldgen::GeneratedWorld;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct GameMetrics {
pub plants_died: u64,
pub plants_died_old_age: u64,
pub plants_died_starvation: u64,
pub plants_reproduced: u64,
pub enemies_spawned: u64
}
impl GameMetrics {
pub fn new() -> Self {
GameMetrics { plants_died: 0, plants_reproduced: 0, enemies_spawned: 0 }
GameMetrics { plants_died_old_age: 0, plants_died_starvation: 0, plants_reproduced: 0, enemies_spawned: 0 }
}
}