src/needs.rs 8.9 K · 292 lines · raw · history

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