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