src/paths.rs 6 K · 197 lines · raw · history

1 use bevy::prelude::*;
2
3 use crate::{
4 building::{Blueprints, BuildKind},
5 colonist::Colonist,
6 sim::{SimSystems, SimTick, TICKS_PER_HOUR},
7 structures::Structure,
8 world::{Terrain, TileChanged, TilePos, WorldMap},
9 zones::Zones,
10 };
11
12 pub struct PathsPlugin;
13
14 impl Plugin for PathsPlugin {
15 fn build(&self, app: &mut App) {
16 app.add_systems(
17 FixedUpdate,
18 (
19 plan_paving.in_set(SimSystems::Decide),
20 update_paths.in_set(SimSystems::Act),
21 ),
22 );
23 }
24 }
25
26 pub const PATH_WEAR: f32 = 8.0;
27 pub const PAVE_WEAR: f32 = 20.0;
28 pub const REGROW_WEAR: f32 = 1.0;
29 pub const WEAR_KEPT_PER_HOUR: f32 = 0.98;
30 pub const PAVE_BATCH: usize = 8;
31 const UPDATE_INTERVAL_TICKS: u64 = TICKS_PER_HOUR;
32
33 pub fn wears_into_path(terrain: Terrain) -> bool {
34 matches!(terrain, Terrain::Grass | Terrain::Soil | Terrain::Sand)
35 }
36
37 pub fn evolve(map: &mut WorldMap, zones: &Zones, mut on_change: impl FnMut(UVec2)) {
38 for pos in map.worn_tiles() {
39 let p = pos.as_ivec2();
40 let wear = map.wear(p) * WEAR_KEPT_PER_HOUR;
41 map.set_wear(pos, wear);
42 let Some(terrain) = map.get(p) else {
43 continue;
44 };
45 let becomes = if wears_into_path(terrain)
46 && wear >= PATH_WEAR
47 && map.plant(p).is_none()
48 && !zones.is_farm(pos)
49 {
50 Terrain::Path
51 } else if terrain == Terrain::Path && wear < REGROW_WEAR {
52 Terrain::Grass
53 } else {
54 continue;
55 };
56 map.set(pos, becomes);
57 on_change(pos);
58 }
59 }
60
61 fn update_paths(
62 tick: Res<SimTick>,
63 zones: Res<Zones>,
64 mut map: ResMut<WorldMap>,
65 mut changed: MessageWriter<TileChanged>,
66 ) {
67 if !tick.0.is_multiple_of(UPDATE_INTERVAL_TICKS) {
68 return;
69 }
70 evolve(&mut map, &zones, |pos| {
71 changed.write(TileChanged(pos));
72 });
73 }
74
75 pub fn paving_candidates(
76 map: &WorldMap,
77 blueprints: &Blueprints,
78 occupied: &[UVec2],
79 ) -> Vec<UVec2> {
80 let pending = blueprints
81 .0
82 .values()
83 .filter(|k| **k == BuildKind::Pave)
84 .count();
85 let mut busiest: Vec<UVec2> = map
86 .worn_tiles()
87 .into_iter()
88 .filter(|&p| {
89 map.get(p.as_ivec2()) == Some(Terrain::Path)
90 && map.wear(p.as_ivec2()) >= PAVE_WEAR
91 && blueprints.get(p).is_none()
92 && !occupied.contains(&p)
93 })
94 .collect();
95 busiest.sort_by(|a, b| {
96 map.wear(b.as_ivec2())
97 .total_cmp(&map.wear(a.as_ivec2()))
98 .then((a.y, a.x).cmp(&(b.y, b.x)))
99 });
100 busiest.truncate(PAVE_BATCH.saturating_sub(pending));
101 busiest
102 }
103
104 fn plan_paving(
105 tick: Res<SimTick>,
106 map: Res<WorldMap>,
107 structures: Query<&TilePos, (With<Structure>, Without<Colonist>)>,
108 mut blueprints: ResMut<Blueprints>,
109 ) {
110 if !tick.0.is_multiple_of(UPDATE_INTERVAL_TICKS) {
111 return;
112 }
113 let occupied: Vec<UVec2> = structures.iter().map(|p| p.0).collect();
114 for tile in paving_candidates(&map, &blueprints, &occupied) {
115 blueprints.0.insert(tile, BuildKind::Pave);
116 }
117 }
118
119 #[cfg(test)]
120 mod tests {
121 use super::*;
122
123 fn worn(terrain: Terrain, wear: f32) -> WorldMap {
124 let mut map = WorldMap::filled(UVec2::splat(3), terrain);
125 map.set_wear(UVec2::ONE, wear);
126 map
127 }
128
129 #[test]
130 fn busy_ground_wears_into_a_path() {
131 let mut map = worn(Terrain::Grass, PATH_WEAR * 2.0);
132 let mut changes = 0;
133 evolve(&mut map, &Zones::default(), |_| changes += 1);
134 assert_eq!(map.get(IVec2::ONE), Some(Terrain::Path));
135 assert_eq!(map.get(IVec2::ZERO), Some(Terrain::Grass));
136 assert_eq!(changes, 1);
137 }
138
139 #[test]
140 fn farms_plants_and_rock_floors_keep_their_ground() {
141 let mut farm_map = worn(Terrain::Soil, PATH_WEAR * 2.0);
142 let mut zones = Zones::default();
143 zones.add_farm_tiles(&farm_map, [UVec2::ONE]);
144 evolve(&mut farm_map, &zones, |_| {});
145 assert_eq!(farm_map.get(IVec2::ONE), Some(Terrain::Soil));
146
147 let mut bush_map = worn(Terrain::Grass, PATH_WEAR * 2.0);
148 bush_map.set_plant(UVec2::ONE, Some(crate::world::Plant::BerryBush));
149 evolve(&mut bush_map, &Zones::default(), |_| {});
150 assert_eq!(bush_map.get(IVec2::ONE), Some(Terrain::Grass));
151
152 let mut gravel = worn(Terrain::Gravel, PATH_WEAR * 2.0);
153 evolve(&mut gravel, &Zones::default(), |_| {});
154 assert_eq!(gravel.get(IVec2::ONE), Some(Terrain::Gravel));
155 }
156
157 #[test]
158 fn unused_paths_grow_back() {
159 let mut map = worn(Terrain::Path, 0.5);
160 evolve(&mut map, &Zones::default(), |_| {});
161 assert_eq!(map.get(IVec2::ONE), Some(Terrain::Grass));
162 }
163
164 // Only worn tiles are visited each hour (a full-map pass every game hour dominated frame time
165 // at high speeds), so faded wear must drop out of the worn set and stray paths must still
166 // regrow before that happens.
167 #[test]
168 fn faded_paths_regrow_and_leave_the_worn_set() {
169 let mut map = worn(Terrain::Path, PATH_WEAR);
170 for _ in 0..500 {
171 evolve(&mut map, &Zones::default(), |_| {});
172 }
173 assert_eq!(map.get(IVec2::ONE), Some(Terrain::Grass));
174 assert!(map.worn_tiles().is_empty());
175 }
176
177 #[test]
178 fn wear_fades_over_time() {
179 let mut map = worn(Terrain::Grass, 10.0);
180 evolve(&mut map, &Zones::default(), |_| {});
181 assert!(map.wear(IVec2::ONE) < 10.0);
182 }
183
184 #[test]
185 fn the_busiest_paths_are_paved_first_in_batches() {
186 let mut map = WorldMap::filled(UVec2::new(20, 1), Terrain::Path);
187 for x in 0..20 {
188 map.set_wear(UVec2::new(x, 0), PAVE_WEAR + x as f32);
189 }
190 let mut blueprints = Blueprints::default();
191 blueprints.0.insert(UVec2::new(19, 0), BuildKind::Pave);
192 let picked = paving_candidates(&map, &blueprints, &[UVec2::new(18, 0)]);
193 assert_eq!(picked.len(), PAVE_BATCH - 1);
194 assert_eq!(picked[0], UVec2::new(17, 0));
195 assert!(!picked.contains(&UVec2::new(18, 0)));
196 }
197 }