use bevy::prelude::*; use crate::{ building::Blueprints, climate::{FROST_TEMP, growth_factor, outdoor_temperature}, rng::SimRng, sim::{SimSystems, SimTick, TICKS_PER_HOUR}, tilemap::PlantLook, world::{Plant, Terrain, TileChanged, WorldMap}, zones::Zones, }; pub struct PlantsPlugin; impl Plugin for PlantsPlugin { fn build(&self, app: &mut App) { app.add_systems( FixedUpdate, (grow_plants, spread_trees).in_set(SimSystems::Act), ); } } pub const GROWTH_INTERVAL_TICKS: u64 = 60; const WILD_MIN_FERTILITY: f32 = 0.8; pub const SPREAD_SAMPLES: usize = 2000; pub const SEED_CHANCE: f32 = 0.03; pub const FOREST_DENSITY: usize = 9; pub const CLEARING_RADIUS: i32 = 12; 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); if plant == Plant::Sapling && grown >= 1.0 { map.set_plant(pos, Some(Plant::Tree)); map.set_growth(pos, 0.0); on_change(pos); continue; } 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)); }); } pub fn seed_chance(map: &WorldMap, zones: &Zones, blueprints: &Blueprints, pos: UVec2) -> f32 { let p = pos.as_ivec2(); let open = map.is_forest(pos) && matches!(map.get(p), Some(Terrain::Grass | Terrain::Soil)) && map.plant(p).is_none() && !map.is_wall(p) && !map.is_roofed(p) && map.wear(p) == 0.0 && !zones.is_stockpile(pos) && !zones.is_farm(pos) && blueprints.get(pos).is_none() && (p - zones.home(map.size / 2).as_ivec2()) .abs() .max_element() > CLEARING_RADIUS; if !open { return 0.0; } let tree = |d: IVec2| map.plant(p + d).is_some_and(|t| t.is_tree()); let mut near = 0; let mut around = 0; for y in -2..=2 { for x in -2..=2 { let d = IVec2::new(x, y); if tree(d) { around += 1; near += (d.abs().max_element() == 1) as usize; } } } if around >= FOREST_DENSITY { return 0.0; } SEED_CHANCE * near as f32 } fn spread_trees( tick: Res, zones: Res, blueprints: Res, mut map: ResMut, mut rng: ResMut, mut changed: MessageWriter, ) { if !tick.0.is_multiple_of(TICKS_PER_HOUR) { return; } let size = map.size.as_ivec2(); for _ in 0..SPREAD_SAMPLES { let pos = IVec2::new(rng.range_i32(0..size.x), rng.range_i32(0..size.y)).as_uvec2(); let chance = seed_chance(&map, &zones, &blueprints, pos); if chance > 0.0 && rng.range_f32(0.0..1.0) < chance { map.set_plant(pos, Some(Plant::Sapling)); map.set_growth(pos, 0.0); changed.write(TileChanged(pos)); } } } #[cfg(test)] mod tests { use super::*; 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)); } // Cut forest grows back from neighbouring trees: saplings appear on free forest tiles next // to trees and mature into trees after days. Paths, zones, buildings and the colony core stay // clear, open grassland never turns into forest, and forests don't grow denser than worldgen // made them. #[test] fn forests_reseed_cleared_tiles_and_saplings_mature() { let mut map = WorldMap::filled(UVec2::splat(40), Terrain::Grass); let mut zones = Zones::default(); zones.add_zone( crate::zones::ZoneKind::Stockpile, &map, IVec2::new(1, 1), IVec2::new(2, 2), ); for x in 25..35 { for y in 25..35 { map.set_forest(UVec2::new(x, y), true); } } map.set_plant(UVec2::new(30, 30), Some(Plant::Tree)); let none = Blueprints::default(); assert!(seed_chance(&map, &zones, &none, UVec2::new(31, 30)) > 0.0); assert_eq!(seed_chance(&map, &zones, &none, UVec2::new(33, 30)), 0.0); map.set_plant(UVec2::new(24, 30), Some(Plant::Tree)); assert_eq!(seed_chance(&map, &zones, &none, UVec2::new(23, 30)), 0.0); map.set_wear(UVec2::new(29, 30), 1.0); assert_eq!(seed_chance(&map, &zones, &none, UVec2::new(29, 30)), 0.0); for x in 26..33 { for y in 28..33 { if (x + y) % 2 == 0 { map.set_plant(UVec2::new(x, y), Some(Plant::Tree)); } } } assert_eq!(seed_chance(&map, &zones, &none, UVec2::new(29, 29)), 0.0); map.set_plant(UVec2::new(31, 34), Some(Plant::Sapling)); let day = 24.0 * crate::sim::TICKS_PER_HOUR as f32; grow(&mut map, 7.0 * day, SUMMER, |_| {}); assert_eq!(map.plant(IVec2::new(31, 34)), Some(Plant::Sapling)); grow(&mut map, 3.0 * day, SUMMER, |_| {}); assert_eq!(map.plant(IVec2::new(31, 34)), Some(Plant::Tree)); } }