src/needs.rs 13 K · 403 lines · raw · history

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