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