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