src/colonist.rs 7.5 K · 246 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.move_cost(next.as_ivec2()).unwrap_or(100) as f32 / 100.0;
158 let max_distance = TILES_PER_SECOND * TILE_SIZE / cost * time.delta_secs();
159 let target = WorldMap::tile_center(next);
160 let arrived;
161 (pos.0, arrived) = step_towards(pos.0, target, max_distance);
162 if arrived {
163 tile.0 = next;
164 path.0.pop_front();
165 }
166 }
167 }
168
169 pub fn step_towards(pos: Vec2, target: Vec2, max_distance: f32) -> (Vec2, bool) {
170 let delta = target - pos;
171 if delta.length() <= max_distance {
172 (target, true)
173 } else {
174 (pos + delta.normalize() * max_distance, false)
175 }
176 }
177
178 #[cfg(test)]
179 mod tests {
180 use super::*;
181 use crate::world::Terrain;
182
183 #[test]
184 fn step_towards_stops_exactly_on_target() {
185 assert_eq!(
186 step_towards(Vec2::ZERO, Vec2::new(3.0, 4.0), 10.0),
187 (Vec2::new(3.0, 4.0), true)
188 );
189 let (pos, arrived) = step_towards(Vec2::ZERO, Vec2::new(3.0, 4.0), 2.5);
190 assert!(!arrived);
191 assert!(pos.abs_diff_eq(Vec2::new(1.5, 2.0), 1e-5));
192 }
193
194 // A colonist halfway between two tiles keeps heading to the tile it is entering; repathing
195 // from its last reached tile would make it turn around mid-step or walk diagonally past walls.
196 #[test]
197 fn repath_while_moving_keeps_the_tile_being_entered() {
198 let map = WorldMap::filled(UVec2::new(10, 10), Terrain::Grass);
199 let current = VecDeque::from([UVec2::new(3, 2), UVec2::new(4, 2)]);
200 let path = repath(&map, UVec2::new(2, 2), &current, UVec2::new(3, 5)).unwrap();
201 assert_eq!(path.front(), Some(&UVec2::new(3, 2)));
202 assert_eq!(path.back(), Some(&UVec2::new(3, 5)));
203 }
204
205 #[test]
206 fn repath_when_idle_starts_from_current_tile() {
207 let map = WorldMap::filled(UVec2::new(10, 10), Terrain::Grass);
208 let path = repath(&map, UVec2::new(2, 2), &VecDeque::new(), UVec2::new(4, 2)).unwrap();
209 assert_eq!(path, VecDeque::from([UVec2::new(3, 2), UVec2::new(4, 2)]));
210 }
211
212 #[test]
213 fn colonists_walk_their_path_and_update_tile_pos() {
214 let mut app = App::new();
215 app.insert_resource(WorldMap::filled(UVec2::new(10, 10), Terrain::Grass))
216 .init_resource::<Time>()
217 .add_systems(Update, follow_path);
218 let colonist = app
219 .world_mut()
220 .spawn((
221 Colonist {
222 name: "Test".into(),
223 },
224 TilePos(UVec2::new(1, 1)),
225 Path(VecDeque::from([UVec2::new(2, 1), UVec2::new(3, 1)])),
226 sim_pos(WorldMap::tile_center(UVec2::new(1, 1))),
227 ))
228 .id();
229 for _ in 0..100 {
230 app.world_mut()
231 .resource_mut::<Time>()
232 .advance_by(Duration::from_millis(50));
233 app.update();
234 }
235 let world = app.world();
236 assert_eq!(
237 world.get::<TilePos>(colonist),
238 Some(&TilePos(UVec2::new(3, 1)))
239 );
240 assert!(world.get::<Path>(colonist).unwrap().0.is_empty());
241 assert_eq!(
242 world.get::<SimPos>(colonist),
243 Some(&SimPos(WorldMap::tile_center(UVec2::new(3, 1))))
244 );
245 }
246 }