src/plants.rs 4.1 K · 132 lines · raw · history

1 use bevy::prelude::*;
2
3 use crate::{
4 climate::{FROST_TEMP, growth_factor, outdoor_temperature},
5 sim::{SimSystems, SimTick},
6 tilemap::PlantLook,
7 world::{Terrain, TileChanged, WorldMap},
8 };
9
10 pub struct PlantsPlugin;
11
12 impl Plugin for PlantsPlugin {
13 fn build(&self, app: &mut App) {
14 app.add_systems(FixedUpdate, grow_plants.in_set(SimSystems::Act));
15 }
16 }
17
18 pub const GROWTH_INTERVAL_TICKS: u64 = 60;
19 const WILD_MIN_FERTILITY: f32 = 0.8;
20
21 pub fn fertility(terrain: Terrain) -> f32 {
22 match terrain {
23 Terrain::Soil => 1.0,
24 Terrain::Grass => 0.8,
25 Terrain::Sand => 0.4,
26 Terrain::Gravel => 0.3,
27 _ => 0.0,
28 }
29 }
30
31 pub fn grow(
32 map: &mut WorldMap,
33 elapsed_ticks: f32,
34 temperature: f32,
35 mut on_change: impl FnMut(UVec2),
36 ) {
37 let warmth = growth_factor(temperature);
38 for pos in map.growing_plants() {
39 let p = pos.as_ivec2();
40 let Some(plant) = map.plant(p) else {
41 continue;
42 };
43 let Some(ripening) = plant.ripening_ticks() else {
44 continue;
45 };
46 let growth = map.growth(p);
47 if growth >= 1.0 {
48 continue;
49 }
50 if plant == crate::world::Plant::Crop && temperature < FROST_TEMP {
51 map.set_plant(pos, None);
52 on_change(pos);
53 continue;
54 }
55 let soil = fertility(map.get(p).unwrap_or(Terrain::Grass));
56 let soil = match plant {
57 crate::world::Plant::Crop => soil,
58 _ => soil.max(WILD_MIN_FERTILITY),
59 };
60 let grown = (growth + elapsed_ticks / ripening * soil * warmth).min(1.0);
61 map.set_growth(pos, grown);
62 if PlantLook::of(plant, growth) != PlantLook::of(plant, grown) {
63 on_change(pos);
64 }
65 }
66 }
67
68 fn grow_plants(
69 tick: Res<SimTick>,
70 mut map: ResMut<WorldMap>,
71 mut changed: MessageWriter<TileChanged>,
72 ) {
73 if !tick.0.is_multiple_of(GROWTH_INTERVAL_TICKS) {
74 return;
75 }
76 let temperature = outdoor_temperature(*tick);
77 grow(&mut map, GROWTH_INTERVAL_TICKS as f32, temperature, |pos| {
78 changed.write(TileChanged(pos));
79 });
80 }
81
82 #[cfg(test)]
83 mod tests {
84 use super::*;
85 use crate::world::Plant;
86
87 const SUMMER: f32 = 20.0;
88
89 fn growth_after(terrain: Terrain, plant: Plant, ticks: f32) -> f32 {
90 let mut map = WorldMap::filled(UVec2::splat(2), terrain);
91 map.set_plant(UVec2::ZERO, Some(plant));
92 grow(&mut map, ticks, SUMMER, |_| {});
93 map.growth(IVec2::ZERO)
94 }
95
96 #[test]
97 fn crops_ripen_faster_on_fertile_soil() {
98 let day = 24.0 * crate::sim::TICKS_PER_HOUR as f32;
99 assert!(
100 growth_after(Terrain::Soil, Plant::Crop, day)
101 > growth_after(Terrain::Sand, Plant::Crop, day)
102 );
103 assert_eq!(growth_after(Terrain::Soil, Plant::Crop, 2.0 * day), 1.0);
104 }
105
106 #[test]
107 fn trees_do_not_grow_and_stage_changes_are_reported() {
108 assert_eq!(growth_after(Terrain::Soil, Plant::Tree, 1e9), 0.0);
109 let mut map = WorldMap::filled(UVec2::splat(2), Terrain::Soil);
110 map.set_plant(UVec2::ZERO, Some(Plant::Crop));
111 let mut changes = 0;
112 grow(&mut map, 1e9, SUMMER, |_| changes += 1);
113 assert_eq!(changes, 1);
114 }
115
116 // Unripe crops die in a hard frost, so the harvest has to be in before winter; ripe crops and
117 // wild bushes survive but stop growing.
118 #[test]
119 fn frost_kills_unripe_crops_and_cold_stops_growth() {
120 let mut map = WorldMap::filled(UVec2::new(3, 1), Terrain::Soil);
121 map.set_plant(UVec2::new(0, 0), Some(Plant::Crop));
122 map.set_plant(UVec2::new(1, 0), Some(Plant::Crop));
123 map.set_growth(UVec2::new(1, 0), 1.0);
124 map.set_plant(UVec2::new(2, 0), Some(Plant::BerryBush));
125 grow(&mut map, 1000.0, 2.0, |_| {});
126 assert_eq!(map.growth(IVec2::new(2, 0)), 0.0);
127 grow(&mut map, 1000.0, FROST_TEMP - 1.0, |_| {});
128 assert_eq!(map.plant(IVec2::new(0, 0)), None);
129 assert_eq!(map.plant(IVec2::new(1, 0)), Some(Plant::Crop));
130 assert_eq!(map.plant(IVec2::new(2, 0)), Some(Plant::BerryBush));
131 }
132 }