src/threats.rs 9.8 K · 302 lines · raw · history

1 use std::collections::HashSet;
2
3 use bevy::prelude::*;
4
5 use crate::{
6 animals::Animal,
7 colonist::{Colonist, Path},
8 items::{Carrying, ItemAssets, ItemQuery, drop_items},
9 jobs::{CurrentJob, Reservations, Target},
10 mood::Losses,
11 needs::is_sleeping,
12 pathfinding::{find_path_adjacent, is_adjacent},
13 population::ColonyLog,
14 sim::{SimSystems, SimTick, TICKS_PER_HOUR},
15 structures::{Structure, StructureKind},
16 world::{TilePos, WorldMap},
17 };
18
19 pub struct ThreatsPlugin;
20
21 impl Plugin for ThreatsPlugin {
22 fn build(&self, app: &mut App) {
23 app.add_systems(
24 FixedUpdate,
25 (
26 prowl.in_set(SimSystems::Decide),
27 (bite, heal).chain().in_set(SimSystems::Act),
28 ),
29 );
30 }
31 }
32
33 pub const PROWL_RANGE: i32 = 24;
34 pub const COLONIST_DETOUR: i32 = 6;
35 pub const COLONIST_BITE: f32 = 0.25;
36 pub const ANIMAL_BITE: f32 = 0.5;
37 pub const BITE_COOLDOWN_TICKS: u32 = 60;
38 pub const FED_TICKS: u64 = 12 * TICKS_PER_HOUR;
39 pub const INJURED: f32 = 0.5;
40 pub const HURT: f32 = 0.75;
41 const HEAL_PER_DAY: f32 = 0.3;
42 const BED_HEAL_FACTOR: f32 = 3.0;
43 const HEAL_INTERVAL_TICKS: u64 = 60;
44 const PROWL_INTERVAL_TICKS: u64 = 30;
45
46 #[derive(Component, Debug, Clone, Copy, PartialEq)]
47 pub struct Health(pub f32);
48
49 impl Default for Health {
50 fn default() -> Self {
51 Self(1.0)
52 }
53 }
54
55 #[derive(Component, Debug, Default, Clone, Copy, PartialEq)]
56 pub struct Predator {
57 pub target: Option<Entity>,
58 pub fed_until: u64,
59 pub cooldown: u32,
60 }
61
62 #[derive(Clone, Copy, Debug, PartialEq)]
63 pub struct Prey {
64 pub entity: Entity,
65 pub tile: UVec2,
66 pub colonist: bool,
67 }
68
69 pub fn pick_prey(from: UVec2, prey: &[Prey]) -> Option<Prey> {
70 prey.iter()
71 .filter_map(|p| {
72 let distance = (p.tile.as_ivec2() - from.as_ivec2()).abs().max_element();
73 let cost = distance + if p.colonist { COLONIST_DETOUR } else { 0 };
74 (distance <= PROWL_RANGE).then_some((cost, p))
75 })
76 .min_by_key(|(cost, p)| (*cost, p.tile.y, p.tile.x))
77 .map(|(_, p)| *p)
78 }
79
80 #[allow(clippy::type_complexity)]
81 fn prowl(
82 tick: Res<SimTick>,
83 map: Res<WorldMap>,
84 mut wolves: Query<(&TilePos, &mut Path, &mut Predator), With<Animal>>,
85 animals: Query<(Entity, &TilePos), (With<Animal>, Without<Predator>)>,
86 colonists: Query<(Entity, &TilePos), With<Colonist>>,
87 ) {
88 if !tick.0.is_multiple_of(PROWL_INTERVAL_TICKS) {
89 return;
90 }
91 let prey: Vec<Prey> = animals
92 .iter()
93 .map(|(entity, tile)| Prey {
94 entity,
95 tile: tile.0,
96 colonist: false,
97 })
98 .chain(colonists.iter().map(|(entity, tile)| Prey {
99 entity,
100 tile: tile.0,
101 colonist: true,
102 }))
103 .collect();
104 for (tile, mut path, mut predator) in &mut wolves {
105 if tick.0 < predator.fed_until {
106 predator.target = None;
107 continue;
108 }
109 let Some(target) = pick_prey(tile.0, &prey) else {
110 predator.target = None;
111 continue;
112 };
113 predator.target = Some(target.entity);
114 let end = path.0.back().copied().unwrap_or(tile.0);
115 if is_adjacent(end.as_ivec2(), target.tile.as_ivec2()) || end == target.tile {
116 continue;
117 }
118 if let Some(route) = find_path_adjacent(&map, tile.0, target.tile) {
119 let entering = path.0.front().copied();
120 path.0 = entering.into_iter().chain(route).collect();
121 }
122 }
123 }
124
125 #[allow(clippy::too_many_arguments, clippy::type_complexity)]
126 fn bite(
127 mut commands: Commands,
128 tick: Res<SimTick>,
129 item_assets: Res<ItemAssets>,
130 mut log: ResMut<ColonyLog>,
131 mut losses: ResMut<Losses>,
132 mut reservations: ResMut<Reservations>,
133 mut items: ItemQuery,
134 mut wolves: Query<(&TilePos, &mut Predator), With<Animal>>,
135 mut colonists: Query<
136 (&Colonist, &TilePos, &mut Health, &Carrying),
137 (Without<Animal>, Without<Predator>),
138 >,
139 mut prey: Query<(&TilePos, &mut Health), (With<Animal>, Without<Predator>, Without<Colonist>)>,
140 ) {
141 for (tile, mut predator) in &mut wolves {
142 predator.cooldown = predator.cooldown.saturating_sub(1);
143 let Some(target) = predator.target else {
144 continue;
145 };
146 if predator.cooldown > 0 {
147 continue;
148 }
149 let near = |other: &TilePos| {
150 other.0 == tile.0 || is_adjacent(other.0.as_ivec2(), tile.0.as_ivec2())
151 };
152 if let Ok((colonist, pos, mut health, carrying)) = colonists.get_mut(target) {
153 if !near(pos) {
154 continue;
155 }
156 predator.cooldown = BITE_COOLDOWN_TICKS;
157 if health.0 >= 1.0 {
158 log.push(*tick, format!("{} was attacked by a wolf", colonist.name));
159 }
160 health.0 -= COLONIST_BITE;
161 if health.0 <= 0.0 {
162 if let Some(load) = carrying.0 {
163 drop_items(&mut commands, &mut items, &item_assets, pos.0, load);
164 }
165 reservations.release_all(target);
166 commands.entity(target).despawn();
167 losses.0.push(tick.0);
168 log.push(*tick, format!("{} was killed by a wolf", colonist.name));
169 predator.fed_until = tick.0 + FED_TICKS;
170 predator.target = None;
171 }
172 } else if let Ok((pos, mut health)) = prey.get_mut(target) {
173 if !near(pos) {
174 continue;
175 }
176 predator.cooldown = BITE_COOLDOWN_TICKS;
177 health.0 -= ANIMAL_BITE;
178 if health.0 <= 0.0 {
179 reservations.release(Target::Animal(target));
180 commands.entity(target).despawn();
181 predator.fed_until = tick.0 + FED_TICKS;
182 predator.target = None;
183 }
184 } else {
185 predator.target = None;
186 }
187 }
188 }
189
190 pub fn healed(health: f32, ticks: f32, in_bed: bool) -> f32 {
191 let rate = HEAL_PER_DAY / (24.0 * TICKS_PER_HOUR as f32);
192 let rate = if in_bed { rate * BED_HEAL_FACTOR } else { rate };
193 (health + rate * ticks).min(1.0)
194 }
195
196 #[allow(clippy::type_complexity)]
197 fn heal(
198 tick: Res<SimTick>,
199 structures: Query<(&TilePos, &Structure), Without<Health>>,
200 mut patients: Query<(&mut Health, &TilePos, Option<(&CurrentJob, &Path)>)>,
201 ) {
202 if !tick.0.is_multiple_of(HEAL_INTERVAL_TICKS) {
203 return;
204 }
205 let beds: HashSet<UVec2> = structures
206 .iter()
207 .filter(|(_, s)| s.kind == StructureKind::Bed)
208 .map(|(pos, _)| pos.0)
209 .collect();
210 for (mut health, tile, job) in &mut patients {
211 if health.0 >= 1.0 {
212 continue;
213 }
214 let in_bed =
215 job.is_some_and(|(job, path)| is_sleeping(job, tile, path) && beds.contains(&tile.0));
216 health.0 = healed(health.0, HEAL_INTERVAL_TICKS as f32, in_bed);
217 }
218 }
219
220 #[cfg(test)]
221 mod tests {
222 use super::*;
223
224 // Wolves go for the nearest meal but would rather take an animal than a person, unless the
225 // person is much closer.
226 #[test]
227 fn wolves_prefer_animals_to_people() {
228 let e = |i: u32| Entity::from_raw_u32(i).unwrap();
229 let deer = Prey {
230 entity: e(1),
231 tile: UVec2::new(10, 0),
232 colonist: false,
233 };
234 let person = Prey {
235 entity: e(2),
236 tile: UVec2::new(8, 0),
237 colonist: true,
238 };
239 assert_eq!(pick_prey(UVec2::ZERO, &[deer, person]), Some(deer));
240 let close = Prey {
241 tile: UVec2::new(2, 0),
242 ..person
243 };
244 assert_eq!(pick_prey(UVec2::ZERO, &[deer, close]), Some(close));
245 let far = Prey {
246 tile: UVec2::new(PROWL_RANGE as u32 + 1, 0),
247 ..deer
248 };
249 assert_eq!(pick_prey(UVec2::ZERO, &[far]), None);
250 }
251
252 #[test]
253 fn wounds_heal_slowly_and_faster_in_bed() {
254 let day = 24.0 * TICKS_PER_HOUR as f32;
255 assert!((healed(0.5, day, false) - 0.8).abs() < 1e-4);
256 assert_eq!(healed(0.5, day, true), 1.0);
257 }
258
259 // A hungry wolf stalks the nearest prey, bites it to death and then rests while it digests.
260 #[test]
261 fn wolves_hunt_down_prey_and_then_rest() {
262 use crate::{
263 animals::{AnimalIdle, AnimalKind},
264 colonist::{MoveSpeed, follow_path},
265 sim::sim_pos,
266 world::Terrain,
267 };
268 let mut app = App::new();
269 app.insert_resource(WorldMap::filled(UVec2::splat(30), Terrain::Grass))
270 .init_resource::<SimTick>()
271 .init_resource::<Time>()
272 .init_resource::<ItemAssets>()
273 .init_resource::<ColonyLog>()
274 .init_resource::<Losses>()
275 .init_resource::<Reservations>()
276 .add_systems(Update, (prowl, follow_path, bite).chain());
277 let spawn = |app: &mut App, kind: AnimalKind, tile: UVec2| {
278 app.world_mut()
279 .spawn((
280 Animal { kind },
281 TilePos(tile),
282 sim_pos(WorldMap::tile_center(tile)),
283 MoveSpeed(3.0),
284 AnimalIdle::default(),
285 ))
286 .id()
287 };
288 let wolf = spawn(&mut app, AnimalKind::Wolf, UVec2::new(2, 2));
289 app.world_mut().entity_mut(wolf).insert(Predator::default());
290 let rabbit = spawn(&mut app, AnimalKind::Rabbit, UVec2::new(12, 2));
291 for t in 0..2000 {
292 app.world_mut().resource_mut::<SimTick>().0 = t;
293 app.world_mut()
294 .resource_mut::<Time>()
295 .advance_by(std::time::Duration::from_secs_f64(1.0 / 60.0));
296 app.update();
297 }
298 assert!(app.world().get_entity(rabbit).is_err());
299 let predator = app.world().get::<Predator>(wolf).unwrap();
300 assert!(predator.fed_until > 0 && predator.target.is_none());
301 }
302 }