src/colonist.rs 9.7 K · 325 lines · raw · history

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