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