use bevy::prelude::*; use crate::{ colonist::{Colonist, Path}, items::{Item, ItemKind}, jobs::{CurrentJob, Reservations, Target}, needs::is_sleeping, pathfinding::nearest_tile, sim::{SimSystems, TICKS_PER_HOUR}, world::{TilePos, WorldMap}, }; pub struct FoodPlugin; impl Plugin for FoodPlugin { fn build(&self, app: &mut App) { app.add_systems(FixedUpdate, update_hunger.in_set(SimSystems::Act)); } } pub const HUNGER_PER_TICK: f32 = 1.0 / (24.0 * TICKS_PER_HOUR as f32); pub const SLEEPING_HUNGER_FACTOR: f32 = 0.5; pub const HUNGRY: f32 = 0.35; pub const STARVING: f32 = 0.05; pub const EAT_TICKS: u32 = 90; const FOOD_SEARCH_TILES: usize = 15000; #[derive(Component, Debug, Clone, Copy, PartialEq)] pub struct Hunger(pub f32); impl Default for Hunger { fn default() -> Self { Self(1.0) } } pub fn wants_food(fed: f32) -> bool { fed < HUNGRY } pub fn portion(kind: ItemKind, fed: f32) -> u32 { ((1.0 - fed) / kind.nutrition()).ceil().max(1.0) as u32 } pub fn find_food( map: &WorldMap, reservations: &Reservations, items: &[(Item, TilePos)], from: UVec2, ) -> Option { let has = |p: UVec2, accept: fn(ItemKind) -> bool| { reservations.is_free(Target::Pickup(p)) && items .iter() .any(|(item, pos)| pos.0 == p && accept(item.kind)) }; nearest_tile(map, from, FOOD_SEARCH_TILES, |p| { has(p, |k| k == ItemKind::Meal) }) .or_else(|| { nearest_tile(map, from, FOOD_SEARCH_TILES, |p| { has(p, ItemKind::is_raw_food) }) }) } pub fn update_hunger( mut colonists: Query<(&mut Hunger, &CurrentJob, &TilePos, &Path), With>, ) { for (mut hunger, job, tile, path) in &mut colonists { let rate = if is_sleeping(job, tile, path) { HUNGER_PER_TICK * SLEEPING_HUNGER_FACTOR } else { HUNGER_PER_TICK }; hunger.0 = (hunger.0 - rate).max(0.0); } } #[cfg(test)] mod tests { use super::*; use crate::world::Terrain; #[test] fn portions_fill_a_colonist_up() { assert_eq!(portion(ItemKind::Meal, 0.2), 1); assert_eq!(portion(ItemKind::Berries, 0.3), 7); assert_eq!(portion(ItemKind::Berries, 0.99), 1); } #[test] fn colonists_prefer_meals_over_raw_food() { let map = WorldMap::filled(UVec2::splat(20), Terrain::Grass); let reservations = Reservations::default(); let food = |kind, x| (Item { kind, count: 5 }, TilePos(UVec2::new(x, 0))); let items = [ food(ItemKind::Berries, 1), food(ItemKind::Meal, 15), food(ItemKind::Wood, 0), ]; assert_eq!( find_food(&map, &reservations, &items, UVec2::ZERO), Some(UVec2::new(15, 0)) ); assert_eq!( find_food(&map, &reservations, &items[..1], UVec2::ZERO), Some(UVec2::new(1, 0)) ); assert_eq!( find_food(&map, &reservations, &items[2..], UVec2::ZERO), None ); } }