src/colonist.rs 6.1 K · 193 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 map: Res<WorldMap>,
93 mut movers: Query<(&mut TilePos, &mut Path, &mut SimPos, Option<&MoveSpeed>)>,
94 ) {
95 for (mut tile, mut path, mut pos, speed) in &mut movers {
96 let Some(&next) = path.0.front() else {
97 continue;
98 };
99 let cost = map
100 .step_cost(tile.0.as_ivec2(), next.as_ivec2())
101 .unwrap_or(100) as f32
102 / 100.0;
103 let tiles_per_second = speed.map_or(TILES_PER_SECOND, |s| s.0);
104 let max_distance = tiles_per_second * TILE_SIZE / cost * time.delta_secs();
105 let target = WorldMap::tile_center(next);
106 let arrived;
107 (pos.0, arrived) = step_towards(pos.0, target, max_distance);
108 if arrived {
109 tile.0 = next;
110 path.0.pop_front();
111 }
112 }
113 }
114
115 pub fn step_towards(pos: Vec2, target: Vec2, max_distance: f32) -> (Vec2, bool) {
116 let delta = target - pos;
117 if delta.length() <= max_distance {
118 (target, true)
119 } else {
120 (pos + delta.normalize() * max_distance, false)
121 }
122 }
123
124 #[cfg(test)]
125 mod tests {
126 use super::*;
127 use crate::world::Terrain;
128 use std::time::Duration;
129
130 #[test]
131 fn step_towards_stops_exactly_on_target() {
132 assert_eq!(
133 step_towards(Vec2::ZERO, Vec2::new(3.0, 4.0), 10.0),
134 (Vec2::new(3.0, 4.0), true)
135 );
136 let (pos, arrived) = step_towards(Vec2::ZERO, Vec2::new(3.0, 4.0), 2.5);
137 assert!(!arrived);
138 assert!(pos.abs_diff_eq(Vec2::new(1.5, 2.0), 1e-5));
139 }
140
141 // A colonist halfway between two tiles keeps heading to the tile it is entering; repathing
142 // from its last reached tile would make it turn around mid-step or walk diagonally past walls.
143 #[test]
144 fn repath_while_moving_keeps_the_tile_being_entered() {
145 let map = WorldMap::filled(UVec2::new(10, 10), Terrain::Grass);
146 let current = VecDeque::from([UVec2::new(3, 2), UVec2::new(4, 2)]);
147 let path = repath(&map, UVec2::new(2, 2), &current, UVec2::new(3, 5)).unwrap();
148 assert_eq!(path.front(), Some(&UVec2::new(3, 2)));
149 assert_eq!(path.back(), Some(&UVec2::new(3, 5)));
150 }
151
152 #[test]
153 fn repath_when_idle_starts_from_current_tile() {
154 let map = WorldMap::filled(UVec2::new(10, 10), Terrain::Grass);
155 let path = repath(&map, UVec2::new(2, 2), &VecDeque::new(), UVec2::new(4, 2)).unwrap();
156 assert_eq!(path, VecDeque::from([UVec2::new(3, 2), UVec2::new(4, 2)]));
157 }
158
159 #[test]
160 fn colonists_walk_their_path_and_update_tile_pos() {
161 let mut app = App::new();
162 app.insert_resource(WorldMap::filled(UVec2::new(10, 10), Terrain::Grass))
163 .init_resource::<Time>()
164 .add_systems(Update, follow_path);
165 let colonist = app
166 .world_mut()
167 .spawn((
168 Colonist {
169 name: "Test".into(),
170 },
171 TilePos(UVec2::new(1, 1)),
172 Path(VecDeque::from([UVec2::new(2, 1), UVec2::new(3, 1)])),
173 sim_pos(WorldMap::tile_center(UVec2::new(1, 1))),
174 ))
175 .id();
176 for _ in 0..100 {
177 app.world_mut()
178 .resource_mut::<Time>()
179 .advance_by(Duration::from_millis(50));
180 app.update();
181 }
182 let world = app.world();
183 assert_eq!(
184 world.get::<TilePos>(colonist),
185 Some(&TilePos(UVec2::new(3, 1)))
186 );
187 assert!(world.get::<Path>(colonist).unwrap().0.is_empty());
188 assert_eq!(
189 world.get::<SimPos>(colonist),
190 Some(&SimPos(WorldMap::tile_center(UVec2::new(3, 1))))
191 );
192 }
193 }