src/jobs/mod.rs 70.5 K · 2058 lines · raw · history

1 use serde::{Deserialize, Serialize};
2 use std::{
3 collections::{HashMap, HashSet},
4 ops::Range,
5 time::Duration,
6 };
7
8 use bevy::prelude::*;
9
10 use crate::{
11 animals::{Animal, AnimalKind, MAX_TAME, Tame},
12 building::{Blueprints, BuildKind},
13 climate::{COLD_TEMP, WARM_TEMP, Warmth, outdoor_temperature, sowing_season, tile_temperature},
14 colonist::{Colonist, Path, repath},
15 designations::Designations,
16 economy::{ColonyGoals, in_colony},
17 family::{Child, Family, Homes},
18 food::{EAT_TICKS, Hunger, STARVING, find_food, portion, wants_food},
19 items::{
20 CARRY_CAPACITY, Carrying, Item, ItemAssets, ItemKind, ItemQuery, STACK_LIMIT, drop_items,
21 room_for, stock, take_items,
22 },
23 light::{
24 LIGHT_LOOKAHEAD_TICKS, LIT_BRIGHTNESS, LightMap, brightness, dark_soon, light_from,
25 line_of_sight,
26 },
27 mood::{Ate, Sick},
28 needs::{EXHAUSTED_REST, Rest, SleepContext, done_sleeping, wants_sleep},
29 pathfinding::{find_path, find_path_adjacent, is_adjacent, nearest_of, nearest_tile},
30 rng::SimRng,
31 sim::{SimSystems, SimTick, hour_of_day},
32 structures::{Fuel, Structure, StructureAssets, StructureKind, spawn_structure},
33 world::{Plant, Terrain, TileChanged, TilePos, WorldMap},
34 worldgen::WorldGenerated,
35 zones::{
36 StoragePriority, Zones, can_zone, find_stockpile_site, new_stockpile_tiles, plan_expansion,
37 },
38 };
39
40 pub struct JobsPlugin;
41
42 impl Plugin for JobsPlugin {
43 fn build(&self, app: &mut App) {
44 app.insert_resource(SimRng::new(0x5EED))
45 .init_resource::<Reservations>()
46 .add_message::<Order>()
47 .add_message::<TileChanged>()
48 .add_systems(
49 Update,
50 (
51 clear_reservations.run_if(on_message::<WorldGenerated>),
52 handle_orders,
53 ),
54 )
55 .add_systems(FixedUpdate, choose_jobs.in_set(SimSystems::Decide))
56 .add_systems(FixedUpdate, run_jobs.in_set(SimSystems::Act));
57 }
58 }
59
60 pub const EXPAND_WORK: u32 = 240;
61 pub const REFUEL_BELOW_SHARE: u32 = 3;
62 pub const CAMPFIRE_MIN_FUEL: u32 = 3 * crate::sim::TICKS_PER_HOUR as u32;
63 pub const HOME_RADIUS: f32 = 10.0;
64 pub const BED_STOCKPILE_CLEARANCE: i32 = 3;
65 const LIT_WORK_RATE: u32 = 2;
66 const DARK_WORK_RATE: u32 = 1;
67 pub const EXPANSION_TILES: usize = 8;
68 pub const MIN_EMPTY_STOCKPILE_TILES: usize = 3;
69 const SEARCH_TILES: usize = 15000;
70 const FORAGE_SEARCH_TILES: usize = 5000;
71 const WANDER_RADIUS: i32 = 8;
72 const IDLE_SECS: Range<f32> = 1.0..4.0;
73 const IDLE_AFTER_ORDER_SECS: f32 = 10.0;
74 const WORK_PAUSE_SECS: Range<f32> = 0.2..0.8;
75 const NEIGHBORS: [IVec2; 8] = [
76 IVec2::new(1, 0),
77 IVec2::new(-1, 0),
78 IVec2::new(0, 1),
79 IVec2::new(0, -1),
80 IVec2::new(1, 1),
81 IVec2::new(1, -1),
82 IVec2::new(-1, 1),
83 IVec2::new(-1, -1),
84 ];
85
86 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
87 pub enum WorkType {
88 Food,
89 Build,
90 Gather,
91 Haul,
92 Tame,
93 Craft,
94 }
95
96 impl WorkType {
97 pub const ALL: [WorkType; 6] = [
98 WorkType::Food,
99 WorkType::Build,
100 WorkType::Gather,
101 WorkType::Haul,
102 WorkType::Tame,
103 WorkType::Craft,
104 ];
105
106 pub fn label(self) -> &'static str {
107 match self {
108 WorkType::Food => "Food",
109 WorkType::Build => "Build",
110 WorkType::Gather => "Gather",
111 WorkType::Haul => "Haul",
112 WorkType::Tame => "Tame",
113 WorkType::Craft => "Craft",
114 }
115 }
116 }
117
118 #[derive(Component, Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119 pub struct WorkPriorities {
120 disabled: Vec<WorkType>,
121 }
122
123 impl WorkPriorities {
124 pub fn allows(&self, work: WorkType) -> bool {
125 !self.disabled.contains(&work)
126 }
127
128 pub fn toggle(&mut self, work: WorkType) {
129 if self.allows(work) {
130 self.disabled.push(work);
131 } else {
132 self.disabled.retain(|w| *w != work);
133 }
134 }
135 }
136
137 #[derive(Component, Default, Debug, Clone, PartialEq)]
138 pub struct CurrentJob(pub Option<Job>);
139
140 #[derive(Debug, Clone, Copy, PartialEq)]
141 pub struct Job {
142 pub task: Task,
143 pub ordered: bool,
144 }
145
146 #[derive(Debug, Clone, Copy, PartialEq)]
147 pub enum Task {
148 MoveTo(UVec2),
149 Work {
150 site: UVec2,
151 kind: WorkKind,
152 progress: u32,
153 },
154 Haul {
155 pickup: Option<UVec2>,
156 to: UVec2,
157 },
158 ExpandStockpile {
159 site: UVec2,
160 progress: u32,
161 },
162 Build {
163 site: UVec2,
164 kind: BuildKind,
165 pickup: Option<UVec2>,
166 progress: u32,
167 },
168 Sleep {
169 spot: UVec2,
170 },
171 Eat {
172 pickup: Option<UVec2>,
173 progress: u32,
174 },
175 Hunt {
176 animal: Entity,
177 kind: AnimalKind,
178 progress: u32,
179 },
180 Craft {
181 recipe: Recipe,
182 pickup: Option<UVec2>,
183 station: UVec2,
184 progress: u32,
185 },
186 Equip {
187 pickup: UVec2,
188 },
189 Butcher {
190 site: UVec2,
191 progress: u32,
192 },
193 Tame {
194 animal: Entity,
195 kind: AnimalKind,
196 progress: u32,
197 },
198 }
199
200 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
201 pub enum Recipe {
202 Meal,
203 Tools,
204 }
205
206 impl Recipe {
207 pub fn station(self) -> StructureKind {
208 match self {
209 Recipe::Meal => StructureKind::Campfire,
210 Recipe::Tools => StructureKind::Workbench,
211 }
212 }
213
214 pub fn accepts(self, kind: ItemKind) -> bool {
215 match self {
216 Recipe::Meal => kind.is_raw_food(),
217 Recipe::Tools => kind == ItemKind::Iron,
218 }
219 }
220
221 pub fn input(self) -> u32 {
222 match self {
223 Recipe::Meal => COOK_INPUT,
224 Recipe::Tools => TOOL_IRON,
225 }
226 }
227
228 pub fn work(self) -> u32 {
229 match self {
230 Recipe::Meal => COOK_WORK,
231 Recipe::Tools => TOOL_WORK,
232 }
233 }
234
235 pub fn output(self) -> ItemKind {
236 match self {
237 Recipe::Meal => ItemKind::Meal,
238 Recipe::Tools => ItemKind::Tools,
239 }
240 }
241 }
242
243 pub const TOOL_IRON: u32 = 3;
244 pub const TOOL_WORK: u32 = 600;
245 pub const TOOL_LIFE_TICKS: u32 = 2 * 24 * crate::sim::TICKS_PER_HOUR as u32;
246
247 #[derive(Component, Debug, Default, Clone, Copy, PartialEq, Eq)]
248 pub struct Tool {
249 pub wear: u32,
250 }
251
252 pub fn tool_rate(rate: u32, tool: bool) -> u32 {
253 if tool { (rate * 3).div_ceil(2) } else { rate }
254 }
255
256 pub fn work_rate(light: &LightMap, tile: UVec2) -> u32 {
257 if light.is_lit(tile) {
258 LIT_WORK_RATE
259 } else {
260 DARK_WORK_RATE
261 }
262 }
263
264 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
265 pub enum WorkKind {
266 Chop,
267 Mine,
268 Harvest,
269 Sow,
270 }
271
272 impl WorkKind {
273 pub fn work(self) -> u32 {
274 match self {
275 WorkKind::Chop => 480,
276 WorkKind::Mine => 720,
277 WorkKind::Harvest => 240,
278 WorkKind::Sow => 160,
279 }
280 }
281
282 fn complete(self, map: &mut WorldMap, site: UVec2) {
283 match self {
284 WorkKind::Chop => map.set_plant(site, None),
285 WorkKind::Mine => map.set(site, Terrain::Gravel),
286 WorkKind::Harvest => {
287 if map.plant(site.as_ivec2()) == Some(Plant::Crop) {
288 map.set_plant(site, None);
289 }
290 map.set_growth(site, 0.0);
291 }
292 WorkKind::Sow => {
293 map.set_plant(site, Some(Plant::Crop));
294 map.set_growth(site, 0.0);
295 }
296 }
297 }
298 }
299
300 pub struct SiteWork {
301 pub kind: WorkKind,
302 pub item: ItemKind,
303 pub count: u32,
304 }
305
306 pub fn site_work(map: &WorldMap, site: UVec2) -> Option<SiteWork> {
307 let p = site.as_ivec2();
308 let harvest = |item, count| SiteWork {
309 kind: WorkKind::Harvest,
310 item,
311 count,
312 };
313 match map.plant(p) {
314 Some(Plant::Tree) => {
315 return Some(SiteWork {
316 kind: WorkKind::Chop,
317 item: ItemKind::Wood,
318 count: 10,
319 });
320 }
321 Some(Plant::BerryBush) if map.ripe_plant(p).is_some() => {
322 return Some(harvest(ItemKind::Berries, 8));
323 }
324 Some(Plant::Crop) if map.ripe_plant(p).is_some() => {
325 return Some(harvest(ItemKind::Vegetables, 10));
326 }
327 _ => {}
328 }
329 let (item, count) = match map.get(p)? {
330 Terrain::Stone => (ItemKind::Stone, 10),
331 Terrain::IronOre => (ItemKind::Iron, 10),
332 Terrain::GoldOre => (ItemKind::Gold, 5),
333 _ => return None,
334 };
335 Some(SiteWork {
336 kind: WorkKind::Mine,
337 item,
338 count,
339 })
340 }
341
342 #[derive(Component, Debug)]
343 pub struct IdleTimer(Timer);
344
345 impl Default for IdleTimer {
346 fn default() -> Self {
347 Self(Timer::from_seconds(IDLE_SECS.start, TimerMode::Once))
348 }
349 }
350
351 impl IdleTimer {
352 fn restart(&mut self, secs: f32) {
353 self.0.set_duration(Duration::from_secs_f32(secs));
354 self.0.reset();
355 }
356 }
357
358 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
359 pub enum Target {
360 Site(UVec2),
361 Pickup(UVec2),
362 Storage(UVec2),
363 StockpileExpansion,
364 SleepSpot(UVec2),
365 Animal(Entity),
366 Station(UVec2),
367 }
368
369 #[derive(Resource, Default, Debug)]
370 pub struct Reservations(HashMap<Target, Entity>);
371
372 impl Reservations {
373 pub fn reserve(&mut self, target: Target, by: Entity) {
374 self.0.insert(target, by);
375 }
376
377 pub fn holder(&self, target: Target) -> Option<Entity> {
378 self.0.get(&target).copied()
379 }
380
381 pub fn is_free(&self, target: Target) -> bool {
382 self.holder(target).is_none()
383 }
384
385 pub fn release(&mut self, target: Target) {
386 self.0.remove(&target);
387 }
388
389 pub fn release_all(&mut self, by: Entity) {
390 self.0.retain(|_, holder| *holder != by);
391 }
392 }
393
394 #[derive(Message, Debug, Clone, Copy)]
395 pub struct Order {
396 pub colonist: Entity,
397 pub tile: UVec2,
398 }
399
400 fn clear_reservations(mut reservations: ResMut<Reservations>) {
401 reservations.0.clear();
402 }
403
404 fn handle_orders(
405 mut orders: MessageReader<Order>,
406 map: Res<WorldMap>,
407 zones: Res<Zones>,
408 items: Query<(&Item, &TilePos)>,
409 animals: Query<(Entity, &TilePos, &Animal), Without<Colonist>>,
410 mut reservations: ResMut<Reservations>,
411 mut colonists: Query<(&TilePos, &mut Path, &mut CurrentJob), With<Colonist>>,
412 ) {
413 for order in orders.read() {
414 let Ok((tile, mut path, mut job)) = colonists.get_mut(order.colonist) else {
415 continue;
416 };
417 let loose_item = items
418 .iter()
419 .find(|(_, pos)| pos.0 == order.tile && !zones.is_stockpile(pos.0));
420 let animal = animals.iter().find(|(_, pos, _)| pos.0 == order.tile);
421 let task = if let Some((animal, _, a)) = animal {
422 Some(Task::Hunt {
423 animal,
424 kind: a.kind,
425 progress: 0,
426 })
427 } else if let Some(work) = site_work(&map, order.tile) {
428 Some(Task::Work {
429 site: order.tile,
430 kind: work.kind,
431 progress: 0,
432 })
433 } else if let Some((item, _)) = loose_item {
434 find_storage(&map, &zones, &reservations, &items, order.tile, item.kind).map(|to| {
435 Task::Haul {
436 pickup: Some(order.tile),
437 to,
438 }
439 })
440 } else {
441 None
442 };
443 if let Some(task) = task {
444 reservations.release_all(order.colonist);
445 reserve_task(&mut reservations, task, order.colonist);
446 path.0.truncate(1);
447 job.0 = Some(Job {
448 task,
449 ordered: true,
450 });
451 } else if let Some(new_path) = repath(&map, tile.0, &path.0, order.tile) {
452 reservations.release_all(order.colonist);
453 path.0 = new_path;
454 job.0 = Some(Job {
455 task: Task::MoveTo(order.tile),
456 ordered: true,
457 });
458 }
459 }
460 }
461
462 fn reserve_task(reservations: &mut Reservations, task: Task, by: Entity) {
463 match task {
464 Task::MoveTo(_) => {}
465 Task::Work { site, .. } => reservations.reserve(Target::Site(site), by),
466 Task::Haul { pickup, to } => {
467 if let Some(pickup) = pickup {
468 reservations.reserve(Target::Pickup(pickup), by);
469 }
470 reservations.reserve(Target::Storage(to), by);
471 }
472 Task::ExpandStockpile { .. } => reservations.reserve(Target::StockpileExpansion, by),
473 Task::Sleep { spot } => reservations.reserve(Target::SleepSpot(spot), by),
474 Task::Eat { pickup, .. } => {
475 if let Some(pickup) = pickup {
476 reservations.reserve(Target::Pickup(pickup), by);
477 }
478 }
479 Task::Hunt { animal, .. } | Task::Tame { animal, .. } => {
480 reservations.reserve(Target::Animal(animal), by)
481 }
482 Task::Butcher { site, .. } => reservations.reserve(Target::Pickup(site), by),
483 Task::Equip { pickup } => reservations.reserve(Target::Pickup(pickup), by),
484 Task::Craft {
485 pickup, station, ..
486 } => {
487 reservations.reserve(Target::Station(station), by);
488 if let Some(pickup) = pickup {
489 reservations.reserve(Target::Pickup(pickup), by);
490 }
491 }
492 Task::Build { site, pickup, .. } => {
493 reservations.reserve(Target::Site(site), by);
494 if let Some(pickup) = pickup {
495 reservations.reserve(Target::Pickup(pickup), by);
496 }
497 }
498 }
499 }
500
501 pub fn find_work(
502 map: &WorldMap,
503 reservations: &Reservations,
504 from: UVec2,
505 wanted: impl Fn(UVec2, ItemKind) -> bool,
506 ) -> Option<Task> {
507 find_work_within(map, reservations, from, SEARCH_TILES, wanted)
508 }
509
510 pub fn find_work_within(
511 map: &WorldMap,
512 reservations: &Reservations,
513 from: UVec2,
514 max_tiles: usize,
515 wanted: impl Fn(UVec2, ItemKind) -> bool,
516 ) -> Option<Task> {
517 let mut found = None;
518 nearest_tile(map, from, max_tiles, |stand| {
519 found = NEIGHBORS.iter().find_map(|&d| {
520 let site = stand.as_ivec2() + d;
521 if site.x < 0 || site.y < 0 {
522 return None;
523 }
524 let site = site.as_uvec2();
525 let work = site_work(map, site)?;
526 (wanted(site, work.item) && reservations.is_free(Target::Site(site))).then_some(
527 Task::Work {
528 site,
529 kind: work.kind,
530 progress: 0,
531 },
532 )
533 });
534 found.is_some()
535 })?;
536 found
537 }
538
539 fn stacks_by_tile<'a>(
540 items: impl IntoIterator<Item = (&'a Item, &'a TilePos)>,
541 ) -> HashMap<UVec2, (ItemKind, u32)> {
542 let mut stacks: HashMap<UVec2, (ItemKind, u32)> = HashMap::new();
543 for (item, pos) in items {
544 stacks.entry(pos.0).or_insert((item.kind, 0)).1 += item.count;
545 }
546 stacks
547 }
548
549 fn storage_candidates<'z>(
550 zones: &'z Zones,
551 reservations: &'z Reservations,
552 stacks: &'z HashMap<UVec2, (ItemKind, u32)>,
553 kind: ItemKind,
554 ) -> impl Iterator<Item = UVec2> + 'z {
555 zones.stockpile_tiles().filter(move |&t| {
556 zones.accepts(t, kind)
557 && reservations.is_free(Target::Storage(t))
558 && match stacks.get(&t) {
559 None => true,
560 Some(&(other, count)) => other == kind && count < STACK_LIMIT,
561 }
562 })
563 }
564
565 pub fn find_storage<'a>(
566 map: &WorldMap,
567 zones: &Zones,
568 reservations: &Reservations,
569 items: impl IntoIterator<Item = (&'a Item, &'a TilePos)> + Clone,
570 from: UVec2,
571 kind: ItemKind,
572 ) -> Option<UVec2> {
573 let stacks = stacks_by_tile(items);
574 StoragePriority::DESCENDING
575 .into_iter()
576 .find_map(|priority| {
577 let candidates: HashSet<UVec2> = storage_candidates(zones, reservations, &stacks, kind)
578 .filter(|&t| zones.rule(t).priority == priority)
579 .collect();
580 nearest_of(map, from, SEARCH_TILES, &candidates)
581 })
582 }
583
584 pub fn find_haul(
585 map: &WorldMap,
586 zones: &Zones,
587 reservations: &Reservations,
588 items: &[(Item, TilePos)],
589 from: UVec2,
590 ) -> Option<Task> {
591 let stacks = stacks_by_tile(items.iter().map(|(i, p)| (i, p)));
592 let mut best: HashMap<ItemKind, StoragePriority> = HashMap::new();
593 let misplaced = |item: &Item, tile: UVec2, best: &mut HashMap<ItemKind, StoragePriority>| {
594 if !zones.accepts(tile, item.kind) {
595 return true;
596 }
597 let top = *best.entry(item.kind).or_insert_with(|| {
598 storage_candidates(zones, reservations, &stacks, item.kind)
599 .map(|t| zones.rule(t).priority)
600 .max()
601 .unwrap_or(StoragePriority::Low)
602 });
603 zones.rule(tile).priority < top
604 };
605 let loose: HashSet<UVec2> = items
606 .iter()
607 .filter(|(item, pos)| {
608 reservations.is_free(Target::Pickup(pos.0)) && misplaced(item, pos.0, &mut best)
609 })
610 .map(|(_, pos)| pos.0)
611 .collect();
612 if loose.is_empty() {
613 return None;
614 }
615 let pickup = nearest_tile(map, from, SEARCH_TILES, |p| loose.contains(&p))?;
616 let (item, _) = items.iter().find(|(_, pos)| pos.0 == pickup)?;
617 let refs = items.iter().map(|(i, p)| (i, p));
618 let to = find_storage(map, zones, reservations, refs, pickup, item.kind)?;
619 Some(Task::Haul {
620 pickup: Some(pickup),
621 to,
622 })
623 }
624
625 pub fn sowable(map: &WorldMap, zones: &Zones, site: UVec2) -> bool {
626 zones.is_farm(site) && map.plant(site.as_ivec2()).is_none()
627 }
628
629 pub fn find_sowing(
630 map: &WorldMap,
631 zones: &Zones,
632 reservations: &Reservations,
633 from: UVec2,
634 ) -> Option<Task> {
635 let open: HashSet<UVec2> = zones
636 .farm_tiles()
637 .filter(|&p| sowable(map, zones, p) && reservations.is_free(Target::Site(p)))
638 .collect();
639 let site = nearest_of(map, from, SEARCH_TILES, &open)?;
640 Some(Task::Work {
641 site,
642 kind: WorkKind::Sow,
643 progress: 0,
644 })
645 }
646
647 pub const HUNT_RADIUS: f32 = 60.0;
648 pub const HUNT_RANGE: f32 = 6.0;
649
650 pub type Wildlife = (Entity, UVec2, AnimalKind, bool);
651
652 fn nearest_reachable(
653 map: &WorldMap,
654 reservations: &Reservations,
655 animals: &[Wildlife],
656 from: UVec2,
657 pick: impl Fn(&Wildlife) -> bool,
658 ) -> Option<Wildlife> {
659 let mut candidates: Vec<&Wildlife> = animals
660 .iter()
661 .filter(|a| {
662 pick(a)
663 && reservations.is_free(Target::Animal(a.0))
664 && a.1.as_vec2().distance(from.as_vec2()) <= HUNT_RADIUS
665 })
666 .collect();
667 candidates.sort_by(|a, b| {
668 a.1.as_vec2()
669 .distance_squared(from.as_vec2())
670 .total_cmp(&b.1.as_vec2().distance_squared(from.as_vec2()))
671 });
672 candidates
673 .into_iter()
674 .find(|a| find_path_adjacent(map, from, a.1).is_some())
675 .copied()
676 }
677
678 pub fn find_hunt(
679 map: &WorldMap,
680 reservations: &Reservations,
681 animals: &[Wildlife],
682 from: UVec2,
683 ) -> Option<Task> {
684 let herd = |kind: AnimalKind| animals.iter().filter(|a| a.3 && a.2 == kind).count();
685 let spare_tame = |a: &Wildlife| a.3 && herd(a.2) > 2;
686 let (animal, _, kind, _) = nearest_reachable(map, reservations, animals, from, spare_tame)
687 .or_else(|| nearest_reachable(map, reservations, animals, from, |a| !a.3))?;
688 Some(Task::Hunt {
689 animal,
690 kind,
691 progress: 0,
692 })
693 }
694
695 pub fn find_taming(
696 map: &WorldMap,
697 reservations: &Reservations,
698 animals: &[Wildlife],
699 from: UVec2,
700 ) -> Option<Task> {
701 if animals.iter().filter(|a| a.3).count() >= MAX_TAME {
702 return None;
703 }
704 let (animal, _, kind, _) = nearest_reachable(map, reservations, animals, from, |a| !a.3)?;
705 Some(Task::Tame {
706 animal,
707 kind,
708 progress: 0,
709 })
710 }
711
712 pub fn stockpile_too_full(zones: &Zones, items: &[(Item, TilePos)]) -> bool {
713 let occupied: HashSet<UVec2> = items.iter().map(|(_, pos)| pos.0).collect();
714 zones.has_stockpile() && zones.empty_stockpile_tiles(&occupied) < MIN_EMPTY_STOCKPILE_TILES
715 }
716
717 pub fn find_expansion(
718 map: &WorldMap,
719 zones: &Zones,
720 reservations: &Reservations,
721 items: &[(Item, TilePos)],
722 blueprints: &Blueprints,
723 ) -> Option<Task> {
724 if !reservations.is_free(Target::StockpileExpansion) {
725 return None;
726 }
727 let site = if zones.has_stockpile() {
728 if !stockpile_too_full(zones, items) {
729 return None;
730 }
731 *plan_expansion(map, zones, &blueprints.tiles(), EXPANSION_TILES).first()?
732 } else {
733 find_stockpile_site(map, central_item_tile(items)?)?
734 };
735 Some(Task::ExpandStockpile { site, progress: 0 })
736 }
737
738 fn central_item_tile(items: &[(Item, TilePos)]) -> Option<UVec2> {
739 let tiles: Vec<Vec2> = items.iter().map(|(_, pos)| pos.0.as_vec2()).collect();
740 let centroid = tiles.iter().sum::<Vec2>() / tiles.len().max(1) as f32;
741 items.iter().map(|(_, pos)| pos.0).min_by(|a, b| {
742 a.as_vec2()
743 .distance_squared(centroid)
744 .total_cmp(&b.as_vec2().distance_squared(centroid))
745 })
746 }
747
748 pub struct BuildContext<'a> {
749 pub map: &'a WorldMap,
750 pub zones: &'a Zones,
751 pub reservations: &'a Reservations,
752 pub items: &'a [(Item, TilePos)],
753 pub structures: &'a [(UVec2, StructureKind, u32)],
754 pub planned: &'a [(UVec2, StructureKind)],
755 pub blueprints: &'a Blueprints,
756 pub occupied: &'a HashSet<UVec2>,
757 }
758
759 impl BuildContext<'_> {
760 fn buildable(&self, tile: UVec2) -> bool {
761 can_zone(self.map, tile.as_ivec2())
762 && !self.zones.is_stockpile(tile)
763 && !self.zones.is_farm(tile)
764 && self.map.plant(tile.as_ivec2()).is_none()
765 && !self.occupied.contains(&tile)
766 && self.blueprints.get(tile).is_none()
767 && self.reservations.is_free(Target::Site(tile))
768 }
769
770 fn lights(&self) -> impl Iterator<Item = (UVec2, StructureKind)> + Clone + '_ {
771 self.structures
772 .iter()
773 .map(|&(pos, kind, _)| (pos, kind))
774 .chain(self.planned.iter().copied())
775 }
776
777 fn campfire_needed(&self, home: UVec2) -> bool {
778 let burning = self.structures.iter().any(|&(pos, kind, fuel)| {
779 kind == StructureKind::Campfire
780 && fuel >= CAMPFIRE_MIN_FUEL
781 && pos.as_vec2().distance(home.as_vec2()) <= HOME_RADIUS
782 });
783 let planned = self
784 .planned
785 .iter()
786 .any(|(_, kind)| *kind == StructureKind::Campfire);
787 !burning && !planned
788 }
789
790 fn darkest_stockpile_tile(&self, home: UVec2) -> Option<UVec2> {
791 self.zones
792 .stockpile_tiles()
793 .filter(|&t| brightness(light_from(t, self.lights())) < LIT_BRIGHTNESS)
794 .min_by(|a, b| {
795 a.as_vec2()
796 .distance_squared(home.as_vec2())
797 .total_cmp(&b.as_vec2().distance_squared(home.as_vec2()))
798 .then((a.y, a.x).cmp(&(b.y, b.x)))
799 })
800 }
801
802 fn site_near(&self, target: UVec2, max_distance: f32) -> Option<UVec2> {
803 self.site_near_where(target, max_distance, |_| true)
804 }
805
806 fn site_near_where(
807 &self,
808 target: UVec2,
809 max_distance: f32,
810 extra: impl Fn(UVec2) -> bool,
811 ) -> Option<UVec2> {
812 nearest_tile(self.map, target, SEARCH_TILES, |p| {
813 self.buildable(p) && extra(p)
814 })
815 .filter(|site| site.as_vec2().distance(target.as_vec2()) <= max_distance)
816 }
817
818 fn clear_of_stockpile(&self, tile: UVec2) -> bool {
819 self.zones
820 .stockpile_tiles()
821 .chain(self.zones.farm_tiles())
822 .all(|s| {
823 (s.as_ivec2() - tile.as_ivec2()).abs().max_element() >= BED_STOCKPILE_CLEARANCE
824 })
825 }
826
827 fn count(&self, kind: StructureKind) -> usize {
828 let built = self.structures.iter().filter(|s| s.1 == kind).count();
829 built + self.planned.iter().filter(|p| p.1 == kind).count()
830 }
831
832 fn bed_anchor(&self, home: UVec2) -> UVec2 {
833 let positions = |kind: StructureKind| {
834 let built = self.structures.iter().map(|s| (s.0, s.1));
835 built
836 .chain(self.planned.iter().copied())
837 .filter(move |s| s.1 == kind)
838 .map(|s| s.0)
839 };
840 let nearest = |kind: StructureKind| {
841 positions(kind).min_by(|a, b| {
842 a.as_vec2()
843 .distance_squared(home.as_vec2())
844 .total_cmp(&b.as_vec2().distance_squared(home.as_vec2()))
845 })
846 };
847 nearest(StructureKind::Bed)
848 .or_else(|| nearest(StructureKind::Campfire))
849 .unwrap_or(home)
850 }
851
852 fn material_source(&self, from: UVec2, (kind, amount): (ItemKind, u32)) -> Option<UVec2> {
853 let stacks: Vec<(u32, UVec2)> = self
854 .items
855 .iter()
856 .filter(|(item, pos)| {
857 item.kind == kind && self.reservations.is_free(Target::Pickup(pos.0))
858 })
859 .map(|(item, pos)| (item.count, pos.0))
860 .collect();
861 if stacks.iter().map(|(count, _)| count).sum::<u32>() < amount {
862 return None;
863 }
864 let tiles: HashSet<UVec2> = stacks.into_iter().map(|(_, tile)| tile).collect();
865 nearest_of(self.map, from, SEARCH_TILES, &tiles)
866 }
867
868 fn build_task(&self, from: UVec2, site: UVec2, kind: BuildKind) -> Option<Task> {
869 let pickup = match kind.cost() {
870 (_, 0) => None,
871 cost => Some(self.material_source(from, cost)?),
872 };
873 Some(Task::Build {
874 site,
875 kind,
876 pickup,
877 progress: 0,
878 })
879 }
880 }
881
882 pub const COOK_INPUT: u32 = 6;
883 pub const COOK_WORK: u32 = 300;
884 pub const COOK_MIN_FUEL: u32 = crate::sim::TICKS_PER_HOUR as u32;
885
886 pub fn find_campfire(ctx: &BuildContext, from: UVec2) -> Option<Task> {
887 let home = ctx.zones.home(from);
888 if !ctx.campfire_needed(home) {
889 return None;
890 }
891 let site = ctx.site_near(home, HOME_RADIUS)?;
892 ctx.build_task(from, site, BuildKind::Structure(StructureKind::Campfire))
893 }
894
895 pub fn food_to_fetch(reservations: &Reservations, food_tiles: &HashSet<UVec2>) -> bool {
896 food_tiles
897 .iter()
898 .any(|&p| reservations.is_free(Target::Pickup(p)))
899 }
900
901 pub fn input_source(ctx: &BuildContext, from: UVec2, recipe: Recipe) -> Option<UVec2> {
902 let sources: HashSet<UVec2> = ctx
903 .items
904 .iter()
905 .filter(|(item, pos)| {
906 recipe.accepts(item.kind)
907 && item.count >= recipe.input()
908 && ctx.reservations.is_free(Target::Pickup(pos.0))
909 })
910 .map(|(_, pos)| pos.0)
911 .collect();
912 nearest_of(ctx.map, from, SEARCH_TILES, &sources)
913 }
914
915 pub const BUTCHER_WORK: u32 = 240;
916
917 pub fn find_butchering(
918 map: &WorldMap,
919 reservations: &Reservations,
920 items: &[(Item, TilePos)],
921 from: UVec2,
922 ) -> Option<Task> {
923 let carcasses: HashSet<UVec2> = items
924 .iter()
925 .filter(|(item, pos)| {
926 item.kind == ItemKind::Carcass && reservations.is_free(Target::Pickup(pos.0))
927 })
928 .map(|(_, pos)| pos.0)
929 .collect();
930 let site = nearest_of(map, from, SEARCH_TILES, &carcasses)?;
931 Some(Task::Butcher { site, progress: 0 })
932 }
933
934 pub fn find_workbench(ctx: &BuildContext, from: UVec2) -> Option<Task> {
935 if ctx.count(StructureKind::Workbench) > 0 {
936 return None;
937 }
938 let home = ctx.zones.home(from);
939 let site = ctx.site_near(home, HOME_RADIUS * 1.5)?;
940 ctx.build_task(from, site, BuildKind::Structure(StructureKind::Workbench))
941 }
942
943 pub fn tool_source(
944 map: &WorldMap,
945 reservations: &Reservations,
946 items: &[(Item, TilePos)],
947 from: UVec2,
948 ) -> Option<UVec2> {
949 let tools: HashSet<UVec2> = items
950 .iter()
951 .filter(|(item, pos)| {
952 item.kind == ItemKind::Tools && reservations.is_free(Target::Pickup(pos.0))
953 })
954 .map(|(_, pos)| pos.0)
955 .collect();
956 if tools.is_empty() {
957 return None;
958 }
959 nearest_of(map, from, SEARCH_TILES, &tools)
960 }
961
962 pub fn find_crafting(ctx: &BuildContext, from: UVec2, recipe: Recipe) -> Option<Task> {
963 let station = ctx
964 .structures
965 .iter()
966 .filter(|&&(pos, kind, fuel)| {
967 kind == recipe.station()
968 && fuel >= COOK_MIN_FUEL
969 && ctx.reservations.is_free(Target::Station(pos))
970 })
971 .map(|&(pos, ..)| pos)
972 .min_by(|a, b| {
973 a.as_vec2()
974 .distance_squared(from.as_vec2())
975 .total_cmp(&b.as_vec2().distance_squared(from.as_vec2()))
976 })?;
977 let pickup = input_source(ctx, from, recipe)?;
978 Some(Task::Craft {
979 recipe,
980 pickup: Some(pickup),
981 station,
982 progress: 0,
983 })
984 }
985
986 pub fn find_lighting(ctx: &BuildContext, from: UVec2, dark: bool) -> Option<Task> {
987 let home = ctx.zones.home(from);
988 let (kind, site) = if ctx.campfire_needed(home) {
989 (StructureKind::Campfire, ctx.site_near(home, HOME_RADIUS)?)
990 } else if !dark {
991 return None;
992 } else {
993 let dark = ctx.darkest_stockpile_tile(home)?;
994 let reach = StructureKind::Torch.radius() * 0.6;
995 (StructureKind::Torch, ctx.site_near(dark, reach)?)
996 };
997 ctx.build_task(from, site, BuildKind::Structure(kind))
998 }
999
1000 pub fn chilly_soon(tick: SimTick) -> bool {
1001 let later = SimTick(tick.0 + LIGHT_LOOKAHEAD_TICKS);
1002 outdoor_temperature(tick) < COLD_TEMP || outdoor_temperature(later) < COLD_TEMP
1003 }
1004
1005 pub fn find_warm_spot(
1006 map: &WorldMap,
1007 structures: &[(UVec2, StructureKind, u32)],
1008 outdoor: f32,
1009 from: UVec2,
1010 ) -> Option<UVec2> {
1011 let fires: Vec<(UVec2, StructureKind)> = structures
1012 .iter()
1013 .filter(|(_, kind, _)| kind.heat() > 0.0)
1014 .map(|&(pos, kind, _)| (pos, kind))
1015 .collect();
1016 let taken: HashSet<UVec2> = structures.iter().map(|(pos, ..)| *pos).collect();
1017 let warm: HashSet<UVec2> = fires
1018 .iter()
1019 .flat_map(|&(pos, kind)| {
1020 let r = kind.radius().ceil() as i32;
1021 (-r..=r).flat_map(move |dy| (-r..=r).map(move |dx| pos.as_ivec2() + IVec2::new(dx, dy)))
1022 })
1023 .filter(|&p| map.in_bounds(p) && map.move_cost(p).is_some())
1024 .map(|p| p.as_uvec2())
1025 .filter(|p| !taken.contains(p))
1026 .filter(|&p| tile_temperature(map, outdoor, p, fires.iter().copied()) >= WARM_TEMP)
1027 .collect();
1028 nearest_of(map, from, SEARCH_TILES, &warm)
1029 }
1030
1031 pub fn find_refuel(ctx: &BuildContext, from: UVec2) -> Option<Task> {
1032 let home = ctx.zones.home(from);
1033 let (site, kind, _) = ctx
1034 .structures
1035 .iter()
1036 .filter(|&&(pos, kind, fuel)| {
1037 kind.fuel_ticks()
1038 .is_some_and(|full| fuel < full / REFUEL_BELOW_SHARE)
1039 && pos.as_vec2().distance(home.as_vec2()) <= HOME_RADIUS * 2.0
1040 && ctx.reservations.is_free(Target::Site(pos))
1041 })
1042 .min_by_key(|(.., fuel)| *fuel)?;
1043 ctx.build_task(from, *site, BuildKind::Refuel(*kind))
1044 }
1045
1046 pub fn find_bed_site(ctx: &BuildContext, colonists: usize, from: UVec2) -> Option<Task> {
1047 if ctx.count(StructureKind::Bed) >= colonists {
1048 return None;
1049 }
1050 let anchor = ctx.bed_anchor(ctx.zones.home(from));
1051 let site = ctx.site_near_where(anchor, HOME_RADIUS * 1.5, |p| ctx.clear_of_stockpile(p))?;
1052 ctx.build_task(from, site, BuildKind::Structure(StructureKind::Bed))
1053 }
1054
1055 pub fn find_blueprint_work(ctx: &BuildContext, from: UVec2) -> Option<Task> {
1056 if ctx.blueprints.0.is_empty() {
1057 return None;
1058 }
1059 let mut found = None;
1060 nearest_tile(ctx.map, from, SEARCH_TILES, |p| {
1061 found = std::iter::once(IVec2::ZERO)
1062 .chain(NEIGHBORS)
1063 .filter_map(|d| {
1064 let site = p.as_ivec2() + d;
1065 (site.min_element() >= 0).then(|| site.as_uvec2())
1066 })
1067 .filter(|&site| site == p || ctx.map.move_cost(site.as_ivec2()).is_none())
1068 .find_map(|site| {
1069 let kind = ctx.blueprints.get(site)?;
1070 if !ctx.reservations.is_free(Target::Site(site)) {
1071 return None;
1072 }
1073 ctx.build_task(from, site, kind)
1074 });
1075 found.is_some()
1076 })?;
1077 found
1078 }
1079
1080 fn on_the_way(map: &WorldMap, jobs: impl Iterator<Item = Option<Job>>) -> HashMap<ItemKind, u32> {
1081 let mut totals = HashMap::new();
1082 for job in jobs.flatten() {
1083 if let Task::Work { site, .. } = job.task
1084 && let Some(work) = site_work(map, site)
1085 {
1086 *totals.entry(work.item).or_insert(0) += work.count;
1087 }
1088 }
1089 totals
1090 }
1091
1092 #[allow(clippy::too_many_arguments)]
1093 fn choose_jobs(
1094 mut commands: Commands,
1095 time: Res<Time>,
1096 map: Res<WorldMap>,
1097 zones: Res<Zones>,
1098 goals: Res<ColonyGoals>,
1099 item_assets: Res<ItemAssets>,
1100 tick: Res<SimTick>,
1101 blueprints: Res<Blueprints>,
1102 mut rng: ResMut<SimRng>,
1103 mut reservations: ResMut<Reservations>,
1104 mut items: ItemQuery,
1105 structures: Query<(&TilePos, &Structure, Option<&Fuel>), Without<Colonist>>,
1106 animals: Query<(Entity, &TilePos, &Animal, Has<Tame>), Without<Colonist>>,
1107 mut colonists: Query<
1108 (
1109 Entity,
1110 &TilePos,
1111 &mut Path,
1112 &mut CurrentJob,
1113 &mut IdleTimer,
1114 &mut Carrying,
1115 &Rest,
1116 &Hunger,
1117 Option<&Family>,
1118 Has<Child>,
1119 &WorkPriorities,
1120 &Warmth,
1121 Has<Tool>,
1122 ),
1123 With<Colonist>,
1124 >,
1125 plans: (Res<Homes>, Res<Designations>),
1126 ) {
1127 let (homes, designations) = plans;
1128 let mut anyone_ready = false;
1129 for (_, _, path, job, mut idle, _, rest, hunger, ..) in &mut colonists {
1130 let idle_now = job.0.is_none() && path.0.is_empty();
1131 if idle_now {
1132 idle.0.tick(time.delta());
1133 }
1134 anyone_ready |=
1135 (idle_now && idle.0.is_finished()) || rest.0 < EXHAUSTED_REST || hunger.0 < STARVING;
1136 }
1137 if !anyone_ready {
1138 return;
1139 }
1140 let wildlife: Vec<Wildlife> = animals
1141 .iter()
1142 .map(|(e, pos, a, tame)| (e, pos.0, a.kind, tame))
1143 .collect();
1144 let adults = colonists.iter().filter(|c| !c.9).count();
1145 let structures_list: Vec<(UVec2, StructureKind, u32)> = structures
1146 .iter()
1147 .map(|(pos, s, fuel)| (pos.0, s.kind, fuel.map_or(u32::MAX, |f| f.0)))
1148 .collect();
1149 let mut planned: Vec<(UVec2, StructureKind)> = colonists
1150 .iter()
1151 .filter_map(|c| match c.3.0?.task {
1152 Task::Build {
1153 site,
1154 kind: BuildKind::Structure(kind),
1155 ..
1156 } => Some((site, kind)),
1157 _ => None,
1158 })
1159 .collect();
1160 let dark = dark_soon(*tick);
1161 let chilly = chilly_soon(*tick);
1162 let outdoor = outdoor_temperature(*tick);
1163 let sowing = sowing_season(*tick);
1164 let carried: Vec<Item> = colonists.iter().filter_map(|c| c.5.0).collect();
1165 let home = zones.home(map.size / 2);
1166 let stocks: HashMap<ItemKind, u32> = ItemKind::ALL
1167 .iter()
1168 .map(|&kind| {
1169 let all = items
1170 .iter()
1171 .filter(|(_, _, pos)| in_colony(home, pos.0))
1172 .map(|(_, item, _)| item)
1173 .chain(carried.iter());
1174 (kind, stock(all, kind))
1175 })
1176 .collect();
1177 let mut pending = on_the_way(&map, colonists.iter().map(|c| c.3.0));
1178 for job in colonists.iter().filter_map(|c| c.3.0) {
1179 match job.task {
1180 Task::Hunt { kind, .. } => *pending.entry(ItemKind::Meat).or_insert(0) += kind.meat(),
1181 Task::Craft { recipe, .. } => *pending.entry(recipe.output()).or_insert(0) += 1,
1182 _ => {}
1183 }
1184 }
1185 let food_tiles: HashSet<UVec2> = items
1186 .iter()
1187 .filter(|(_, item, _)| item.kind.is_food())
1188 .map(|(_, _, pos)| pos.0)
1189 .collect();
1190 let hour = hour_of_day(*tick);
1191 for (
1192 entity,
1193 tile,
1194 mut path,
1195 mut job,
1196 mut idle,
1197 mut carrying,
1198 rest,
1199 hunger,
1200 family,
1201 child,
1202 priorities,
1203 warmth,
1204 has_tool,
1205 ) in &mut colonists
1206 {
1207 let exhausted = rest.0 < EXHAUSTED_REST;
1208 let starving = hunger.0 < STARVING && food_to_fetch(&reservations, &food_tiles);
1209 let urgent = exhausted || starving;
1210 if urgent
1211 && job.0.is_some_and(|j| {
1212 !j.ordered && !matches!(j.task, Task::Sleep { .. } | Task::Eat { .. })
1213 })
1214 {
1215 reservations.release_all(entity);
1216 job.0 = None;
1217 path.0.truncate(1);
1218 }
1219 let rested = idle.0.is_finished();
1220 if job.0.is_some() || !path.0.is_empty() || !(rested || urgent) {
1221 continue;
1222 }
1223 let snapshot: Vec<(Item, TilePos)> =
1224 items.iter().map(|(_, item, pos)| (*item, *pos)).collect();
1225 let refs = snapshot.iter().map(|(i, p)| (i, p));
1226 let wanted = |kind: ItemKind| goals.wanted(kind, &stocks, &pending);
1227 let sleep = SleepContext {
1228 map: &map,
1229 zones: &zones,
1230 reservations: &reservations,
1231 items: &snapshot,
1232 structures: &structures_list,
1233 blueprints: &blueprints,
1234 homes: &homes,
1235 family: family.copied(),
1236 child,
1237 };
1238 let food = wants_food(hunger.0)
1239 .then(|| find_food(&map, &reservations, &snapshot, tile.0))
1240 .flatten();
1241 let task = if exhausted {
1242 Some(Task::Sleep {
1243 spot: sleep.spot(tile.0),
1244 })
1245 } else if let Some(pickup) = food {
1246 if let Some(load) = carrying.0.take() {
1247 drop_items(&mut commands, &mut items, &item_assets, tile.0, load);
1248 }
1249 Some(Task::Eat {
1250 pickup: Some(pickup),
1251 progress: 0,
1252 })
1253 } else if wants_sleep(rest.0, hour) {
1254 Some(Task::Sleep {
1255 spot: sleep.spot(tile.0),
1256 })
1257 } else if let Some(spot) = warmth
1258 .cold()
1259 .then(|| find_warm_spot(&map, &structures_list, outdoor, tile.0))
1260 .flatten()
1261 .filter(|&spot| spot != tile.0)
1262 {
1263 find_path(&map, tile.0, spot).map(|route| {
1264 path.0 = route.into();
1265 Task::MoveTo(spot)
1266 })
1267 } else if child {
1268 None
1269 } else if let Some(load) = carrying.0 {
1270 let storage = find_storage(&map, &zones, &reservations, refs, tile.0, load.kind);
1271 if storage.is_none() {
1272 drop_items(&mut commands, &mut items, &item_assets, tile.0, load);
1273 carrying.0 = None;
1274 }
1275 storage.map(|to| Task::Haul { pickup: None, to })
1276 } else if let Some(pickup) = (!has_tool)
1277 .then(|| tool_source(&map, &reservations, &snapshot, tile.0))
1278 .flatten()
1279 {
1280 Some(Task::Equip { pickup })
1281 } else {
1282 let occupied: HashSet<UVec2> = structures_list
1283 .iter()
1284 .map(|(pos, ..)| *pos)
1285 .chain(planned.iter().map(|(pos, _)| *pos))
1286 .chain(snapshot.iter().map(|(_, pos)| pos.0))
1287 .collect();
1288 let build = BuildContext {
1289 map: &map,
1290 zones: &zones,
1291 reservations: &reservations,
1292 items: &snapshot,
1293 structures: &structures_list,
1294 planned: &planned,
1295 blueprints: &blueprints,
1296 occupied: &occupied,
1297 };
1298 let cooking = || {
1299 (wanted(ItemKind::Meal) && input_source(&build, tile.0, Recipe::Meal).is_some())
1300 .then(|| {
1301 find_crafting(&build, tile.0, Recipe::Meal)
1302 .or_else(|| find_campfire(&build, tile.0))
1303 })
1304 .flatten()
1305 };
1306 let toolmaking = || {
1307 (wanted(ItemKind::Tools) && input_source(&build, tile.0, Recipe::Tools).is_some())
1308 .then(|| {
1309 find_crafting(&build, tile.0, Recipe::Tools)
1310 .or_else(|| find_workbench(&build, tile.0))
1311 })
1312 .flatten()
1313 };
1314 let designated = || {
1315 (!designations.0.is_empty())
1316 .then(|| {
1317 find_work(&map, &reservations, tile.0, |site, _| {
1318 designations.0.contains(&site)
1319 })
1320 })
1321 .flatten()
1322 };
1323 let candidates: [(WorkType, &dyn Fn() -> Option<Task>); 16] = [
1324 (WorkType::Haul, &|| {
1325 find_expansion(&map, &zones, &reservations, &snapshot, &blueprints)
1326 }),
1327 (WorkType::Build, &|| find_refuel(&build, tile.0)),
1328 (WorkType::Build, &|| {
1329 (dark || chilly)
1330 .then(|| find_lighting(&build, tile.0, dark))
1331 .flatten()
1332 }),
1333 (WorkType::Food, &cooking),
1334 (WorkType::Food, &|| {
1335 find_butchering(&map, &reservations, &snapshot, tile.0)
1336 }),
1337 (WorkType::Food, &|| {
1338 ItemKind::ALL
1339 .iter()
1340 .any(|&k| k.is_food() && wanted(k))
1341 .then(|| {
1342 let food = |_, k: ItemKind| k.is_food() && wanted(k);
1343 find_work_within(&map, &reservations, tile.0, FORAGE_SEARCH_TILES, food)
1344 })
1345 .flatten()
1346 }),
1347 (WorkType::Gather, &designated),
1348 (WorkType::Food, &|| {
1349 (sowing && wanted(ItemKind::Vegetables))
1350 .then(|| find_sowing(&map, &zones, &reservations, tile.0))
1351 .flatten()
1352 }),
1353 (WorkType::Build, &|| find_bed_site(&build, adults, tile.0)),
1354 (WorkType::Craft, &toolmaking),
1355 (WorkType::Build, &|| find_blueprint_work(&build, tile.0)),
1356 (WorkType::Gather, &|| {
1357 ItemKind::ALL
1358 .iter()
1359 .any(|&k| wanted(k))
1360 .then(|| find_work(&map, &reservations, tile.0, |_, k| wanted(k)))
1361 .flatten()
1362 }),
1363 (WorkType::Food, &|| {
1364 wanted(ItemKind::Meat)
1365 .then(|| find_hunt(&map, &reservations, &wildlife, tile.0))
1366 .flatten()
1367 }),
1368 (WorkType::Food, &|| {
1369 sowing
1370 .then(|| find_sowing(&map, &zones, &reservations, tile.0))
1371 .flatten()
1372 }),
1373 (WorkType::Tame, &|| {
1374 (!wanted(ItemKind::Meat))
1375 .then(|| find_taming(&map, &reservations, &wildlife, tile.0))
1376 .flatten()
1377 }),
1378 (WorkType::Haul, &|| {
1379 find_haul(&map, &zones, &reservations, &snapshot, tile.0)
1380 }),
1381 ];
1382 candidates
1383 .iter()
1384 .filter(|(work, _)| priorities.allows(*work))
1385 .find_map(|(_, find)| find())
1386 };
1387 if let Some(task) = task {
1388 if let Task::Work { site, .. } = task
1389 && let Some(work) = site_work(&map, site)
1390 {
1391 *pending.entry(work.item).or_insert(0) += work.count;
1392 }
1393 match task {
1394 Task::Hunt { kind, .. } => {
1395 *pending.entry(ItemKind::Meat).or_insert(0) += kind.meat()
1396 }
1397 Task::Craft { recipe, .. } => *pending.entry(recipe.output()).or_insert(0) += 1,
1398 _ => {}
1399 }
1400 if let Task::Build {
1401 site,
1402 kind: BuildKind::Structure(kind),
1403 ..
1404 } = task
1405 {
1406 planned.push((site, kind));
1407 }
1408 reserve_task(&mut reservations, task, entity);
1409 job.0 = Some(Job {
1410 task,
1411 ordered: false,
1412 });
1413 continue;
1414 }
1415 let goal = tile.0.as_ivec2()
1416 + IVec2::new(
1417 rng.range_i32(-WANDER_RADIUS..WANDER_RADIUS + 1),
1418 rng.range_i32(-WANDER_RADIUS..WANDER_RADIUS + 1),
1419 );
1420 if map.in_bounds(goal)
1421 && let Some(wander_path) = find_path(&map, tile.0, goal.as_uvec2())
1422 {
1423 path.0 = wander_path.into();
1424 idle.restart(rng.range_f32(IDLE_SECS));
1425 }
1426 }
1427 }
1428
1429 enum Step {
1430 Continue,
1431 Done,
1432 }
1433
1434 fn walk_to(map: &WorldMap, from: UVec2, to: UVec2, path: &mut Path) -> Step {
1435 match find_path(map, from, to) {
1436 Some(p) if !p.is_empty() => {
1437 path.0 = p.into();
1438 Step::Continue
1439 }
1440 _ => Step::Done,
1441 }
1442 }
1443
1444 #[allow(clippy::too_many_arguments)]
1445 fn run_jobs(
1446 mut commands: Commands,
1447 mut map: ResMut<WorldMap>,
1448 mut zones: ResMut<Zones>,
1449 mut reservations: ResMut<Reservations>,
1450 mut rng: ResMut<SimRng>,
1451 item_assets: Res<ItemAssets>,
1452 structure_assets: Res<StructureAssets>,
1453 light: Res<LightMap>,
1454 tick: Res<SimTick>,
1455 mut blueprints: ResMut<Blueprints>,
1456 mut goals: ResMut<ColonyGoals>,
1457 mut items: ItemQuery,
1458 mut structures: Query<(Entity, &TilePos, &Structure, Option<&mut Fuel>), Without<Colonist>>,
1459 animals: Query<(&TilePos, &Path), (With<Animal>, Without<Colonist>)>,
1460 messages: (MessageWriter<TileChanged>, MessageWriter<Ate>),
1461 mut colonists: Query<
1462 (
1463 Entity,
1464 &TilePos,
1465 &mut Path,
1466 &mut CurrentJob,
1467 &mut IdleTimer,
1468 &mut Carrying,
1469 &Rest,
1470 &mut Hunger,
1471 Has<Sick>,
1472 Option<&mut Tool>,
1473 ),
1474 With<Colonist>,
1475 >,
1476 ) {
1477 let (mut changed, mut ate) = messages;
1478 let hour = hour_of_day(*tick);
1479 let occupied: Vec<UVec2> = colonists
1480 .iter()
1481 .flat_map(|c| [Some(c.1.0), c.2.0.front().copied()])
1482 .chain(
1483 animals
1484 .iter()
1485 .flat_map(|(t, p)| [Some(t.0), p.0.front().copied()]),
1486 )
1487 .flatten()
1488 .collect();
1489 for (
1490 entity,
1491 tile,
1492 mut path,
1493 mut current,
1494 mut idle,
1495 mut carrying,
1496 rest,
1497 mut hunger,
1498 sick,
1499 mut tool,
1500 ) in &mut colonists
1501 {
1502 let Some(mut job) = current.0 else {
1503 continue;
1504 };
1505 let rate = match work_rate(&light, tile.0) {
1506 rate if sick => rate.div_ceil(2),
1507 rate => rate,
1508 };
1509 let rate = tool_rate(rate, tool.is_some());
1510 if !path.0.is_empty() {
1511 continue;
1512 }
1513 if let Some(tool) = tool.as_mut()
1514 && !matches!(
1515 job.task,
1516 Task::MoveTo(_) | Task::Sleep { .. } | Task::Eat { .. } | Task::Equip { .. }
1517 )
1518 {
1519 tool.wear += 1;
1520 if tool.wear >= TOOL_LIFE_TICKS {
1521 commands.entity(entity).remove::<Tool>();
1522 }
1523 }
1524 let step = match &mut job.task {
1525 Task::MoveTo(_) => Step::Done,
1526 Task::Equip { pickup } => {
1527 let from = *pickup;
1528 if reservations.holder(Target::Pickup(from)) != Some(entity) {
1529 Step::Done
1530 } else if tile.0 != from {
1531 walk_to(&map, tile.0, from, &mut path)
1532 } else {
1533 reservations.release(Target::Pickup(from));
1534 let stack = items
1535 .iter()
1536 .any(|(_, item, pos)| pos.0 == from && item.kind == ItemKind::Tools);
1537 if stack && take_items(&mut commands, &mut items, from, 1).is_some() {
1538 goals.record_use(ItemKind::Tools, 1);
1539 commands.entity(entity).insert(Tool::default());
1540 }
1541 Step::Done
1542 }
1543 }
1544 Task::Craft {
1545 recipe,
1546 pickup,
1547 station,
1548 progress,
1549 } => {
1550 let (recipe, station) = (*recipe, *station);
1551 let input = recipe.input();
1552 let fire = structures
1553 .iter()
1554 .any(|(_, pos, s, _)| pos.0 == station && s.kind == recipe.station());
1555 let ours = reservations.holder(Target::Station(station)) == Some(entity);
1556 if !fire || !ours {
1557 Step::Done
1558 } else if let Some(from) = *pickup {
1559 if reservations.holder(Target::Pickup(from)) != Some(entity) {
1560 Step::Done
1561 } else if tile.0 != from {
1562 walk_to(&map, tile.0, from, &mut path)
1563 } else {
1564 let load = take_items(&mut commands, &mut items, from, input)
1565 .filter(|load| recipe.accepts(load.kind) && load.count > 0);
1566 carrying.0 = load;
1567 reservations.release(Target::Pickup(from));
1568 match load {
1569 Some(load) if load.count >= input => {
1570 *pickup = None;
1571 Step::Continue
1572 }
1573 _ => Step::Done,
1574 }
1575 }
1576 } else if !carrying
1577 .0
1578 .is_some_and(|load| recipe.accepts(load.kind) && load.count >= input)
1579 {
1580 Step::Done
1581 } else if !is_adjacent(tile.0.as_ivec2(), station.as_ivec2()) {
1582 match find_path_adjacent(&map, tile.0, station) {
1583 Some(p) if !p.is_empty() => {
1584 path.0 = p.into();
1585 Step::Continue
1586 }
1587 _ => Step::Done,
1588 }
1589 } else {
1590 *progress += rate;
1591 if *progress < recipe.work() {
1592 Step::Continue
1593 } else {
1594 if let Some(load) = carrying.0 {
1595 goals.record_use(load.kind, input);
1596 }
1597 carrying.0 = carrying.0.and_then(|load| {
1598 (load.count > input).then_some(Item {
1599 count: load.count - input,
1600 ..load
1601 })
1602 });
1603 drop_items(
1604 &mut commands,
1605 &mut items,
1606 &item_assets,
1607 tile.0,
1608 Item::new(recipe.output(), 1),
1609 );
1610 Step::Done
1611 }
1612 }
1613 }
1614 Task::Hunt {
1615 animal,
1616 kind,
1617 progress,
1618 } => match animals.get(*animal) {
1619 Ok((prey, _)) if reservations.holder(Target::Animal(*animal)) == Some(entity) => {
1620 let prey = prey.0;
1621 let in_range = tile.0.as_vec2().distance(prey.as_vec2()) <= HUNT_RANGE
1622 && line_of_sight(&map, tile.0, prey);
1623 let stale = path.0.back().is_none_or(|end| {
1624 end.as_vec2().distance(prey.as_vec2()) > HUNT_RANGE / 2.0
1625 });
1626 if !in_range && !stale {
1627 Step::Continue
1628 } else if !in_range {
1629 match find_path_adjacent(&map, tile.0, prey) {
1630 Some(p) if !p.is_empty() => {
1631 path.0 = p.into();
1632 Step::Continue
1633 }
1634 _ => Step::Done,
1635 }
1636 } else {
1637 path.0.truncate(1);
1638 *progress += rate;
1639 if *progress < kind.hunt_work() {
1640 Step::Continue
1641 } else {
1642 commands.entity(*animal).despawn();
1643 drop_items(
1644 &mut commands,
1645 &mut items,
1646 &item_assets,
1647 prey,
1648 Item::new(ItemKind::Carcass, kind.meat()),
1649 );
1650 Step::Done
1651 }
1652 }
1653 }
1654 _ => Step::Done,
1655 },
1656 Task::Tame {
1657 animal,
1658 kind,
1659 progress,
1660 } => match animals.get(*animal) {
1661 Ok((prey, _)) if reservations.holder(Target::Animal(*animal)) == Some(entity) => {
1662 let prey = prey.0;
1663 if prey != tile.0 && !is_adjacent(tile.0.as_ivec2(), prey.as_ivec2()) {
1664 if path.0.is_empty() {
1665 match find_path_adjacent(&map, tile.0, prey) {
1666 Some(p) if !p.is_empty() => {
1667 path.0 = p.into();
1668 Step::Continue
1669 }
1670 _ => Step::Done,
1671 }
1672 } else {
1673 Step::Continue
1674 }
1675 } else {
1676 *progress += rate;
1677 if *progress < kind.tame_work() {
1678 Step::Continue
1679 } else {
1680 commands.entity(*animal).insert(Tame);
1681 Step::Done
1682 }
1683 }
1684 }
1685 _ => Step::Done,
1686 },
1687 Task::Butcher { site, progress } => {
1688 let site = *site;
1689 if reservations.holder(Target::Pickup(site)) != Some(entity) {
1690 Step::Done
1691 } else if tile.0 != site {
1692 walk_to(&map, tile.0, site, &mut path)
1693 } else {
1694 *progress += rate;
1695 if *progress < BUTCHER_WORK {
1696 Step::Continue
1697 } else {
1698 let is_carcass = items
1699 .iter()
1700 .any(|(_, item, pos)| pos.0 == site && item.kind == ItemKind::Carcass);
1701 if is_carcass
1702 && let Some(carcass) =
1703 take_items(&mut commands, &mut items, site, u32::MAX)
1704 {
1705 let meat = Item {
1706 kind: ItemKind::Meat,
1707 ..carcass
1708 };
1709 drop_items(&mut commands, &mut items, &item_assets, site, meat);
1710 }
1711 Step::Done
1712 }
1713 }
1714 }
1715 Task::Eat { pickup, progress } => {
1716 if let Some(from) = *pickup {
1717 if reservations.holder(Target::Pickup(from)) != Some(entity) {
1718 Step::Done
1719 } else if tile.0 != from {
1720 walk_to(&map, tile.0, from, &mut path)
1721 } else {
1722 let kind = items
1723 .iter()
1724 .find(|(_, item, pos)| pos.0 == from && item.kind.is_food())
1725 .map(|(_, item, _)| item.kind);
1726 let meal = kind.and_then(|kind| {
1727 take_items(&mut commands, &mut items, from, portion(kind, hunger.0))
1728 });
1729 reservations.release(Target::Pickup(from));
1730 match meal.filter(|m| m.kind.is_food()) {
1731 Some(meal) => {
1732 carrying.0 = Some(meal);
1733 *pickup = None;
1734 Step::Continue
1735 }
1736 None => Step::Done,
1737 }
1738 }
1739 } else if let Some(meal) = carrying.0.filter(|m| m.kind.is_food()) {
1740 *progress += 1;
1741 if *progress < EAT_TICKS {
1742 Step::Continue
1743 } else {
1744 hunger.0 = (hunger.0 + meal.count as f32 * meal.kind.nutrition()).min(1.0);
1745 goals.record_use(meal.kind, meal.count);
1746 ate.write(Ate {
1747 colonist: entity,
1748 kind: meal.kind,
1749 });
1750 carrying.0 = None;
1751 Step::Done
1752 }
1753 } else {
1754 Step::Done
1755 }
1756 }
1757 Task::Sleep { spot } => {
1758 if reservations.holder(Target::SleepSpot(*spot)) != Some(entity) {
1759 Step::Done
1760 } else if tile.0 != *spot {
1761 let step = walk_to(&map, tile.0, *spot, &mut path);
1762 if matches!(step, Step::Done) {
1763 reservations.release(Target::SleepSpot(*spot));
1764 *spot = tile.0;
1765 reservations.reserve(Target::SleepSpot(*spot), entity);
1766 }
1767 Step::Continue
1768 } else if done_sleeping(rest.0, hour) {
1769 Step::Done
1770 } else {
1771 Step::Continue
1772 }
1773 }
1774 Task::Work {
1775 site,
1776 kind,
1777 progress,
1778 } => {
1779 let (site, kind) = (*site, *kind);
1780 let work = site_work(&map, site).filter(|w| w.kind == kind);
1781 let valid =
1782 work.is_some() || (kind == WorkKind::Sow && sowable(&map, &zones, site));
1783 let ours = reservations.holder(Target::Site(site)) == Some(entity);
1784 if !valid || !ours {
1785 Step::Done
1786 } else if !is_adjacent(tile.0.as_ivec2(), site.as_ivec2()) {
1787 match find_path_adjacent(&map, tile.0, site) {
1788 Some(p) if !p.is_empty() => {
1789 path.0 = p.into();
1790 Step::Continue
1791 }
1792 _ => Step::Done,
1793 }
1794 } else {
1795 *progress += rate;
1796 if *progress < kind.work() {
1797 Step::Continue
1798 } else {
1799 kind.complete(&mut map, site);
1800 changed.write(TileChanged(site));
1801 if let Some(work) = work {
1802 drop_items(
1803 &mut commands,
1804 &mut items,
1805 &item_assets,
1806 site,
1807 Item::new(work.item, work.count),
1808 );
1809 }
1810 Step::Done
1811 }
1812 }
1813 }
1814 Task::ExpandStockpile { site, progress } => {
1815 if tile.0 != *site {
1816 walk_to(&map, tile.0, *site, &mut path)
1817 } else {
1818 *progress += rate;
1819 if *progress >= EXPAND_WORK {
1820 if zones.has_stockpile() {
1821 let excluded = blueprints.tiles();
1822 let tiles = plan_expansion(&map, &zones, &excluded, EXPANSION_TILES);
1823 zones.add_tiles(&map, tiles);
1824 } else {
1825 zones.add_tiles(&map, new_stockpile_tiles(*site));
1826 }
1827 Step::Done
1828 } else {
1829 Step::Continue
1830 }
1831 }
1832 }
1833 Task::Build {
1834 site,
1835 kind,
1836 pickup,
1837 progress,
1838 } => {
1839 let (site, kind) = (*site, *kind);
1840 let ours = reservations.holder(Target::Site(site)) == Some(entity);
1841 let planned = match kind {
1842 BuildKind::Structure(
1843 StructureKind::Campfire
1844 | StructureKind::Torch
1845 | StructureKind::Bed
1846 | StructureKind::Workbench,
1847 )
1848 | BuildKind::Refuel(_) => true,
1849 _ => blueprints.get(site) == Some(kind),
1850 };
1851 let structure_there = structures.iter().any(|(_, pos, ..)| pos.0 == site);
1852 let blocked = map.plant(site.as_ivec2()).is_some()
1853 || match kind {
1854 BuildKind::Roof => false,
1855 BuildKind::Refuel(fire) => !structures
1856 .iter()
1857 .any(|(_, pos, s, _)| pos.0 == site && s.kind == fire),
1858 BuildKind::Deconstruct => !structure_there,
1859 _ => structure_there,
1860 };
1861 let (material, cost) = kind.cost();
1862 if !ours || blocked || !planned {
1863 Step::Done
1864 } else if let Some(from) = *pickup {
1865 if reservations.holder(Target::Pickup(from)) != Some(entity) {
1866 Step::Done
1867 } else if tile.0 != from {
1868 walk_to(&map, tile.0, from, &mut path)
1869 } else if carrying.0.is_some_and(|load| load.kind != material) {
1870 Step::Done
1871 } else {
1872 let have = carrying.0.map_or(0, |load| load.count);
1873 let taken = take_items(&mut commands, &mut items, from, cost - have)
1874 .filter(|load| load.kind == material)
1875 .map_or(0, |load| load.count);
1876 reservations.release(Target::Pickup(from));
1877 let total = have + taken;
1878 carrying.0 = (total > 0).then_some(Item::new(material, total));
1879 let next = nearest_tile(&map, tile.0, SEARCH_TILES, |p| {
1880 reservations.is_free(Target::Pickup(p))
1881 && items.iter().any(|(_, item, pos)| {
1882 pos.0 == p && item.kind == material && item.count > 0
1883 })
1884 });
1885 match next {
1886 _ if total >= cost => {
1887 *pickup = None;
1888 Step::Continue
1889 }
1890 Some(next) => {
1891 reservations.reserve(Target::Pickup(next), entity);
1892 *pickup = Some(next);
1893 Step::Continue
1894 }
1895 None => Step::Done,
1896 }
1897 }
1898 } else if cost > 0
1899 && !carrying
1900 .0
1901 .is_some_and(|load| load.kind == material && load.count >= cost)
1902 {
1903 Step::Done
1904 } else if !is_adjacent(tile.0.as_ivec2(), site.as_ivec2()) {
1905 match find_path_adjacent(&map, tile.0, site) {
1906 Some(p) if !p.is_empty() => {
1907 path.0 = p.into();
1908 Step::Continue
1909 }
1910 _ => Step::Done,
1911 }
1912 } else {
1913 *progress = (*progress + rate).min(kind.work());
1914 let someone_in_the_way = kind.blocks_tile() && occupied.contains(&site);
1915 if *progress < kind.work() || someone_in_the_way {
1916 Step::Continue
1917 } else {
1918 goals.record_use(material, cost);
1919 carrying.0 = carrying.0.and_then(|load| {
1920 (load.count > cost).then_some(Item {
1921 count: load.count - cost,
1922 ..load
1923 })
1924 });
1925 match kind {
1926 BuildKind::Structure(structure) => {
1927 spawn_structure(&mut commands, &structure_assets, site, structure);
1928 if kind.blocks_tile() {
1929 map.set_wall(site, true);
1930 changed.write(TileChanged(site));
1931 }
1932 }
1933 BuildKind::Roof => {
1934 map.set_roof(site, true);
1935 changed.write(TileChanged(site));
1936 }
1937 BuildKind::Pave => {
1938 map.set(site, Terrain::Paved);
1939 changed.write(TileChanged(site));
1940 }
1941 BuildKind::Deconstruct => {
1942 for (entity, pos, structure, _) in &structures {
1943 if pos.0 != site {
1944 continue;
1945 }
1946 commands.entity(entity).despawn();
1947 let (refund, amount) = structure.kind.cost();
1948 if amount / 2 > 0 {
1949 drop_items(
1950 &mut commands,
1951 &mut items,
1952 &item_assets,
1953 site,
1954 Item::new(refund, amount / 2),
1955 );
1956 }
1957 }
1958 if map.is_wall(site.as_ivec2()) {
1959 map.set_wall(site, false);
1960 changed.write(TileChanged(site));
1961 }
1962 }
1963 BuildKind::Refuel(fire) => {
1964 for (_, pos, _, fuel) in &mut structures {
1965 if pos.0 == site
1966 && let Some(mut fuel) = fuel
1967 {
1968 fuel.0 = fire.fuel_ticks().unwrap_or(fuel.0);
1969 }
1970 }
1971 }
1972 }
1973 blueprints.0.remove(&site);
1974 Step::Done
1975 }
1976 }
1977 }
1978 Task::Haul { pickup, to } => {
1979 let to = *to;
1980 let destination_valid = zones.is_stockpile(to)
1981 && reservations.holder(Target::Storage(to)) == Some(entity);
1982 if !destination_valid {
1983 Step::Done
1984 } else if let Some(from) = *pickup {
1985 if reservations.holder(Target::Pickup(from)) != Some(entity) {
1986 Step::Done
1987 } else if tile.0 != from {
1988 walk_to(&map, tile.0, from, &mut path)
1989 } else {
1990 let kind = items
1991 .iter()
1992 .find(|(_, _, pos)| pos.0 == from)
1993 .map(|(_, item, _)| item.kind);
1994 let room = kind.map_or(0, |kind| {
1995 room_for(items.iter().map(|(_, i, p)| (i, p)), to, kind)
1996 });
1997 match take_items(&mut commands, &mut items, from, CARRY_CAPACITY.min(room))
1998 .filter(|load| load.count > 0)
1999 {
2000 Some(load) => {
2001 carrying.0 = Some(load);
2002 reservations.release(Target::Pickup(from));
2003 *pickup = None;
2004 Step::Continue
2005 }
2006 None => Step::Done,
2007 }
2008 }
2009 } else if let Some(load) = carrying.0 {
2010 if tile.0 != to {
2011 walk_to(&map, tile.0, to, &mut path)
2012 } else {
2013 let room = room_for(items.iter().map(|(_, i, p)| (i, p)), to, load.kind);
2014 let dropped = load.count.min(room);
2015 if dropped > 0 {
2016 drop_items(
2017 &mut commands,
2018 &mut items,
2019 &item_assets,
2020 to,
2021 load.with_count(dropped),
2022 );
2023 }
2024 carrying.0 = (load.count > dropped).then_some(Item {
2025 count: load.count - dropped,
2026 ..load
2027 });
2028 Step::Done
2029 }
2030 } else {
2031 Step::Done
2032 }
2033 }
2034 };
2035 match step {
2036 Step::Continue => current.0 = Some(job),
2037 Step::Done => {
2038 reservations.release_all(entity);
2039 current.0 = None;
2040 let ordered_move = matches!(
2041 job,
2042 Job {
2043 task: Task::MoveTo(_),
2044 ordered: true
2045 }
2046 );
2047 idle.restart(if ordered_move {
2048 IDLE_AFTER_ORDER_SECS
2049 } else {
2050 rng.range_f32(WORK_PAUSE_SECS)
2051 });
2052 }
2053 }
2054 }
2055 }
2056
2057 #[cfg(test)]
2058 mod tests;