src/needs.rs 10 K · 319 lines · raw · history

1 use std::collections::HashSet;
2
3 use bevy::prelude::*;
4
5 use crate::{
6 building::Blueprints,
7 colonist::{Colonist, Path},
8 items::Item,
9 jobs::{CurrentJob, Job, Reservations, Target, Task},
10 pathfinding::nearest_tile,
11 sim::{SimSystems, TICKS_PER_HOUR},
12 structures::{Structure, StructureKind},
13 world::{TILE_SIZE, TilePos, WorldMap},
14 zones::{Zones, can_zone},
15 };
16
17 pub struct NeedsPlugin;
18
19 impl Plugin for NeedsPlugin {
20 fn build(&self, app: &mut App) {
21 app.add_systems(FixedUpdate, update_rest.in_set(SimSystems::Act))
22 .add_systems(Update, show_sleep_markers);
23 }
24 }
25
26 pub const REST_DRAIN_PER_TICK: f32 = 1.0 / (24.0 * TICKS_PER_HOUR as f32);
27 pub const REST_RECOVERY_PER_TICK: f32 = 1.0 / (7.0 * TICKS_PER_HOUR as f32);
28 pub const BED_REST_RECOVERY_PER_TICK: f32 = 1.0 / (5.0 * TICKS_PER_HOUR as f32);
29 pub const ROOF_REST_BONUS: f32 = 1.25;
30 pub const BEDTIME_REST: f32 = 0.9;
31 pub const NAP_REST: f32 = 0.3;
32 pub const EXHAUSTED_REST: f32 = 0.1;
33 const SLEEP_SPOT_SEARCH_TILES: usize = 3000;
34
35 #[derive(Component, Debug, Clone, Copy, PartialEq)]
36 pub struct Rest(pub f32);
37
38 impl Default for Rest {
39 fn default() -> Self {
40 Self(1.0)
41 }
42 }
43
44 pub fn is_night(hour: f32) -> bool {
45 !(6.0..21.0).contains(&hour)
46 }
47
48 pub fn wants_sleep(rest: f32, hour: f32) -> bool {
49 rest < NAP_REST || (is_night(hour) && rest < BEDTIME_REST)
50 }
51
52 pub fn done_sleeping(rest: f32, hour: f32) -> bool {
53 rest >= 1.0 && !is_night(hour)
54 }
55
56 pub fn is_sleeping(job: &CurrentJob, tile: &TilePos, path: &Path) -> bool {
57 matches!(
58 job.0,
59 Some(Job {
60 task: Task::Sleep { spot },
61 ..
62 }) if spot == tile.0 && path.0.is_empty()
63 )
64 }
65
66 pub fn update_rest(
67 map: Res<WorldMap>,
68 structures: Query<(&TilePos, &Structure), Without<Colonist>>,
69 mut colonists: Query<(&mut Rest, &CurrentJob, &TilePos, &Path), With<Colonist>>,
70 ) {
71 let beds: HashSet<UVec2> = structures
72 .iter()
73 .filter(|(_, s)| s.kind == StructureKind::Bed)
74 .map(|(pos, _)| pos.0)
75 .collect();
76 for (mut rest, job, tile, path) in &mut colonists {
77 let change = match (is_sleeping(job, tile, path), beds.contains(&tile.0)) {
78 (true, true) => BED_REST_RECOVERY_PER_TICK,
79 (true, false) => REST_RECOVERY_PER_TICK,
80 (false, _) => -REST_DRAIN_PER_TICK,
81 };
82 let change = if change > 0.0 && map.is_roofed(tile.0.as_ivec2()) {
83 change * ROOF_REST_BONUS
84 } else {
85 change
86 };
87 rest.0 = (rest.0 + change).clamp(0.0, 1.0);
88 }
89 }
90
91 pub struct SleepContext<'a> {
92 pub map: &'a WorldMap,
93 pub zones: &'a Zones,
94 pub reservations: &'a Reservations,
95 pub items: &'a [(Item, TilePos)],
96 pub structures: &'a [(UVec2, StructureKind, u32)],
97 pub blueprints: &'a Blueprints,
98 }
99
100 impl SleepContext<'_> {
101 fn free(&self, tile: UVec2) -> bool {
102 can_zone(self.map, tile.as_ivec2())
103 && self.map.plant(tile.as_ivec2()).is_none()
104 && !self.zones.is_stockpile(tile)
105 && !self.structures.iter().any(|(pos, ..)| *pos == tile)
106 && !self.items.iter().any(|(_, pos)| pos.0 == tile)
107 && self.blueprints.get(tile).is_none()
108 && self.reservations.is_free(Target::SleepSpot(tile))
109 }
110
111 fn nearest(&self, kind: StructureKind, from: UVec2) -> Option<UVec2> {
112 self.structures
113 .iter()
114 .filter(|(pos, k, _)| {
115 *k == kind
116 && (kind != StructureKind::Bed
117 || self.reservations.is_free(Target::SleepSpot(*pos)))
118 })
119 .map(|(pos, ..)| *pos)
120 .min_by(|a, b| {
121 a.as_vec2()
122 .distance_squared(from.as_vec2())
123 .total_cmp(&b.as_vec2().distance_squared(from.as_vec2()))
124 })
125 }
126
127 pub fn spot(&self, from: UVec2) -> UVec2 {
128 if let Some(bed) = self.nearest(StructureKind::Bed, from) {
129 return bed;
130 }
131 let anchor = self
132 .nearest(StructureKind::Campfire, from)
133 .unwrap_or_else(|| self.zones.home(from));
134 nearest_tile(self.map, anchor, SLEEP_SPOT_SEARCH_TILES, |p| self.free(p)).unwrap_or(from)
135 }
136 }
137
138 #[derive(Component)]
139 pub struct SleepMarker;
140
141 pub fn sleep_marker() -> impl Bundle {
142 (
143 SleepMarker,
144 Text2d::new("z"),
145 TextFont::from_font_size(12.0),
146 TextColor(Color::srgb(0.85, 0.9, 1.0)),
147 Transform::from_xyz(TILE_SIZE * 0.35, TILE_SIZE * 0.45, 0.2),
148 Visibility::Hidden,
149 )
150 }
151
152 fn show_sleep_markers(
153 colonists: Query<(&CurrentJob, &TilePos, &Path, &Children), With<Colonist>>,
154 mut markers: Query<&mut Visibility, With<SleepMarker>>,
155 ) {
156 for (job, tile, path, children) in &colonists {
157 let shown = if is_sleeping(job, tile, path) {
158 Visibility::Inherited
159 } else {
160 Visibility::Hidden
161 };
162 for &child in children {
163 if let Ok(mut visibility) = markers.get_mut(child) {
164 visibility.set_if_neq(shown);
165 }
166 }
167 }
168 }
169
170 #[cfg(test)]
171 mod tests {
172 use super::*;
173 use crate::world::Terrain;
174
175 #[test]
176 fn colonists_sleep_at_night_and_nap_only_when_tired() {
177 assert!(wants_sleep(0.8, 22.0));
178 assert!(!wants_sleep(0.95, 22.0));
179 assert!(!wants_sleep(0.8, 12.0));
180 assert!(wants_sleep(0.2, 12.0));
181 }
182
183 // A colonist that is fully rested at 3 am keeps resting rather than wandering off into the
184 // dark; the night ends sleep, a full rest bar alone does not.
185 #[test]
186 fn sleep_lasts_until_morning() {
187 assert!(!done_sleeping(1.0, 3.0));
188 assert!(done_sleeping(1.0, 7.0));
189 assert!(!done_sleeping(0.7, 7.0));
190 }
191
192 #[test]
193 fn rest_drains_awake_and_recovers_asleep() {
194 let mut app = App::new();
195 app.insert_resource(WorldMap::filled(UVec2::splat(10), Terrain::Grass))
196 .add_systems(Update, update_rest);
197 let tile = UVec2::new(3, 3);
198 let awake = app
199 .world_mut()
200 .spawn((
201 Colonist {
202 name: "Waker".into(),
203 },
204 Rest(0.5),
205 CurrentJob(None),
206 TilePos(tile),
207 Path::default(),
208 ))
209 .id();
210 let asleep = app
211 .world_mut()
212 .spawn((
213 Colonist {
214 name: "Sleeper".into(),
215 },
216 Rest(0.5),
217 CurrentJob(Some(Job {
218 task: Task::Sleep { spot: tile },
219 ordered: false,
220 })),
221 TilePos(tile),
222 Path::default(),
223 ))
224 .id();
225 app.update();
226 assert!(app.world().get::<Rest>(awake).unwrap().0 < 0.5);
227 assert!(app.world().get::<Rest>(asleep).unwrap().0 > 0.5);
228 }
229
230 fn sleeper(app: &mut App, tile: UVec2) -> Entity {
231 app.world_mut()
232 .spawn((
233 Colonist {
234 name: "Sleeper".into(),
235 },
236 Rest(0.2),
237 CurrentJob(Some(Job {
238 task: Task::Sleep { spot: tile },
239 ordered: false,
240 })),
241 TilePos(tile),
242 Path::default(),
243 ))
244 .id()
245 }
246
247 #[test]
248 fn beds_restore_rest_faster_than_the_ground() {
249 let mut app = App::new();
250 app.insert_resource(WorldMap::filled(UVec2::splat(10), Terrain::Grass))
251 .add_systems(Update, update_rest);
252 let bed = UVec2::new(1, 1);
253 app.world_mut().spawn((
254 Structure {
255 kind: StructureKind::Bed,
256 },
257 TilePos(bed),
258 ));
259 let in_bed = sleeper(&mut app, bed);
260 let on_ground = sleeper(&mut app, UVec2::new(5, 5));
261 app.update();
262 let rest = |e| app.world().get::<Rest>(e).unwrap().0;
263 assert!(rest(in_bed) > rest(on_ground));
264 }
265
266 #[test]
267 fn sleeping_under_a_roof_restores_more_rest() {
268 let mut map = WorldMap::filled(UVec2::splat(10), Terrain::Grass);
269 map.set_roof(UVec2::new(1, 1), true);
270 let mut app = App::new();
271 app.insert_resource(map).add_systems(Update, update_rest);
272 let sheltered = sleeper(&mut app, UVec2::new(1, 1));
273 let outside = sleeper(&mut app, UVec2::new(5, 5));
274 app.update();
275 let rest = |e| app.world().get::<Rest>(e).unwrap().0;
276 assert!(rest(sheltered) > rest(outside));
277 }
278
279 #[test]
280 fn sleepers_take_a_free_bed_over_the_campfire() {
281 let map = WorldMap::filled(UVec2::splat(20), Terrain::Grass);
282 let zones = Zones::default();
283 let mut reservations = Reservations::default();
284 let structures = [
285 (UVec2::new(3, 3), StructureKind::Campfire, 1000),
286 (UVec2::new(15, 15), StructureKind::Bed, u32::MAX),
287 (UVec2::new(16, 15), StructureKind::Bed, u32::MAX),
288 ];
289 reservations.reserve(Target::SleepSpot(UVec2::new(15, 15)), Entity::PLACEHOLDER);
290 let ctx = SleepContext {
291 map: &map,
292 zones: &zones,
293 reservations: &reservations,
294 items: &[],
295 structures: &structures,
296 blueprints: &Blueprints::default(),
297 };
298 assert_eq!(ctx.spot(UVec2::new(2, 2)), UVec2::new(16, 15));
299 }
300
301 #[test]
302 fn sleepers_bed_down_by_the_campfire() {
303 let map = WorldMap::filled(UVec2::splat(20), Terrain::Grass);
304 let zones = Zones::default();
305 let reservations = Reservations::default();
306 let fires = [(UVec2::new(15, 15), StructureKind::Campfire, 1000)];
307 let ctx = SleepContext {
308 map: &map,
309 zones: &zones,
310 reservations: &reservations,
311 items: &[],
312 structures: &fires,
313 blueprints: &Blueprints::default(),
314 };
315 let spot = ctx.spot(UVec2::new(2, 2));
316 assert!(spot.as_vec2().distance(Vec2::splat(15.0)) < 2.0);
317 assert_ne!(spot, UVec2::new(15, 15));
318 }
319 }