src/colonist.rs 5.8 K · 185 lines · raw · history

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