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