src/colonist.rs 5.7 K · 182 lines · raw · history

1 use std::collections::VecDeque;
2
3 use bevy::prelude::*;
4
5 use crate::{
6 jobs::{CurrentJob, IdleTimer},
7 pathfinding::find_path,
8 sim::{SimPos, SimSystems, sim_pos},
9 world::{TILE_SIZE, TilePos, 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.add_systems(Update, spawn_colonists.run_if(on_message::<WorldGenerated>))
18 .add_systems(FixedUpdate, follow_path.in_set(SimSystems::Move));
19 }
20 }
21
22 const TILES_PER_SECOND: f32 = 4.0;
23
24 const STARTING_COLONISTS: [(&str, Color); 3] = [
25 ("Ada", Color::srgb(0.9, 0.3, 0.3)),
26 ("Bram", Color::srgb(0.3, 0.5, 0.95)),
27 ("Cleo", Color::srgb(0.95, 0.75, 0.3)),
28 ];
29
30 #[derive(Component, Debug)]
31 #[require(Transform, SimPos, TilePos, Path, CurrentJob, IdleTimer)]
32 pub struct Colonist {
33 pub name: String,
34 }
35
36 #[derive(Component, Default, Debug)]
37 pub struct Path(pub VecDeque<UVec2>);
38
39 fn spawn_colonists(
40 mut commands: Commands,
41 map: Res<WorldMap>,
42 existing: Query<Entity, With<Colonist>>,
43 mut meshes: ResMut<Assets<Mesh>>,
44 mut materials: ResMut<Assets<ColorMaterial>>,
45 ) {
46 for entity in &existing {
47 commands.entity(entity).despawn();
48 }
49 let mesh = meshes.add(Circle::new(TILE_SIZE * 0.35));
50 let tiles = spawn_tiles(&map, STARTING_COLONISTS.len());
51 for ((name, color), tile) in STARTING_COLONISTS.into_iter().zip(tiles) {
52 commands.spawn((
53 Name::new(name),
54 Colonist { name: name.into() },
55 TilePos(tile),
56 Mesh2d(mesh.clone()),
57 MeshMaterial2d(materials.add(color)),
58 sim_pos(WorldMap::tile_center(tile)),
59 Transform::from_translation(WorldMap::tile_center(tile).extend(1.0)),
60 ));
61 }
62 }
63
64 pub fn repath(
65 map: &WorldMap,
66 tile: UVec2,
67 current: &VecDeque<UVec2>,
68 goal: UVec2,
69 ) -> Option<VecDeque<UVec2>> {
70 match current.front() {
71 Some(&next) => {
72 let mut path: VecDeque<_> = find_path(map, next, goal)?.into();
73 path.push_front(next);
74 Some(path)
75 }
76 None => find_path(map, tile, goal).map(VecDeque::from),
77 }
78 }
79
80 pub fn follow_path(
81 time: Res<Time>,
82 map: Res<WorldMap>,
83 mut colonists: Query<(&mut TilePos, &mut Path, &mut SimPos)>,
84 ) {
85 for (mut tile, mut path, mut pos) in &mut colonists {
86 let Some(&next) = path.0.front() else {
87 continue;
88 };
89 let cost = map
90 .step_cost(tile.0.as_ivec2(), next.as_ivec2())
91 .unwrap_or(100) as f32
92 / 100.0;
93 let max_distance = TILES_PER_SECOND * TILE_SIZE / cost * time.delta_secs();
94 let target = WorldMap::tile_center(next);
95 let arrived;
96 (pos.0, arrived) = step_towards(pos.0, target, max_distance);
97 if arrived {
98 tile.0 = next;
99 path.0.pop_front();
100 }
101 }
102 }
103
104 pub fn step_towards(pos: Vec2, target: Vec2, max_distance: f32) -> (Vec2, bool) {
105 let delta = target - pos;
106 if delta.length() <= max_distance {
107 (target, true)
108 } else {
109 (pos + delta.normalize() * max_distance, false)
110 }
111 }
112
113 #[cfg(test)]
114 mod tests {
115 use super::*;
116 use crate::world::Terrain;
117 use std::time::Duration;
118
119 #[test]
120 fn step_towards_stops_exactly_on_target() {
121 assert_eq!(
122 step_towards(Vec2::ZERO, Vec2::new(3.0, 4.0), 10.0),
123 (Vec2::new(3.0, 4.0), true)
124 );
125 let (pos, arrived) = step_towards(Vec2::ZERO, Vec2::new(3.0, 4.0), 2.5);
126 assert!(!arrived);
127 assert!(pos.abs_diff_eq(Vec2::new(1.5, 2.0), 1e-5));
128 }
129
130 // A colonist halfway between two tiles keeps heading to the tile it is entering; repathing
131 // from its last reached tile would make it turn around mid-step or walk diagonally past walls.
132 #[test]
133 fn repath_while_moving_keeps_the_tile_being_entered() {
134 let map = WorldMap::filled(UVec2::new(10, 10), Terrain::Grass);
135 let current = VecDeque::from([UVec2::new(3, 2), UVec2::new(4, 2)]);
136 let path = repath(&map, UVec2::new(2, 2), &current, UVec2::new(3, 5)).unwrap();
137 assert_eq!(path.front(), Some(&UVec2::new(3, 2)));
138 assert_eq!(path.back(), Some(&UVec2::new(3, 5)));
139 }
140
141 #[test]
142 fn repath_when_idle_starts_from_current_tile() {
143 let map = WorldMap::filled(UVec2::new(10, 10), Terrain::Grass);
144 let path = repath(&map, UVec2::new(2, 2), &VecDeque::new(), UVec2::new(4, 2)).unwrap();
145 assert_eq!(path, VecDeque::from([UVec2::new(3, 2), UVec2::new(4, 2)]));
146 }
147
148 #[test]
149 fn colonists_walk_their_path_and_update_tile_pos() {
150 let mut app = App::new();
151 app.insert_resource(WorldMap::filled(UVec2::new(10, 10), Terrain::Grass))
152 .init_resource::<Time>()
153 .add_systems(Update, follow_path);
154 let colonist = app
155 .world_mut()
156 .spawn((
157 Colonist {
158 name: "Test".into(),
159 },
160 TilePos(UVec2::new(1, 1)),
161 Path(VecDeque::from([UVec2::new(2, 1), UVec2::new(3, 1)])),
162 sim_pos(WorldMap::tile_center(UVec2::new(1, 1))),
163 ))
164 .id();
165 for _ in 0..100 {
166 app.world_mut()
167 .resource_mut::<Time>()
168 .advance_by(Duration::from_millis(50));
169 app.update();
170 }
171 let world = app.world();
172 assert_eq!(
173 world.get::<TilePos>(colonist),
174 Some(&TilePos(UVec2::new(3, 1)))
175 );
176 assert!(world.get::<Path>(colonist).unwrap().0.is_empty());
177 assert_eq!(
178 world.get::<SimPos>(colonist),
179 Some(&SimPos(WorldMap::tile_center(UVec2::new(3, 1))))
180 );
181 }
182 }