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