src/needs.rs 6 K · 203 lines · raw · history

1 use bevy::prelude::*;
2
3 use crate::{
4 colonist::{Colonist, Path},
5 items::Item,
6 jobs::{CurrentJob, Job, Reservations, Target, Task},
7 light::StructureKind,
8 pathfinding::nearest_tile,
9 sim::{SimSystems, TICKS_PER_HOUR},
10 world::{TILE_SIZE, TilePos, WorldMap},
11 zones::{Zones, can_zone},
12 };
13
14 pub struct NeedsPlugin;
15
16 impl Plugin for NeedsPlugin {
17 fn build(&self, app: &mut App) {
18 app.add_systems(FixedUpdate, update_rest.in_set(SimSystems::Act))
19 .add_systems(Update, show_sleep_markers);
20 }
21 }
22
23 pub const REST_DRAIN_PER_TICK: f32 = 1.0 / (24.0 * TICKS_PER_HOUR as f32);
24 pub const REST_RECOVERY_PER_TICK: f32 = 1.0 / (7.0 * TICKS_PER_HOUR as f32);
25 pub const BEDTIME_REST: f32 = 0.9;
26 pub const NAP_REST: f32 = 0.3;
27 pub const EXHAUSTED_REST: f32 = 0.1;
28 const SLEEP_SPOT_SEARCH_TILES: usize = 3000;
29
30 #[derive(Component, Debug, Clone, Copy, PartialEq)]
31 pub struct Rest(pub f32);
32
33 impl Default for Rest {
34 fn default() -> Self {
35 Self(1.0)
36 }
37 }
38
39 pub fn is_night(hour: f32) -> bool {
40 !(6.0..21.0).contains(&hour)
41 }
42
43 pub fn wants_sleep(rest: f32, hour: f32) -> bool {
44 rest < NAP_REST || (is_night(hour) && rest < BEDTIME_REST)
45 }
46
47 pub fn done_sleeping(rest: f32, hour: f32) -> bool {
48 rest >= 1.0 && !is_night(hour)
49 }
50
51 pub fn is_sleeping(job: &CurrentJob, tile: &TilePos, path: &Path) -> bool {
52 matches!(
53 job.0,
54 Some(Job {
55 task: Task::Sleep { spot },
56 ..
57 }) if spot == tile.0 && path.0.is_empty()
58 )
59 }
60
61 pub fn update_rest(mut colonists: Query<(&mut Rest, &CurrentJob, &TilePos, &Path)>) {
62 for (mut rest, job, tile, path) in &mut colonists {
63 let change = if is_sleeping(job, tile, path) {
64 REST_RECOVERY_PER_TICK
65 } else {
66 -REST_DRAIN_PER_TICK
67 };
68 rest.0 = (rest.0 + change).clamp(0.0, 1.0);
69 }
70 }
71
72 pub struct SleepContext<'a> {
73 pub map: &'a WorldMap,
74 pub zones: &'a Zones,
75 pub reservations: &'a Reservations,
76 pub items: &'a [(Item, TilePos)],
77 pub fires: &'a [(UVec2, StructureKind, u32)],
78 }
79
80 impl SleepContext<'_> {
81 fn free(&self, tile: UVec2) -> bool {
82 can_zone(self.map, tile.as_ivec2())
83 && self.map.plant(tile.as_ivec2()).is_none()
84 && !self.zones.is_stockpile(tile)
85 && !self.fires.iter().any(|(pos, ..)| *pos == tile)
86 && !self.items.iter().any(|(_, pos)| pos.0 == tile)
87 && self.reservations.is_free(Target::SleepSpot(tile))
88 }
89
90 pub fn spot(&self, from: UVec2) -> UVec2 {
91 let campfire = self
92 .fires
93 .iter()
94 .filter(|(_, kind, _)| *kind == StructureKind::Campfire)
95 .map(|(pos, ..)| *pos)
96 .min_by(|a, b| {
97 a.as_vec2()
98 .distance_squared(from.as_vec2())
99 .total_cmp(&b.as_vec2().distance_squared(from.as_vec2()))
100 });
101 let anchor = campfire.unwrap_or_else(|| self.zones.home(from));
102 nearest_tile(self.map, anchor, SLEEP_SPOT_SEARCH_TILES, |p| self.free(p)).unwrap_or(from)
103 }
104 }
105
106 #[derive(Component)]
107 pub struct SleepMarker;
108
109 pub fn sleep_marker() -> impl Bundle {
110 (
111 SleepMarker,
112 Text2d::new("z"),
113 TextFont::from_font_size(12.0),
114 TextColor(Color::srgb(0.85, 0.9, 1.0)),
115 Transform::from_xyz(TILE_SIZE * 0.35, TILE_SIZE * 0.45, 0.2),
116 Visibility::Hidden,
117 )
118 }
119
120 fn show_sleep_markers(
121 colonists: Query<(&CurrentJob, &TilePos, &Path, &Children), With<Colonist>>,
122 mut markers: Query<&mut Visibility, With<SleepMarker>>,
123 ) {
124 for (job, tile, path, children) in &colonists {
125 let shown = if is_sleeping(job, tile, path) {
126 Visibility::Inherited
127 } else {
128 Visibility::Hidden
129 };
130 for &child in children {
131 if let Ok(mut visibility) = markers.get_mut(child) {
132 visibility.set_if_neq(shown);
133 }
134 }
135 }
136 }
137
138 #[cfg(test)]
139 mod tests {
140 use super::*;
141 use crate::world::Terrain;
142
143 #[test]
144 fn colonists_sleep_at_night_and_nap_only_when_tired() {
145 assert!(wants_sleep(0.8, 22.0));
146 assert!(!wants_sleep(0.95, 22.0));
147 assert!(!wants_sleep(0.8, 12.0));
148 assert!(wants_sleep(0.2, 12.0));
149 }
150
151 // A colonist that is fully rested at 3 am keeps resting rather than wandering off into the
152 // dark; the night ends sleep, a full rest bar alone does not.
153 #[test]
154 fn sleep_lasts_until_morning() {
155 assert!(!done_sleeping(1.0, 3.0));
156 assert!(done_sleeping(1.0, 7.0));
157 assert!(!done_sleeping(0.7, 7.0));
158 }
159
160 #[test]
161 fn rest_drains_awake_and_recovers_asleep() {
162 let mut app = App::new();
163 app.add_systems(Update, update_rest);
164 let tile = UVec2::new(3, 3);
165 let awake = app
166 .world_mut()
167 .spawn((Rest(0.5), CurrentJob(None), TilePos(tile), Path::default()))
168 .id();
169 let asleep = app
170 .world_mut()
171 .spawn((
172 Rest(0.5),
173 CurrentJob(Some(Job {
174 task: Task::Sleep { spot: tile },
175 ordered: false,
176 })),
177 TilePos(tile),
178 Path::default(),
179 ))
180 .id();
181 app.update();
182 assert!(app.world().get::<Rest>(awake).unwrap().0 < 0.5);
183 assert!(app.world().get::<Rest>(asleep).unwrap().0 > 0.5);
184 }
185
186 #[test]
187 fn sleepers_bed_down_by_the_campfire() {
188 let map = WorldMap::filled(UVec2::splat(20), Terrain::Grass);
189 let zones = Zones::default();
190 let reservations = Reservations::default();
191 let fires = [(UVec2::new(15, 15), StructureKind::Campfire, 1000)];
192 let ctx = SleepContext {
193 map: &map,
194 zones: &zones,
195 reservations: &reservations,
196 items: &[],
197 fires: &fires,
198 };
199 let spot = ctx.spot(UVec2::new(2, 2));
200 assert!(spot.as_vec2().distance(Vec2::splat(15.0)) < 2.0);
201 assert_ne!(spot, UVec2::new(15, 15));
202 }
203 }