diff --git a/Cargo.lock b/Cargo.lock index 56c7185..918d64b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 48f747e..34ad549 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/components.rs b/src/components.rs index 035d4b7..c66dcae 100644 --- a/src/components.rs +++ b/src/components.rs @@ -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, + pub fungible: IndexMap, + pub non_fungible: Vec, 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 } } diff --git a/src/main.rs b/src/main.rs index 892a280..f1ae6e9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 { 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?; diff --git a/src/plant.rs b/src/plant.rs index ed3eb53..e96fd6e 100644 --- a/src/plant.rs +++ b/src/plant.rs @@ -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), }; diff --git a/src/render.rs b/src/render.rs index e0ae6d3..ceea273 100644 --- a/src/render.rs +++ b/src/render.rs @@ -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, 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 { } } -fn mat_vec_mul(mat: &[f32], n: usize, v: &[f32]) -> Vec { - 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::().sqrt() -} - -fn normalize_vec(v: &mut [f32]) { - let n = vec_norm(v); +fn normalize_vec(v: &mut Array1) { + 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> { - let mut a = cov.to_vec(); +fn top_pca_vectors(mut a: Array2, k: usize) -> Vec> { + 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::>(); + 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::::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::::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]; diff --git a/src/save.rs b/src/save.rs index ba5c5c4..41709a5 100644 --- a/src/save.rs +++ b/src/save.rs @@ -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 } } }