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