src/colonist.rs 7.5 K · 249 lines · raw · history

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