use std::{ collections::{HashMap, HashSet}, ops::Range, time::Duration, }; use bevy::prelude::*; use crate::{ colonist::{Colonist, Path, repath}, items::{ CARRY_CAPACITY, Carrying, Item, ItemAssets, ItemKind, ItemQuery, drop_items, room_for, stock, take_items, }, pathfinding::{find_path, find_path_adjacent, is_adjacent, nearest_tile}, rng::SimRng, sim::SimSystems, world::{Plant, Terrain, TileChanged, TilePos, WorldMap}, worldgen::WorldGenerated, zones::{Zones, plan_expansion}, }; pub struct JobsPlugin; impl Plugin for JobsPlugin { fn build(&self, app: &mut App) { app.insert_resource(SimRng::new(0x5EED)) .init_resource::() .init_resource::() .add_message::() .add_message::() .add_systems( Update, ( clear_reservations.run_if(on_message::), handle_orders, ), ) .add_systems(FixedUpdate, choose_jobs.in_set(SimSystems::Decide)) .add_systems(FixedUpdate, run_jobs.in_set(SimSystems::Act)); } } pub const EXPAND_TICKS: u32 = 120; pub const EXPANSION_TILES: usize = 8; pub const MIN_EMPTY_STOCKPILE_TILES: usize = 3; const SEARCH_TILES: usize = 15000; const WANDER_RADIUS: i32 = 8; const IDLE_SECS: Range = 1.0..4.0; const IDLE_AFTER_ORDER_SECS: f32 = 10.0; const WORK_PAUSE_SECS: Range = 0.2..0.8; const NEIGHBORS: [IVec2; 8] = [ IVec2::new(1, 0), IVec2::new(-1, 0), IVec2::new(0, 1), IVec2::new(0, -1), IVec2::new(1, 1), IVec2::new(1, -1), IVec2::new(-1, 1), IVec2::new(-1, -1), ]; #[derive(Component, Default, Debug, Clone, PartialEq)] pub struct CurrentJob(pub Option); #[derive(Debug, Clone, Copy, PartialEq)] pub struct Job { pub task: Task, pub ordered: bool, } #[derive(Debug, Clone, Copy, PartialEq)] pub enum Task { MoveTo(UVec2), Work { site: UVec2, kind: WorkKind, progress: u32, }, Haul { pickup: Option, to: UVec2, }, ExpandStockpile { site: UVec2, progress: u32, }, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WorkKind { Chop, Mine, } impl WorkKind { pub fn ticks(self) -> u32 { match self { WorkKind::Chop => 240, WorkKind::Mine => 360, } } fn complete(self, map: &mut WorldMap, site: UVec2) { match self { WorkKind::Chop => map.set_plant(site, None), WorkKind::Mine => map.set(site, Terrain::Gravel), } } } pub struct SiteWork { pub kind: WorkKind, pub item: ItemKind, pub count: u32, } pub fn site_work(map: &WorldMap, site: UVec2) -> Option { let p = site.as_ivec2(); if map.plant(p) == Some(Plant::Tree) { return Some(SiteWork { kind: WorkKind::Chop, item: ItemKind::Wood, count: 10, }); } let (item, count) = match map.get(p)? { Terrain::Stone => (ItemKind::Stone, 10), Terrain::IronOre => (ItemKind::Iron, 10), Terrain::GoldOre => (ItemKind::Gold, 5), _ => return None, }; Some(SiteWork { kind: WorkKind::Mine, item, count, }) } #[derive(Component, Debug)] pub struct IdleTimer(Timer); impl Default for IdleTimer { fn default() -> Self { Self(Timer::from_seconds(IDLE_SECS.start, TimerMode::Once)) } } impl IdleTimer { fn restart(&mut self, secs: f32) { self.0.set_duration(Duration::from_secs_f32(secs)); self.0.reset(); } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Target { Site(UVec2), Pickup(UVec2), Storage(UVec2), StockpileExpansion, } #[derive(Resource, Default, Debug)] pub struct Reservations(HashMap); impl Reservations { pub fn reserve(&mut self, target: Target, by: Entity) { self.0.insert(target, by); } pub fn holder(&self, target: Target) -> Option { self.0.get(&target).copied() } pub fn is_free(&self, target: Target) -> bool { self.holder(target).is_none() } pub fn release(&mut self, target: Target) { self.0.remove(&target); } pub fn release_all(&mut self, by: Entity) { self.0.retain(|_, holder| *holder != by); } } #[derive(Resource, Debug, Clone)] pub struct ColonyGoals(pub HashMap); impl Default for ColonyGoals { fn default() -> Self { Self(HashMap::from([ (ItemKind::Wood, 50), (ItemKind::Stone, 40), (ItemKind::Iron, 20), (ItemKind::Gold, 10), ])) } } impl ColonyGoals { pub fn goal(&self, kind: ItemKind) -> u32 { self.0.get(&kind).copied().unwrap_or(0) } pub fn wanted(&self, kind: ItemKind, in_stock: u32, on_the_way: u32) -> bool { in_stock + on_the_way < self.goal(kind) } } #[derive(Message, Debug, Clone, Copy)] pub struct Order { pub colonist: Entity, pub tile: UVec2, } fn clear_reservations(mut reservations: ResMut) { reservations.0.clear(); } fn handle_orders( mut orders: MessageReader, map: Res, zones: Res, items: Query<(&Item, &TilePos)>, mut reservations: ResMut, mut colonists: Query<(&TilePos, &mut Path, &mut CurrentJob)>, ) { for order in orders.read() { let Ok((tile, mut path, mut job)) = colonists.get_mut(order.colonist) else { continue; }; let loose_item = items .iter() .find(|(_, pos)| pos.0 == order.tile && !zones.is_stockpile(pos.0)); let task = if let Some(work) = site_work(&map, order.tile) { Some(Task::Work { site: order.tile, kind: work.kind, progress: 0, }) } else if let Some((item, _)) = loose_item { find_storage(&map, &zones, &reservations, &items, order.tile, item.kind).map(|to| { Task::Haul { pickup: Some(order.tile), to, } }) } else { None }; if let Some(task) = task { reservations.release_all(order.colonist); reserve_task(&mut reservations, task, order.colonist); path.0.truncate(1); job.0 = Some(Job { task, ordered: true, }); } else if let Some(new_path) = repath(&map, tile.0, &path.0, order.tile) { reservations.release_all(order.colonist); path.0 = new_path; job.0 = Some(Job { task: Task::MoveTo(order.tile), ordered: true, }); } } } fn reserve_task(reservations: &mut Reservations, task: Task, by: Entity) { match task { Task::MoveTo(_) => {} Task::Work { site, .. } => reservations.reserve(Target::Site(site), by), Task::Haul { pickup, to } => { if let Some(pickup) = pickup { reservations.reserve(Target::Pickup(pickup), by); } reservations.reserve(Target::Storage(to), by); } Task::ExpandStockpile { .. } => reservations.reserve(Target::StockpileExpansion, by), } } pub fn find_work( map: &WorldMap, reservations: &Reservations, from: UVec2, wanted: impl Fn(ItemKind) -> bool, ) -> Option { let mut found = None; nearest_tile(map, from, SEARCH_TILES, |stand| { found = NEIGHBORS.iter().find_map(|&d| { let site = stand.as_ivec2() + d; if site.x < 0 || site.y < 0 { return None; } let site = site.as_uvec2(); let work = site_work(map, site)?; (wanted(work.item) && reservations.is_free(Target::Site(site))).then_some(Task::Work { site, kind: work.kind, progress: 0, }) }); found.is_some() })?; found } pub fn find_storage<'a>( map: &WorldMap, zones: &Zones, reservations: &Reservations, items: impl IntoIterator + Clone, from: UVec2, kind: ItemKind, ) -> Option { nearest_tile(map, from, SEARCH_TILES, |p| { zones.is_stockpile(p) && reservations.is_free(Target::Storage(p)) && room_for(items.clone().into_iter(), p, kind) > 0 }) } pub fn find_haul( map: &WorldMap, zones: &Zones, reservations: &Reservations, items: &[(Item, TilePos)], from: UVec2, ) -> Option { let loose: HashSet = items .iter() .map(|(_, pos)| pos.0) .filter(|&p| !zones.is_stockpile(p) && reservations.is_free(Target::Pickup(p))) .collect(); if loose.is_empty() { return None; } let pickup = nearest_tile(map, from, SEARCH_TILES, |p| loose.contains(&p))?; let (item, _) = items.iter().find(|(_, pos)| pos.0 == pickup)?; let refs = items.iter().map(|(i, p)| (i, p)); let to = find_storage(map, zones, reservations, refs, pickup, item.kind)?; Some(Task::Haul { pickup: Some(pickup), to, }) } pub fn stockpile_too_full(zones: &Zones, items: &[(Item, TilePos)]) -> bool { let occupied: HashSet = items.iter().map(|(_, pos)| pos.0).collect(); zones.has_stockpile() && zones.empty_stockpile_tiles(&occupied) < MIN_EMPTY_STOCKPILE_TILES } pub fn find_expansion( map: &WorldMap, zones: &Zones, reservations: &Reservations, items: &[(Item, TilePos)], ) -> Option { if !reservations.is_free(Target::StockpileExpansion) || !stockpile_too_full(zones, items) { return None; } let site = *plan_expansion(map, zones, EXPANSION_TILES).first()?; Some(Task::ExpandStockpile { site, progress: 0 }) } fn on_the_way(map: &WorldMap, jobs: impl Iterator>) -> HashMap { let mut totals = HashMap::new(); for job in jobs.flatten() { if let Task::Work { site, .. } = job.task && let Some(work) = site_work(map, site) { *totals.entry(work.item).or_insert(0) += work.count; } } totals } #[allow(clippy::too_many_arguments)] fn choose_jobs( mut commands: Commands, time: Res