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