src/plants.rs 8.2 K · 250 lines · raw · history

1 use bevy::prelude::*;
2
3 use crate::{
4 building::Blueprints,
5 climate::{FROST_TEMP, growth_factor, outdoor_temperature},
6 rng::SimRng,
7 sim::{SimSystems, SimTick, TICKS_PER_HOUR},
8 tilemap::PlantLook,
9 world::{Plant, Terrain, TileChanged, WorldMap},
10 zones::Zones,
11 };
12
13 pub struct PlantsPlugin;
14
15 impl Plugin for PlantsPlugin {
16 fn build(&self, app: &mut App) {
17 app.add_systems(
18 FixedUpdate,
19 (grow_plants, spread_trees).in_set(SimSystems::Act),
20 );
21 }
22 }
23
24 pub const GROWTH_INTERVAL_TICKS: u64 = 60;
25 const WILD_MIN_FERTILITY: f32 = 0.8;
26 pub const SPREAD_SAMPLES: usize = 2000;
27 pub const SEED_CHANCE: f32 = 0.03;
28 pub const FOREST_DENSITY: usize = 9;
29 pub const CLEARING_RADIUS: i32 = 12;
30
31 pub fn fertility(terrain: Terrain) -> f32 {
32 match terrain {
33 Terrain::Soil => 1.0,
34 Terrain::Grass => 0.8,
35 Terrain::Sand => 0.4,
36 Terrain::Gravel => 0.3,
37 _ => 0.0,
38 }
39 }
40
41 pub fn grow(
42 map: &mut WorldMap,
43 elapsed_ticks: f32,
44 temperature: f32,
45 mut on_change: impl FnMut(UVec2),
46 ) {
47 let warmth = growth_factor(temperature);
48 for pos in map.growing_plants() {
49 let p = pos.as_ivec2();
50 let Some(plant) = map.plant(p) else {
51 continue;
52 };
53 let Some(ripening) = plant.ripening_ticks() else {
54 continue;
55 };
56 let growth = map.growth(p);
57 if growth >= 1.0 {
58 continue;
59 }
60 if plant == crate::world::Plant::Crop && temperature < FROST_TEMP {
61 map.set_plant(pos, None);
62 on_change(pos);
63 continue;
64 }
65 let soil = fertility(map.get(p).unwrap_or(Terrain::Grass));
66 let soil = match plant {
67 crate::world::Plant::Crop => soil,
68 _ => soil.max(WILD_MIN_FERTILITY),
69 };
70 let grown = (growth + elapsed_ticks / ripening * soil * warmth).min(1.0);
71 if plant == Plant::Sapling && grown >= 1.0 {
72 map.set_plant(pos, Some(Plant::Tree));
73 map.set_growth(pos, 0.0);
74 on_change(pos);
75 continue;
76 }
77 map.set_growth(pos, grown);
78 if PlantLook::of(plant, growth) != PlantLook::of(plant, grown) {
79 on_change(pos);
80 }
81 }
82 }
83
84 fn grow_plants(
85 tick: Res<SimTick>,
86 mut map: ResMut<WorldMap>,
87 mut changed: MessageWriter<TileChanged>,
88 ) {
89 if !tick.0.is_multiple_of(GROWTH_INTERVAL_TICKS) {
90 return;
91 }
92 let temperature = outdoor_temperature(*tick);
93 grow(&mut map, GROWTH_INTERVAL_TICKS as f32, temperature, |pos| {
94 changed.write(TileChanged(pos));
95 });
96 }
97
98 pub fn seed_chance(map: &WorldMap, zones: &Zones, blueprints: &Blueprints, pos: UVec2) -> f32 {
99 let p = pos.as_ivec2();
100 let open = map.is_forest(pos)
101 && matches!(map.get(p), Some(Terrain::Grass | Terrain::Soil))
102 && map.plant(p).is_none()
103 && !map.is_wall(p)
104 && !map.is_roofed(p)
105 && map.wear(p) == 0.0
106 && !zones.is_stockpile(pos)
107 && !zones.is_farm(pos)
108 && blueprints.get(pos).is_none()
109 && (p - zones.home(map.size / 2).as_ivec2())
110 .abs()
111 .max_element()
112 > CLEARING_RADIUS;
113 if !open {
114 return 0.0;
115 }
116 let tree = |d: IVec2| map.plant(p + d).is_some_and(|t| t.is_tree());
117 let mut near = 0;
118 let mut around = 0;
119 for y in -2..=2 {
120 for x in -2..=2 {
121 let d = IVec2::new(x, y);
122 if tree(d) {
123 around += 1;
124 near += (d.abs().max_element() == 1) as usize;
125 }
126 }
127 }
128 if around >= FOREST_DENSITY {
129 return 0.0;
130 }
131 SEED_CHANCE * near as f32
132 }
133
134 fn spread_trees(
135 tick: Res<SimTick>,
136 zones: Res<Zones>,
137 blueprints: Res<Blueprints>,
138 mut map: ResMut<WorldMap>,
139 mut rng: ResMut<SimRng>,
140 mut changed: MessageWriter<TileChanged>,
141 ) {
142 if !tick.0.is_multiple_of(TICKS_PER_HOUR) {
143 return;
144 }
145 let size = map.size.as_ivec2();
146 for _ in 0..SPREAD_SAMPLES {
147 let pos = IVec2::new(rng.range_i32(0..size.x), rng.range_i32(0..size.y)).as_uvec2();
148 let chance = seed_chance(&map, &zones, &blueprints, pos);
149 if chance > 0.0 && rng.range_f32(0.0..1.0) < chance {
150 map.set_plant(pos, Some(Plant::Sapling));
151 map.set_growth(pos, 0.0);
152 changed.write(TileChanged(pos));
153 }
154 }
155 }
156
157 #[cfg(test)]
158 mod tests {
159 use super::*;
160
161 const SUMMER: f32 = 20.0;
162
163 fn growth_after(terrain: Terrain, plant: Plant, ticks: f32) -> f32 {
164 let mut map = WorldMap::filled(UVec2::splat(2), terrain);
165 map.set_plant(UVec2::ZERO, Some(plant));
166 grow(&mut map, ticks, SUMMER, |_| {});
167 map.growth(IVec2::ZERO)
168 }
169
170 #[test]
171 fn crops_ripen_faster_on_fertile_soil() {
172 let day = 24.0 * crate::sim::TICKS_PER_HOUR as f32;
173 assert!(
174 growth_after(Terrain::Soil, Plant::Crop, day)
175 > growth_after(Terrain::Sand, Plant::Crop, day)
176 );
177 assert_eq!(growth_after(Terrain::Soil, Plant::Crop, 2.0 * day), 1.0);
178 }
179
180 #[test]
181 fn trees_do_not_grow_and_stage_changes_are_reported() {
182 assert_eq!(growth_after(Terrain::Soil, Plant::Tree, 1e9), 0.0);
183 let mut map = WorldMap::filled(UVec2::splat(2), Terrain::Soil);
184 map.set_plant(UVec2::ZERO, Some(Plant::Crop));
185 let mut changes = 0;
186 grow(&mut map, 1e9, SUMMER, |_| changes += 1);
187 assert_eq!(changes, 1);
188 }
189
190 // Unripe crops die in a hard frost, so the harvest has to be in before winter; ripe crops and
191 // wild bushes survive but stop growing.
192 #[test]
193 fn frost_kills_unripe_crops_and_cold_stops_growth() {
194 let mut map = WorldMap::filled(UVec2::new(3, 1), Terrain::Soil);
195 map.set_plant(UVec2::new(0, 0), Some(Plant::Crop));
196 map.set_plant(UVec2::new(1, 0), Some(Plant::Crop));
197 map.set_growth(UVec2::new(1, 0), 1.0);
198 map.set_plant(UVec2::new(2, 0), Some(Plant::BerryBush));
199 grow(&mut map, 1000.0, 2.0, |_| {});
200 assert_eq!(map.growth(IVec2::new(2, 0)), 0.0);
201 grow(&mut map, 1000.0, FROST_TEMP - 1.0, |_| {});
202 assert_eq!(map.plant(IVec2::new(0, 0)), None);
203 assert_eq!(map.plant(IVec2::new(1, 0)), Some(Plant::Crop));
204 assert_eq!(map.plant(IVec2::new(2, 0)), Some(Plant::BerryBush));
205 }
206
207 // Cut forest grows back from neighbouring trees: saplings appear on free forest tiles next
208 // to trees and mature into trees after days. Paths, zones, buildings and the colony core stay
209 // clear, open grassland never turns into forest, and forests don't grow denser than worldgen
210 // made them.
211 #[test]
212 fn forests_reseed_cleared_tiles_and_saplings_mature() {
213 let mut map = WorldMap::filled(UVec2::splat(40), Terrain::Grass);
214 let mut zones = Zones::default();
215 zones.add_zone(
216 crate::zones::ZoneKind::Stockpile,
217 &map,
218 IVec2::new(1, 1),
219 IVec2::new(2, 2),
220 );
221 for x in 25..35 {
222 for y in 25..35 {
223 map.set_forest(UVec2::new(x, y), true);
224 }
225 }
226 map.set_plant(UVec2::new(30, 30), Some(Plant::Tree));
227 let none = Blueprints::default();
228 assert!(seed_chance(&map, &zones, &none, UVec2::new(31, 30)) > 0.0);
229 assert_eq!(seed_chance(&map, &zones, &none, UVec2::new(33, 30)), 0.0);
230 map.set_plant(UVec2::new(24, 30), Some(Plant::Tree));
231 assert_eq!(seed_chance(&map, &zones, &none, UVec2::new(23, 30)), 0.0);
232 map.set_wear(UVec2::new(29, 30), 1.0);
233 assert_eq!(seed_chance(&map, &zones, &none, UVec2::new(29, 30)), 0.0);
234 for x in 26..33 {
235 for y in 28..33 {
236 if (x + y) % 2 == 0 {
237 map.set_plant(UVec2::new(x, y), Some(Plant::Tree));
238 }
239 }
240 }
241 assert_eq!(seed_chance(&map, &zones, &none, UVec2::new(29, 29)), 0.0);
242
243 map.set_plant(UVec2::new(31, 34), Some(Plant::Sapling));
244 let day = 24.0 * crate::sim::TICKS_PER_HOUR as f32;
245 grow(&mut map, 7.0 * day, SUMMER, |_| {});
246 assert_eq!(map.plant(IVec2::new(31, 34)), Some(Plant::Sapling));
247 grow(&mut map, 3.0 * day, SUMMER, |_| {});
248 assert_eq!(map.plant(IVec2::new(31, 34)), Some(Plant::Tree));
249 }
250 }