src/farming.rs 4.5 K · 147 lines · raw · history

1 use bevy::prelude::*;
2
3 use crate::{
4 building::Blueprints,
5 colonist::Colonist,
6 items::Item,
7 pathfinding::nearest_tile,
8 plants::fertility,
9 sim::{SimSystems, SimTick},
10 structures::Structure,
11 world::{Terrain, TilePos, WorldMap},
12 zones::{Zones, can_farm},
13 };
14
15 pub struct FarmingPlugin;
16
17 impl Plugin for FarmingPlugin {
18 fn build(&self, app: &mut App) {
19 app.add_systems(FixedUpdate, plan_farm.in_set(SimSystems::Decide));
20 }
21 }
22
23 pub const FARM_SIZE: u32 = 5;
24 pub const FARM_CLEARANCE: i32 = 2;
25 const PLAN_INTERVAL_TICKS: u64 = 600;
26 const FARM_SEARCH_TILES: usize = 3000;
27 const SOIL_WEIGHT: f32 = 4.0;
28
29 pub struct FarmContext<'a> {
30 pub map: &'a WorldMap,
31 pub zones: &'a Zones,
32 pub blueprints: &'a Blueprints,
33 pub occupied: &'a [UVec2],
34 }
35
36 impl FarmContext<'_> {
37 fn square(corner: UVec2) -> impl Iterator<Item = UVec2> {
38 (0..FARM_SIZE).flat_map(move |y| (0..FARM_SIZE).map(move |x| corner + UVec2::new(x, y)))
39 }
40
41 fn clear(&self, tile: UVec2) -> bool {
42 let near = |other: UVec2| {
43 (other.as_ivec2() - tile.as_ivec2()).abs().max_element() <= FARM_CLEARANCE
44 };
45 can_farm(self.map, tile.as_ivec2())
46 && self.map.plant(tile.as_ivec2()).is_none()
47 && !self.zones.stockpile_tiles().any(near)
48 && !self.blueprints.0.keys().copied().any(near)
49 && !self.occupied.iter().copied().any(near)
50 }
51
52 fn score(&self, corner: UVec2, home: UVec2) -> Option<f32> {
53 let mut soil = 0.0;
54 for tile in Self::square(corner) {
55 if !self.clear(tile) {
56 return None;
57 }
58 soil += fertility(self.map.get(tile.as_ivec2()).unwrap_or(Terrain::Sand));
59 }
60 Some(soil * SOIL_WEIGHT - corner.as_vec2().distance(home.as_vec2()))
61 }
62
63 pub fn plan(&self, home: UVec2) -> Option<Vec<UVec2>> {
64 let mut best: Option<(f32, UVec2)> = None;
65 nearest_tile(self.map, home, FARM_SEARCH_TILES, |corner| {
66 if let Some(score) = self.score(corner, home)
67 && best.is_none_or(|(b, _)| score > b)
68 {
69 best = Some((score, corner));
70 }
71 false
72 });
73 best.map(|(_, corner)| Self::square(corner).collect())
74 }
75 }
76
77 fn plan_farm(
78 tick: Res<SimTick>,
79 map: Res<WorldMap>,
80 mut zones: ResMut<Zones>,
81 blueprints: Res<Blueprints>,
82 structures: Query<&TilePos, (With<Structure>, Without<Colonist>)>,
83 items: Query<&TilePos, With<Item>>,
84 ) {
85 if !tick.0.is_multiple_of(PLAN_INTERVAL_TICKS) || zones.has_farm() || !zones.has_stockpile() {
86 return;
87 }
88 let occupied: Vec<UVec2> = structures.iter().chain(items.iter()).map(|p| p.0).collect();
89 let home = zones.home(map.size / 2);
90 let ctx = FarmContext {
91 map: &map,
92 zones: &zones,
93 blueprints: &blueprints,
94 occupied: &occupied,
95 };
96 if let Some(tiles) = ctx.plan(home) {
97 zones.add_farm_tiles(&map, tiles);
98 }
99 }
100
101 #[cfg(test)]
102 mod tests {
103 use super::*;
104
105 fn plan(map: &WorldMap, zones: &Zones, occupied: &[UVec2]) -> Vec<UVec2> {
106 FarmContext {
107 map,
108 zones,
109 blueprints: &Blueprints::default(),
110 occupied,
111 }
112 .plan(UVec2::new(20, 20))
113 .unwrap()
114 }
115
116 #[test]
117 fn farms_go_on_the_most_fertile_ground_nearby() {
118 let mut map = WorldMap::filled(UVec2::splat(40), Terrain::Sand);
119 for y in 26..34 {
120 for x in 10..18 {
121 map.set(UVec2::new(x, y), Terrain::Soil);
122 }
123 }
124 let farm = plan(&map, &Zones::default(), &[]);
125 assert_eq!(farm.len(), (FARM_SIZE * FARM_SIZE) as usize);
126 assert!(
127 farm.iter()
128 .all(|t| map.get(t.as_ivec2()) == Some(Terrain::Soil))
129 );
130 }
131
132 // Beds, fires and the stockpile need room around them for walls and walkways, and a farm
133 // packed against them would be dug into by every later shelter plan.
134 #[test]
135 fn farms_keep_their_distance_from_the_colony() {
136 let map = WorldMap::filled(UVec2::splat(40), Terrain::Soil);
137 let mut zones = Zones::default();
138 zones.add_tiles(&map, [UVec2::new(20, 20)]);
139 let bed = UVec2::new(24, 20);
140 let farm = plan(&map, &zones, &[bed]);
141 for tile in farm {
142 for other in [UVec2::new(20, 20), bed] {
143 assert!((tile.as_ivec2() - other.as_ivec2()).abs().max_element() > FARM_CLEARANCE);
144 }
145 }
146 }
147 }