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