src/colonist.rs 9.8 K · 328 lines · raw · history

1 use std::collections::VecDeque;
2
3 use bevy::prelude::*;
4
5 use crate::{
6 climate::Warmth,
7 food::Hunger,
8 items::{Carrying, ItemAssets},
9 jobs::{CurrentJob, IdleTimer, WorkPriorities},
10 needs::{Rest, sleep_marker},
11 pathfinding::find_path,
12 population::Starvation,
13 sim::{SimPos, SimSystems, sim_pos},
14 world::{TILE_SIZE, TilePos, WorldMap},
15 worldgen::{WorldGenerated, spawn_tiles},
16 };
17
18 pub struct ColonistPlugin;
19
20 impl Plugin for ColonistPlugin {
21 fn build(&self, app: &mut App) {
22 app.add_systems(Startup, create_colonist_assets)
23 .add_systems(Update, spawn_colonists.run_if(on_message::<WorldGenerated>))
24 .add_systems(FixedUpdate, follow_path.in_set(SimSystems::Move));
25 }
26 }
27
28 const TILES_PER_SECOND: f32 = 4.0;
29
30 pub const NAMES: [&str; 26] = [
31 "Ada", "Bram", "Cleo", "Dana", "Emil", "Fenna", "Gijs", "Hanna", "Ivo", "Juul", "Kees",
32 "Lotte", "Milan", "Noor", "Otto", "Pien", "Quinn", "Roos", "Sem", "Tess", "Ugo", "Vera",
33 "Wout", "Xem", "Yara", "Zeno",
34 ];
35
36 const COLORS: [Color; 8] = [
37 Color::srgb(0.9, 0.3, 0.3),
38 Color::srgb(0.3, 0.5, 0.95),
39 Color::srgb(0.95, 0.75, 0.3),
40 Color::srgb(0.6, 0.35, 0.85),
41 Color::srgb(0.3, 0.8, 0.75),
42 Color::srgb(0.95, 0.5, 0.75),
43 Color::srgb(0.55, 0.8, 0.3),
44 Color::srgb(0.95, 0.95, 0.95),
45 ];
46
47 const STARTING_COLONISTS: usize = 3;
48
49 #[derive(Resource, Default)]
50 pub struct ColonistAssets {
51 mesh: Handle<Mesh>,
52 }
53
54 fn create_colonist_assets(mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>) {
55 commands.insert_resource(ColonistAssets {
56 mesh: meshes.add(Circle::new(TILE_SIZE * 0.35)),
57 });
58 }
59
60 pub fn spawn_colonist(
61 commands: &mut Commands,
62 assets: &ColonistAssets,
63 item_assets: &ItemAssets,
64 materials: &mut Assets<ColorMaterial>,
65 index: usize,
66 tile: UVec2,
67 ) -> Entity {
68 let name = NAMES[index % NAMES.len()];
69 commands
70 .spawn((
71 Name::new(name),
72 Colonist { name: name.into() },
73 TilePos(tile),
74 Mesh2d(assets.mesh.clone()),
75 MeshMaterial2d(materials.add(COLORS[index % COLORS.len()])),
76 sim_pos(WorldMap::tile_center(tile)),
77 Transform::from_translation(WorldMap::tile_center(tile).extend(1.0)),
78 children![item_assets.carried_visual(), sleep_marker()],
79 ))
80 .id()
81 }
82
83 #[derive(Component, Debug)]
84 #[require(
85 Transform,
86 SimPos,
87 TilePos,
88 Path,
89 CurrentJob,
90 IdleTimer,
91 Carrying,
92 Rest,
93 Hunger,
94 Starvation,
95 WorkPriorities,
96 Warmth
97 )]
98 pub struct Colonist {
99 pub name: String,
100 }
101
102 #[derive(Component, Default, Debug)]
103 pub struct Path(pub VecDeque<UVec2>);
104
105 fn spawn_colonists(
106 mut commands: Commands,
107 map: Res<WorldMap>,
108 existing: Query<Entity, With<Colonist>>,
109 assets: Res<ColonistAssets>,
110 item_assets: Res<ItemAssets>,
111 mut materials: ResMut<Assets<ColorMaterial>>,
112 ) {
113 for entity in &existing {
114 commands.entity(entity).despawn();
115 }
116 let tiles = spawn_tiles(&map, STARTING_COLONISTS);
117 for (index, tile) in tiles.into_iter().enumerate() {
118 spawn_colonist(
119 &mut commands,
120 &assets,
121 &item_assets,
122 &mut materials,
123 index,
124 tile,
125 );
126 }
127 }
128
129 pub fn repath(
130 map: &WorldMap,
131 tile: UVec2,
132 current: &VecDeque<UVec2>,
133 goal: UVec2,
134 ) -> Option<VecDeque<UVec2>> {
135 match current.front() {
136 Some(&next) => {
137 let mut path: VecDeque<_> = find_path(map, next, goal)?.into();
138 path.push_front(next);
139 Some(path)
140 }
141 None => find_path(map, tile, goal).map(VecDeque::from),
142 }
143 }
144
145 #[derive(Component, Debug, Clone, Copy, PartialEq)]
146 pub struct MoveSpeed(pub f32);
147
148 pub fn follow_path(
149 time: Res<Time>,
150 mut map: ResMut<WorldMap>,
151 mut movers: Query<(
152 &mut TilePos,
153 &mut Path,
154 &mut SimPos,
155 Option<&MoveSpeed>,
156 Has<Colonist>,
157 )>,
158 ) {
159 for (mut tile, mut path, mut pos, speed, is_colonist) in &mut movers {
160 let Some(&next) = path.0.front() else {
161 continue;
162 };
163 if map.move_cost(next.as_ivec2()).is_none() {
164 path.0.clear();
165 continue;
166 }
167 let cost = map
168 .step_cost(tile.0.as_ivec2(), next.as_ivec2())
169 .unwrap_or(100) as f32
170 / 100.0;
171 let tiles_per_second = speed.map_or(TILES_PER_SECOND, |s| s.0);
172 let max_distance = tiles_per_second * TILE_SIZE / cost * time.delta_secs();
173 let target = WorldMap::tile_center(next);
174 let arrived;
175 (pos.0, arrived) = step_towards(pos.0, target, max_distance);
176 if arrived {
177 tile.0 = next;
178 path.0.pop_front();
179 if is_colonist {
180 let wear = map.wear(next.as_ivec2());
181 map.set_wear(next, wear + 1.0);
182 }
183 }
184 }
185 }
186
187 pub fn step_towards(pos: Vec2, target: Vec2, max_distance: f32) -> (Vec2, bool) {
188 let delta = target - pos;
189 if delta.length() <= max_distance {
190 (target, true)
191 } else {
192 (pos + delta.normalize() * max_distance, false)
193 }
194 }
195
196 #[cfg(test)]
197 mod tests {
198 use super::*;
199 use crate::world::Terrain;
200 use std::time::Duration;
201
202 #[test]
203 fn step_towards_stops_exactly_on_target() {
204 assert_eq!(
205 step_towards(Vec2::ZERO, Vec2::new(3.0, 4.0), 10.0),
206 (Vec2::new(3.0, 4.0), true)
207 );
208 let (pos, arrived) = step_towards(Vec2::ZERO, Vec2::new(3.0, 4.0), 2.5);
209 assert!(!arrived);
210 assert!(pos.abs_diff_eq(Vec2::new(1.5, 2.0), 1e-5));
211 }
212
213 // A colonist halfway between two tiles keeps heading to the tile it is entering; repathing
214 // from its last reached tile would make it turn around mid-step or walk diagonally past walls.
215 #[test]
216 fn repath_while_moving_keeps_the_tile_being_entered() {
217 let map = WorldMap::filled(UVec2::new(10, 10), Terrain::Grass);
218 let current = VecDeque::from([UVec2::new(3, 2), UVec2::new(4, 2)]);
219 let path = repath(&map, UVec2::new(2, 2), &current, UVec2::new(3, 5)).unwrap();
220 assert_eq!(path.front(), Some(&UVec2::new(3, 2)));
221 assert_eq!(path.back(), Some(&UVec2::new(3, 5)));
222 }
223
224 #[test]
225 fn repath_when_idle_starts_from_current_tile() {
226 let map = WorldMap::filled(UVec2::new(10, 10), Terrain::Grass);
227 let path = repath(&map, UVec2::new(2, 2), &VecDeque::new(), UVec2::new(4, 2)).unwrap();
228 assert_eq!(path, VecDeque::from([UVec2::new(3, 2), UVec2::new(4, 2)]));
229 }
230
231 #[test]
232 fn movers_stop_instead_of_walking_into_a_new_wall() {
233 let mut map = WorldMap::filled(UVec2::new(10, 10), Terrain::Grass);
234 map.set_wall(UVec2::new(3, 1), true);
235 let mut app = App::new();
236 app.insert_resource(map)
237 .init_resource::<Time>()
238 .add_systems(Update, follow_path);
239 let mover = app
240 .world_mut()
241 .spawn((
242 TilePos(UVec2::new(1, 1)),
243 Path(VecDeque::from([
244 UVec2::new(2, 1),
245 UVec2::new(3, 1),
246 UVec2::new(4, 1),
247 ])),
248 sim_pos(WorldMap::tile_center(UVec2::new(1, 1))),
249 ))
250 .id();
251 for _ in 0..40 {
252 app.world_mut()
253 .resource_mut::<Time>()
254 .advance_by(Duration::from_millis(50));
255 app.update();
256 }
257 assert_eq!(
258 app.world().get::<TilePos>(mover),
259 Some(&TilePos(UVec2::new(2, 1)))
260 );
261 assert!(app.world().get::<Path>(mover).unwrap().0.is_empty());
262 }
263
264 #[test]
265 fn colonists_wear_down_the_ground_they_walk_on() {
266 let mut app = App::new();
267 app.insert_resource(WorldMap::filled(UVec2::new(10, 10), Terrain::Grass))
268 .init_resource::<Time>()
269 .add_systems(Update, follow_path);
270 app.world_mut().spawn((
271 Colonist {
272 name: "Test".into(),
273 },
274 TilePos(UVec2::new(1, 1)),
275 Path(VecDeque::from([UVec2::new(2, 1)])),
276 sim_pos(WorldMap::tile_center(UVec2::new(1, 1))),
277 ));
278 app.world_mut().spawn((
279 TilePos(UVec2::new(1, 3)),
280 Path(VecDeque::from([UVec2::new(2, 3)])),
281 sim_pos(WorldMap::tile_center(UVec2::new(1, 3))),
282 ));
283 for _ in 0..20 {
284 app.world_mut()
285 .resource_mut::<Time>()
286 .advance_by(Duration::from_millis(50));
287 app.update();
288 }
289 let map = app.world().resource::<WorldMap>();
290 assert_eq!(map.wear(IVec2::new(2, 1)), 1.0);
291 assert_eq!(map.wear(IVec2::new(2, 3)), 0.0);
292 }
293
294 #[test]
295 fn colonists_walk_their_path_and_update_tile_pos() {
296 let mut app = App::new();
297 app.insert_resource(WorldMap::filled(UVec2::new(10, 10), Terrain::Grass))
298 .init_resource::<Time>()
299 .add_systems(Update, follow_path);
300 let colonist = app
301 .world_mut()
302 .spawn((
303 Colonist {
304 name: "Test".into(),
305 },
306 TilePos(UVec2::new(1, 1)),
307 Path(VecDeque::from([UVec2::new(2, 1), UVec2::new(3, 1)])),
308 sim_pos(WorldMap::tile_center(UVec2::new(1, 1))),
309 ))
310 .id();
311 for _ in 0..100 {
312 app.world_mut()
313 .resource_mut::<Time>()
314 .advance_by(Duration::from_millis(50));
315 app.update();
316 }
317 let world = app.world();
318 assert_eq!(
319 world.get::<TilePos>(colonist),
320 Some(&TilePos(UVec2::new(3, 1)))
321 );
322 assert!(world.get::<Path>(colonist).unwrap().0.is_empty());
323 assert_eq!(
324 world.get::<SimPos>(colonist),
325 Some(&SimPos(WorldMap::tile_center(UVec2::new(3, 1))))
326 );
327 }
328 }