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