use bevy::prelude::*; use crate::{ climate::{FROST_TEMP, growth_factor, outdoor_temperature}, sim::{SimSystems, SimTick}, tilemap::PlantLook, world::{Terrain, TileChanged, WorldMap}, }; pub struct PlantsPlugin; impl Plugin for PlantsPlugin { fn build(&self, app: &mut App) { app.add_systems(FixedUpdate, grow_plants.in_set(SimSystems::Act)); } } pub const GROWTH_INTERVAL_TICKS: u64 = 60; const WILD_MIN_FERTILITY: f32 = 0.8; pub fn fertility(terrain: Terrain) -> f32 { match terrain { Terrain::Soil => 1.0, Terrain::Grass => 0.8, Terrain::Sand => 0.4, Terrain::Gravel => 0.3, _ => 0.0, } } pub fn grow( map: &mut WorldMap, elapsed_ticks: f32, temperature: f32, mut on_change: impl FnMut(UVec2), ) { let warmth = growth_factor(temperature); for pos in map.growing_plants() { let p = pos.as_ivec2(); let Some(plant) = map.plant(p) else { continue; }; let Some(ripening) = plant.ripening_ticks() else { continue; }; let growth = map.growth(p); if growth >= 1.0 { continue; } if plant == crate::world::Plant::Crop && temperature < FROST_TEMP { map.set_plant(pos, None); on_change(pos); continue; } let soil = fertility(map.get(p).unwrap_or(Terrain::Grass)); let soil = match plant { crate::world::Plant::Crop => soil, _ => soil.max(WILD_MIN_FERTILITY), }; let grown = (growth + elapsed_ticks / ripening * soil * warmth).min(1.0); map.set_growth(pos, grown); if PlantLook::of(plant, growth) != PlantLook::of(plant, grown) { on_change(pos); } } } fn grow_plants( tick: Res, mut map: ResMut, mut changed: MessageWriter, ) { if !tick.0.is_multiple_of(GROWTH_INTERVAL_TICKS) { return; } let temperature = outdoor_temperature(*tick); grow(&mut map, GROWTH_INTERVAL_TICKS as f32, temperature, |pos| { changed.write(TileChanged(pos)); }); } #[cfg(test)] mod tests { use super::*; use crate::world::Plant; const SUMMER: f32 = 20.0; fn growth_after(terrain: Terrain, plant: Plant, ticks: f32) -> f32 { let mut map = WorldMap::filled(UVec2::splat(2), terrain); map.set_plant(UVec2::ZERO, Some(plant)); grow(&mut map, ticks, SUMMER, |_| {}); map.growth(IVec2::ZERO) } #[test] fn crops_ripen_faster_on_fertile_soil() { let day = 24.0 * crate::sim::TICKS_PER_HOUR as f32; assert!( growth_after(Terrain::Soil, Plant::Crop, day) > growth_after(Terrain::Sand, Plant::Crop, day) ); assert_eq!(growth_after(Terrain::Soil, Plant::Crop, 2.0 * day), 1.0); } #[test] fn trees_do_not_grow_and_stage_changes_are_reported() { assert_eq!(growth_after(Terrain::Soil, Plant::Tree, 1e9), 0.0); let mut map = WorldMap::filled(UVec2::splat(2), Terrain::Soil); map.set_plant(UVec2::ZERO, Some(Plant::Crop)); let mut changes = 0; grow(&mut map, 1e9, SUMMER, |_| changes += 1); assert_eq!(changes, 1); } // Unripe crops die in a hard frost, so the harvest has to be in before winter; ripe crops and // wild bushes survive but stop growing. #[test] fn frost_kills_unripe_crops_and_cold_stops_growth() { let mut map = WorldMap::filled(UVec2::new(3, 1), Terrain::Soil); map.set_plant(UVec2::new(0, 0), Some(Plant::Crop)); map.set_plant(UVec2::new(1, 0), Some(Plant::Crop)); map.set_growth(UVec2::new(1, 0), 1.0); map.set_plant(UVec2::new(2, 0), Some(Plant::BerryBush)); grow(&mut map, 1000.0, 2.0, |_| {}); assert_eq!(map.growth(IVec2::new(2, 0)), 0.0); grow(&mut map, 1000.0, FROST_TEMP - 1.0, |_| {}); assert_eq!(map.plant(IVec2::new(0, 0)), None); assert_eq!(map.plant(IVec2::new(1, 0)), Some(Plant::Crop)); assert_eq!(map.plant(IVec2::new(2, 0)), Some(Plant::BerryBush)); } }