use std::collections::HashSet; use bevy::prelude::*; use crate::{ animals::Animal, colonist::{Colonist, Path}, items::{Carrying, ItemAssets, ItemQuery, drop_items}, jobs::{CurrentJob, Reservations, Target}, mood::Losses, needs::is_sleeping, pathfinding::{find_path_adjacent_within, is_adjacent}, population::ColonyLog, sim::{SimSystems, SimTick, TICKS_PER_HOUR}, structures::{Structure, StructureKind}, world::{TilePos, WorldMap}, }; pub struct ThreatsPlugin; impl Plugin for ThreatsPlugin { fn build(&self, app: &mut App) { app.add_systems( FixedUpdate, ( prowl.in_set(SimSystems::Decide), (bite, heal).chain().in_set(SimSystems::Act), ), ); } } pub const PROWL_RANGE: i32 = 24; const STALK_SEARCH: usize = 4000; const GIVE_UP_TICKS: u64 = 2 * TICKS_PER_HOUR; pub const COLONIST_DETOUR: i32 = 6; pub const COLONIST_BITE: f32 = 0.25; pub const ANIMAL_BITE: f32 = 0.5; pub const BITE_COOLDOWN_TICKS: u32 = 60; pub const FED_TICKS: u64 = 12 * TICKS_PER_HOUR; pub const INJURED: f32 = 0.5; pub const HURT: f32 = 0.75; const HEAL_PER_DAY: f32 = 0.3; const BED_HEAL_FACTOR: f32 = 3.0; const HEAL_INTERVAL_TICKS: u64 = 60; const PROWL_INTERVAL_TICKS: u64 = 30; #[derive(Component, Debug, Clone, Copy, PartialEq)] pub struct Health(pub f32); impl Default for Health { fn default() -> Self { Self(1.0) } } #[derive(Component, Debug, Clone, Copy, PartialEq)] pub struct Tended { pub until: u64, pub quality: f32, } pub const TENDED_TICKS: u64 = 24 * TICKS_PER_HOUR; pub const TEND_WORK: u32 = 240; const TENDED_HEAL_BONUS: f32 = 2.0; pub fn tend_quality(doctor_speed: f32, herb: bool) -> f32 { (doctor_speed * if herb { 1.0 } else { 0.5 }).min(2.0) } pub fn needs_tending(health: f32, tended: Option<&Tended>, tick: u64) -> bool { health < HURT && tended.is_none_or(|t| tick >= t.until) } #[derive(Component, Debug, Default, Clone, Copy, PartialEq)] pub struct Predator { pub target: Option, pub fed_until: u64, pub cooldown: u32, pub resting_until: u64, } #[derive(Clone, Copy, Debug, PartialEq)] pub struct Prey { pub entity: Entity, pub tile: UVec2, pub colonist: bool, } pub fn pick_prey(from: UVec2, prey: &[Prey]) -> Option { prey.iter() .filter_map(|p| { let distance = (p.tile.as_ivec2() - from.as_ivec2()).abs().max_element(); let cost = distance + if p.colonist { COLONIST_DETOUR } else { 0 }; (distance <= PROWL_RANGE).then_some((cost, p)) }) .min_by_key(|(cost, p)| (*cost, p.tile.y, p.tile.x)) .map(|(_, p)| *p) } #[allow(clippy::type_complexity)] fn prowl( tick: Res, map: Res, mut wolves: Query<(&TilePos, &mut Path, &mut Predator), With>, animals: Query<(Entity, &TilePos), (With, Without)>, colonists: Query<(Entity, &TilePos), With>, ) { if !tick.0.is_multiple_of(PROWL_INTERVAL_TICKS) { return; } let prey: Vec = animals .iter() .map(|(entity, tile)| Prey { entity, tile: tile.0, colonist: false, }) .chain(colonists.iter().map(|(entity, tile)| Prey { entity, tile: tile.0, colonist: true, })) .collect(); for (tile, mut path, mut predator) in &mut wolves { if tick.0 < predator.fed_until || tick.0 < predator.resting_until { predator.target = None; continue; } let Some(target) = pick_prey(tile.0, &prey) else { predator.target = None; continue; }; predator.target = Some(target.entity); let end = path.0.back().copied().unwrap_or(tile.0); if is_adjacent(end.as_ivec2(), target.tile.as_ivec2()) || end == target.tile { continue; } match find_path_adjacent_within(&map, tile.0, target.tile, STALK_SEARCH) { Some(route) => { let entering = path.0.front().copied(); path.0 = entering.into_iter().chain(route).collect(); } None => { predator.target = None; predator.resting_until = tick.0 + GIVE_UP_TICKS; } } } } #[allow(clippy::too_many_arguments, clippy::type_complexity)] fn bite( mut commands: Commands, tick: Res, item_assets: Res, mut log: ResMut, mut losses: ResMut, mut reservations: ResMut, mut items: ItemQuery, mut wolves: Query<(&TilePos, &mut Predator), With>, mut colonists: Query< (&Colonist, &TilePos, &mut Health, &Carrying), (Without, Without), >, mut prey: Query<(&TilePos, &mut Health), (With, Without, Without)>, ) { for (tile, mut predator) in &mut wolves { predator.cooldown = predator.cooldown.saturating_sub(1); let Some(target) = predator.target else { continue; }; if predator.cooldown > 0 { continue; } let near = |other: &TilePos| { other.0 == tile.0 || is_adjacent(other.0.as_ivec2(), tile.0.as_ivec2()) }; if let Ok((colonist, pos, mut health, carrying)) = colonists.get_mut(target) { if !near(pos) { continue; } predator.cooldown = BITE_COOLDOWN_TICKS; if health.0 >= 1.0 { log.push(*tick, format!("{} was attacked by a wolf", colonist.name)); } health.0 -= COLONIST_BITE; if health.0 <= 0.0 { if let Some(load) = carrying.0 { drop_items(&mut commands, &mut items, &item_assets, pos.0, load); } reservations.release_all(target); commands.entity(target).despawn(); losses.0.push(tick.0); log.push(*tick, format!("{} was killed by a wolf", colonist.name)); predator.fed_until = tick.0 + FED_TICKS; predator.target = None; } } else if let Ok((pos, mut health)) = prey.get_mut(target) { if !near(pos) { continue; } predator.cooldown = BITE_COOLDOWN_TICKS; health.0 -= ANIMAL_BITE; if health.0 <= 0.0 { reservations.release(Target::Animal(target)); commands.entity(target).despawn(); predator.fed_until = tick.0 + FED_TICKS; predator.target = None; } } else { predator.target = None; } } } pub fn healed(health: f32, ticks: f32, in_bed: bool, tended: f32) -> f32 { let rate = HEAL_PER_DAY / (24.0 * TICKS_PER_HOUR as f32); let rate = if in_bed { rate * BED_HEAL_FACTOR } else { rate }; let rate = rate * (1.0 + TENDED_HEAL_BONUS * tended); (health + rate * ticks).min(1.0) } #[allow(clippy::type_complexity)] fn heal( tick: Res, structures: Query<(&TilePos, &Structure), Without>, mut patients: Query<( &mut Health, &TilePos, Option<(&CurrentJob, &Path)>, Option<&Tended>, )>, ) { if !tick.0.is_multiple_of(HEAL_INTERVAL_TICKS) { return; } let beds: HashSet = structures .iter() .filter(|(_, s)| s.kind == StructureKind::Bed) .map(|(pos, _)| pos.0) .collect(); for (mut health, tile, job, tended) in &mut patients { if health.0 >= 1.0 { continue; } let in_bed = job.is_some_and(|(job, path)| is_sleeping(job, tile, path) && beds.contains(&tile.0)); let tended = tended .filter(|t| tick.0 < t.until) .map_or(0.0, |t| t.quality); health.0 = healed(health.0, HEAL_INTERVAL_TICKS as f32, in_bed, tended); } } #[cfg(test)] mod tests { use super::*; // Wolves go for the nearest meal but would rather take an animal than a person, unless the // person is much closer. #[test] fn wolves_prefer_animals_to_people() { let e = |i: u32| Entity::from_raw_u32(i).unwrap(); let deer = Prey { entity: e(1), tile: UVec2::new(10, 0), colonist: false, }; let person = Prey { entity: e(2), tile: UVec2::new(8, 0), colonist: true, }; assert_eq!(pick_prey(UVec2::ZERO, &[deer, person]), Some(deer)); let close = Prey { tile: UVec2::new(2, 0), ..person }; assert_eq!(pick_prey(UVec2::ZERO, &[deer, close]), Some(close)); let far = Prey { tile: UVec2::new(PROWL_RANGE as u32 + 1, 0), ..deer }; assert_eq!(pick_prey(UVec2::ZERO, &[far]), None); } #[test] fn wounds_heal_slowly_and_faster_in_bed() { let day = 24.0 * TICKS_PER_HOUR as f32; assert!((healed(0.5, day, false, 0.0) - 0.8).abs() < 1e-4); assert_eq!(healed(0.5, day, true, 0.0), 1.0); assert!(healed(0.2, day, false, 1.0) > healed(0.2, day, false, 0.5)); assert!(tend_quality(1.0, true) > tend_quality(1.0, false)); } // A hungry wolf stalks the nearest prey, bites it to death and then rests while it digests. #[test] fn wolves_hunt_down_prey_and_then_rest() { use crate::{ animals::{AnimalIdle, AnimalKind}, colonist::{MoveSpeed, follow_path}, sim::sim_pos, world::Terrain, }; let mut app = App::new(); app.insert_resource(WorldMap::filled(UVec2::splat(30), Terrain::Grass)) .init_resource::() .init_resource::