src/jobs/mod.rs 87.2 K · 2505 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 map: &WorldMap,
1124 patients: &[(Entity, UVec2)],
1125 reservations: &Reservations,
1126 reach: &ColonyReach,
1127 items: &[(Item, TilePos)],
1128 doctor: Entity,
1129 from: UVec2,
1130 ) -> Option<Task> {
1131 let &(patient, _) = patients
1132 .iter()
1133 .filter(|&&(patient, at)| {
1134 patient != doctor
1135 && reach.allows_near(at)
1136 && reservations.is_free(Target::Patient(patient))
1137 })
1138 .min_by_key(|(_, at)| {
1139 let d = at.as_ivec2() - from.as_ivec2();
1140 (d.length_squared(), at.y, at.x)
1141 })?;
1142 let herbs: HashSet<UVec2> = items
1143 .iter()
1144 .filter(|(item, pos)| {
1145 item.kind == ItemKind::Herbs && reservations.is_free(Target::Pickup(pos.0))
1146 })
1147 .map(|(_, pos)| pos.0)
1148 .collect();
1149 let pickup = (!herbs.is_empty())
1150 .then(|| reach.nearest(map, from, SEARCH_TILES, &herbs))
1151 .flatten();
1152 Some(Task::Tend {
1153 patient,
1154 pickup,
1155 progress: 0,
1156 })
1157 }
1158
1159 pub fn chilly_soon(tick: SimTick) -> bool {
1160 let later = SimTick(tick.0 + LIGHT_LOOKAHEAD_TICKS);
1161 outdoor_temperature(tick) < COLD_TEMP || outdoor_temperature(later) < COLD_TEMP
1162 }
1163
1164 pub fn find_warm_spot(
1165 map: &WorldMap,
1166 structures: &[(UVec2, StructureKind, u32)],
1167 outdoor: f32,
1168 from: UVec2,
1169 ) -> Option<UVec2> {
1170 let fires: Vec<(UVec2, StructureKind)> = structures
1171 .iter()
1172 .filter(|(_, kind, _)| kind.heat() > 0.0)
1173 .map(|&(pos, kind, _)| (pos, kind))
1174 .collect();
1175 let taken: HashSet<UVec2> = structures.iter().map(|(pos, ..)| *pos).collect();
1176 let warm: HashSet<UVec2> = fires
1177 .iter()
1178 .flat_map(|&(pos, kind)| {
1179 let r = kind.radius().ceil() as i32;
1180 (-r..=r).flat_map(move |dy| (-r..=r).map(move |dx| pos.as_ivec2() + IVec2::new(dx, dy)))
1181 })
1182 .filter(|&p| map.in_bounds(p) && map.move_cost(p).is_some())
1183 .map(|p| p.as_uvec2())
1184 .filter(|p| !taken.contains(p))
1185 .filter(|&p| tile_temperature(map, outdoor, p, fires.iter().copied()) >= WARM_TEMP)
1186 .collect();
1187 nearest_of(map, from, SEARCH_TILES, &warm)
1188 }
1189
1190 pub fn find_refuel(ctx: &BuildContext, from: UVec2) -> Option<Task> {
1191 let home = ctx.zones.home(from);
1192 let (site, kind, _) = ctx
1193 .structures
1194 .iter()
1195 .filter(|&&(pos, kind, fuel)| {
1196 kind.fuel_ticks()
1197 .is_some_and(|full| fuel < full / REFUEL_BELOW_SHARE)
1198 && (kind == StructureKind::Hearth
1199 || pos.as_vec2().distance(home.as_vec2()) <= HOME_RADIUS * 2.0)
1200 && ctx.reservations.is_free(Target::Site(pos))
1201 })
1202 .min_by_key(|(.., fuel)| *fuel)?;
1203 ctx.build_task(from, *site, BuildKind::Refuel(*kind))
1204 }
1205
1206 pub fn find_bed_site(ctx: &BuildContext, colonists: usize, from: UVec2) -> Option<Task> {
1207 if ctx.count(StructureKind::Bed) >= colonists {
1208 return None;
1209 }
1210 let anchor = ctx.bed_anchor(ctx.zones.home(from));
1211 let site = ctx.site_near_where(anchor, HOME_RADIUS * 1.5, |p| ctx.clear_of_stockpile(p))?;
1212 ctx.build_task(from, site, BuildKind::Structure(StructureKind::Bed))
1213 }
1214
1215 pub fn find_blueprint_work(ctx: &BuildContext, from: UVec2) -> Option<Task> {
1216 let mut stock: HashMap<ItemKind, u32> = HashMap::new();
1217 for (item, pos) in ctx.items {
1218 if ctx.reservations.is_free(Target::Pickup(pos.0)) {
1219 *stock.entry(item.kind).or_insert(0) += item.count;
1220 }
1221 }
1222 let mut stands: HashSet<UVec2> = HashSet::new();
1223 for (&site, &kind) in &ctx.blueprints.0 {
1224 let (material, amount) = kind.cost();
1225 let affordable = amount == 0 || stock.get(&material).copied().unwrap_or(0) >= amount;
1226 let affordable = affordable && ctx.reach.allows_near(site);
1227 if !affordable || !ctx.reservations.is_free(Target::Site(site)) {
1228 continue;
1229 }
1230 for d in std::iter::once(IVec2::ZERO).chain(NEIGHBORS) {
1231 let stand = site.as_ivec2() + d;
1232 if (d == IVec2::ZERO || ctx.map.move_cost(site.as_ivec2()).is_none())
1233 && ctx.map.move_cost(stand).is_some()
1234 {
1235 stands.insert(stand.as_uvec2());
1236 }
1237 }
1238 }
1239 let reach = search_reach(SEARCH_TILES) as i32;
1240 let in_reach = |p: &UVec2| (p.as_ivec2() - from.as_ivec2()).abs().max_element() <= reach;
1241 if !stands.iter().any(in_reach) {
1242 return None;
1243 }
1244 let mut sources: HashMap<(ItemKind, u32), Option<UVec2>> = HashMap::new();
1245 let mut found = None;
1246 nearest_tile(ctx.map, from, SEARCH_TILES, |p| {
1247 if !stands.contains(&p) {
1248 return false;
1249 }
1250 found = std::iter::once(IVec2::ZERO)
1251 .chain(NEIGHBORS)
1252 .filter_map(|d| {
1253 let site = p.as_ivec2() + d;
1254 (site.min_element() >= 0).then(|| site.as_uvec2())
1255 })
1256 .filter(|&site| site == p || ctx.map.move_cost(site.as_ivec2()).is_none())
1257 .find_map(|site| {
1258 let kind = ctx.blueprints.get(site)?;
1259 if !ctx.reservations.is_free(Target::Site(site)) {
1260 return None;
1261 }
1262 let pickup = match kind.cost() {
1263 (_, 0) => None,
1264 cost => Some(
1265 (*sources
1266 .entry(cost)
1267 .or_insert_with(|| ctx.material_source(from, cost)))?,
1268 ),
1269 };
1270 Some(Task::Build {
1271 site,
1272 kind,
1273 pickup,
1274 progress: 0,
1275 })
1276 });
1277 found.is_some()
1278 })?;
1279 found
1280 }
1281
1282 fn on_the_way(map: &WorldMap, jobs: impl Iterator<Item = Option<Job>>) -> HashMap<ItemKind, u32> {
1283 let mut totals = HashMap::new();
1284 for job in jobs.flatten() {
1285 if let Task::Work { site, .. } = job.task
1286 && let Some(work) = site_work(map, site)
1287 {
1288 *totals.entry(work.item).or_insert(0) += work.count;
1289 }
1290 }
1291 totals
1292 }
1293
1294 #[allow(clippy::too_many_arguments)]
1295 fn choose_jobs(
1296 mut commands: Commands,
1297 time: Res<Time>,
1298 map: Res<WorldMap>,
1299 zones: Res<Zones>,
1300 goals: Res<ColonyGoals>,
1301 item_assets: Res<ItemAssets>,
1302 tick: Res<SimTick>,
1303 blueprints: Res<Blueprints>,
1304 mut rng: ResMut<SimRng>,
1305 mut reservations: ResMut<Reservations>,
1306 mut items: ItemQuery,
1307 structures: Query<(&TilePos, &Structure, Option<&Fuel>), Without<Colonist>>,
1308 animals: Query<(Entity, &TilePos, &Animal, Has<Tame>), Without<Colonist>>,
1309 mut colonists: Query<
1310 (
1311 Entity,
1312 &TilePos,
1313 &mut Path,
1314 &mut CurrentJob,
1315 &mut IdleTimer,
1316 &mut Carrying,
1317 &Rest,
1318 &Hunger,
1319 Option<&Family>,
1320 Has<Child>,
1321 &WorkPriorities,
1322 &Warmth,
1323 Has<Tool>,
1324 &Skills,
1325 &Health,
1326 ),
1327 With<Colonist>,
1328 >,
1329 plans: (
1330 Res<Homes>,
1331 Res<Designations>,
1332 Res<ColonyReach>,
1333 Query<(Entity, &TilePos, &Health, Option<&Tended>), With<Colonist>>,
1334 ),
1335 mut gather_memo: Local<GatherMemo>,
1336 ) {
1337 let (homes, designations, reach, patient_query) = plans;
1338 let patients: Vec<(Entity, UVec2)> = patient_query
1339 .iter()
1340 .filter(|(_, _, health, tended)| needs_tending(health.0, *tended, tick.0))
1341 .map(|(entity, pos, ..)| (entity, pos.0))
1342 .collect();
1343 if zones.is_changed() {
1344 gather_memo.haul = Memo::default();
1345 }
1346 if blueprints.is_changed() {
1347 gather_memo.build = Memo::default();
1348 }
1349 gather_memo.food.retain(|_, until| tick.0 < *until);
1350 let wolves: Vec<(Entity, UVec2)> = animals
1351 .iter()
1352 .filter(|(_, _, animal, _)| animal.kind.is_predator())
1353 .map(|(entity, pos, ..)| (entity, pos.0))
1354 .collect();
1355 let nearest_wolf = |tile: UVec2, range: i32| {
1356 wolves
1357 .iter()
1358 .map(|&(entity, pos)| (entity, pos, chebyshev(pos, tile)))
1359 .filter(|&(.., d)| d <= range)
1360 .min_by_key(|&(_, pos, d)| (d, pos.y, pos.x))
1361 };
1362 let mut anyone_ready = false;
1363 for (_, tile, path, job, mut idle, _, rest, hunger, ..) in &mut colonists {
1364 let idle_now = job.0.is_none() && path.0.is_empty();
1365 if idle_now {
1366 idle.0.tick(time.delta());
1367 }
1368 anyone_ready |= (idle_now && idle.0.is_finished())
1369 || rest.0 < EXHAUSTED_REST
1370 || hunger.0 < STARVING
1371 || nearest_wolf(tile.0, DANGER_RANGE).is_some();
1372 }
1373 if !anyone_ready {
1374 return;
1375 }
1376 let wildlife: Vec<Wildlife> = animals
1377 .iter()
1378 .map(|(e, pos, a, tame)| (e, pos.0, a.kind, tame))
1379 .collect();
1380 let adults = colonists.iter().filter(|c| !c.9).count();
1381 let structures_list: Vec<(UVec2, StructureKind, u32)> = structures
1382 .iter()
1383 .map(|(pos, s, fuel)| (pos.0, s.kind, fuel.map_or(u32::MAX, |f| f.0)))
1384 .collect();
1385 let mut planned: Vec<(UVec2, StructureKind)> = colonists
1386 .iter()
1387 .filter_map(|c| match c.3.0?.task {
1388 Task::Build {
1389 site,
1390 kind: BuildKind::Structure(kind),
1391 ..
1392 } => Some((site, kind)),
1393 _ => None,
1394 })
1395 .collect();
1396 let dark = dark_soon(*tick);
1397 let chilly = chilly_soon(*tick);
1398 let outdoor = outdoor_temperature(*tick);
1399 let sowing = sowing_season(*tick);
1400 let carried: Vec<Item> = colonists.iter().filter_map(|c| c.5.0).collect();
1401 let home = zones.home(map.size / 2);
1402 let mut stocks: HashMap<ItemKind, u32> = ItemKind::ALL.iter().map(|&k| (k, 0)).collect();
1403 let in_store = items
1404 .iter()
1405 .filter(|(_, _, pos)| in_colony(home, pos.0))
1406 .map(|(_, item, _)| item)
1407 .chain(carried.iter());
1408 for item in in_store {
1409 *stocks.entry(item.kind).or_insert(0) += item.count;
1410 }
1411 let mut pending = on_the_way(&map, colonists.iter().map(|c| c.3.0));
1412 for job in colonists.iter().filter_map(|c| c.3.0) {
1413 match job.task {
1414 Task::Hunt { kind, .. } => *pending.entry(ItemKind::Meat).or_insert(0) += kind.meat(),
1415 Task::Craft { recipe, .. } => *pending.entry(recipe.output()).or_insert(0) += 1,
1416 _ => {}
1417 }
1418 }
1419 let mut food_tiles: Option<HashSet<UVec2>> = None;
1420 let hour = hour_of_day(*tick);
1421 let mut occupied: Option<HashSet<UVec2>> = None;
1422 for (
1423 entity,
1424 tile,
1425 mut path,
1426 mut job,
1427 mut idle,
1428 mut carrying,
1429 rest,
1430 hunger,
1431 family,
1432 child,
1433 priorities,
1434 warmth,
1435 has_tool,
1436 skills,
1437 health,
1438 ) in &mut colonists
1439 {
1440 let exhausted = rest.0 < EXHAUSTED_REST;
1441 let skip_food = gather_memo
1442 .food
1443 .get(&entity)
1444 .is_some_and(|&until| tick.0 < until);
1445 let starving = hunger.0 < STARVING && !skip_food && {
1446 let tiles = food_tiles.get_or_insert_with(|| {
1447 items
1448 .iter()
1449 .filter(|(_, item, _)| item.kind.is_food())
1450 .map(|(_, _, pos)| pos.0)
1451 .collect()
1452 });
1453 food_to_fetch(&reservations, tiles)
1454 };
1455 let danger = nearest_wolf(tile.0, DANGER_RANGE);
1456 let urgent = exhausted || starving || danger.is_some();
1457 let fighting_wolf = |task: Task| {
1458 matches!(task, Task::Hunt { kind, .. } if kind.is_predator())
1459 || matches!(task, Task::MoveTo(_))
1460 };
1461 let interrupt = |j: Job| {
1462 !j.ordered
1463 && if danger.is_some() {
1464 !fighting_wolf(j.task)
1465 } else {
1466 !matches!(j.task, Task::Sleep { .. } | Task::Eat { .. })
1467 }
1468 };
1469 if urgent && job.0.is_some_and(interrupt) {
1470 reservations.release_all(entity);
1471 job.0 = None;
1472 path.0.truncate(1);
1473 }
1474 let rested = idle.0.is_finished();
1475 if job.0.is_some() || !path.0.is_empty() || !(rested || urgent) {
1476 continue;
1477 }
1478 let snapshot: Vec<(Item, TilePos)> =
1479 items.iter().map(|(_, item, pos)| (*item, *pos)).collect();
1480 let refs = snapshot.iter().map(|(i, p)| (i, p));
1481 let wanted_kinds = ItemKind::ALL.map(|kind| goals.wanted(kind, &stocks, &pending));
1482 let wanted = |kind: ItemKind| wanted_kinds[kind.index()];
1483 let sleep = SleepContext {
1484 map: &map,
1485 zones: &zones,
1486 reservations: &reservations,
1487 items: &snapshot,
1488 structures: &structures_list,
1489 blueprints: &blueprints,
1490 homes: &homes,
1491 family: family.copied(),
1492 child,
1493 };
1494 let food = (wants_food(hunger.0) && !skip_food)
1495 .then(|| find_food(&map, &reservations, &reach, &snapshot, tile.0))
1496 .flatten();
1497 if wants_food(hunger.0) && !skip_food && food.is_none() {
1498 gather_memo.food.insert(entity, tick.0 + FOOD_RETRY_TICKS);
1499 }
1500 let fighter = !child && health.0 >= INJURED && priorities.allows(WorkType::Food);
1501 let free_to_fight = fighter && !exhausted && !wants_food(hunger.0);
1502 let defend =
1503 danger
1504 .or_else(|| nearest_wolf(home, DEFEND_RANGE))
1505 .filter(|&(wolf, pos, _)| {
1506 free_to_fight
1507 && reach.allows_near(pos)
1508 && reservations.is_free(Target::Animal(wolf))
1509 });
1510 let flee = danger
1511 .filter(|_| !fighter)
1512 .and_then(|(_, wolf, _)| flee_from(&map, tile.0, wolf));
1513 let task = if let Some(route) = flee {
1514 let goal = route.last().copied().unwrap_or(tile.0);
1515 path.0 = route.into();
1516 Some(Task::MoveTo(goal))
1517 } else if let Some((wolf, ..)) = defend {
1518 Some(Task::Hunt {
1519 animal: wolf,
1520 kind: AnimalKind::Wolf,
1521 progress: 0,
1522 })
1523 } else if exhausted {
1524 Some(Task::Sleep {
1525 spot: sleep.spot(tile.0),
1526 })
1527 } else if let Some(pickup) = food {
1528 if let Some(load) = carrying.0.take() {
1529 drop_items(&mut commands, &mut items, &item_assets, tile.0, load);
1530 }
1531 Some(Task::Eat {
1532 pickup: Some(pickup),
1533 progress: 0,
1534 })
1535 } else if wants_sleep(rest.0, hour) || health.0 < INJURED {
1536 Some(Task::Sleep {
1537 spot: sleep.spot(tile.0),
1538 })
1539 } else if let Some(spot) = warmth
1540 .cold()
1541 .then(|| find_warm_spot(&map, &structures_list, outdoor, tile.0))
1542 .flatten()
1543 .filter(|&spot| spot != tile.0)
1544 {
1545 find_path(&map, tile.0, spot).map(|route| {
1546 path.0 = route.into();
1547 Task::MoveTo(spot)
1548 })
1549 } else if child {
1550 None
1551 } else if let Some(load) = carrying.0 {
1552 let storage =
1553 find_storage(&map, &zones, &reservations, &reach, refs, tile.0, load.kind);
1554 if storage.is_none() {
1555 drop_items(&mut commands, &mut items, &item_assets, tile.0, load);
1556 carrying.0 = None;
1557 }
1558 storage.map(|to| Task::Haul { pickup: None, to })
1559 } else if let Some(pickup) = (!has_tool)
1560 .then(|| tool_source(&map, &reservations, &snapshot, tile.0))
1561 .flatten()
1562 {
1563 Some(Task::Equip { pickup })
1564 } else {
1565 let occupied = occupied.get_or_insert_with(|| {
1566 structures_list
1567 .iter()
1568 .map(|(pos, ..)| *pos)
1569 .chain(items.iter().map(|(_, _, pos)| pos.0))
1570 .collect()
1571 });
1572 occupied.extend(planned.iter().map(|(pos, _)| *pos));
1573 let build = BuildContext {
1574 reach: &reach,
1575 map: &map,
1576 zones: &zones,
1577 reservations: &reservations,
1578 items: &snapshot,
1579 structures: &structures_list,
1580 planned: &planned,
1581 blueprints: &blueprints,
1582 occupied,
1583 };
1584 let has_input = |recipe| input_source(&build, tile.0, recipe).is_some();
1585 let own_home = homes.of(family.copied());
1586 let cooking = || {
1587 if !wanted(ItemKind::Meal) {
1588 return None;
1589 }
1590 find_crafting(&build, tile.0, Recipe::Meal, own_home).or_else(|| {
1591 (build.campfire_needed(zones.home(tile.0)) && has_input(Recipe::Meal))
1592 .then(|| find_campfire(&build, tile.0))
1593 .flatten()
1594 })
1595 };
1596 let toolmaking = || {
1597 if !wanted(ItemKind::Tools) {
1598 return None;
1599 }
1600 find_crafting(&build, tile.0, Recipe::Tools, None).or_else(|| {
1601 (build.count(StructureKind::Workbench) == 0 && has_input(Recipe::Tools))
1602 .then(|| find_workbench(&build, tile.0))
1603 .flatten()
1604 })
1605 };
1606 let designated = || {
1607 (!designations.0.is_empty())
1608 .then(|| {
1609 find_work(&map, &reservations, tile.0, |site, _| {
1610 designations.0.contains(&site)
1611 })
1612 })
1613 .flatten()
1614 };
1615 let gather_mask = ItemKind::ALL
1616 .iter()
1617 .filter(|&&k| wanted(k))
1618 .fold(0u32, |mask, k| mask | 1 << k.index());
1619 let food_mask = gather_mask
1620 & ItemKind::ALL
1621 .iter()
1622 .filter(|k| k.is_food())
1623 .fold(0u32, |mask, k| mask | 1 << k.index());
1624 let skip_gather = gather_memo.gather.skips(gather_mask, tick.0);
1625 let skip_forage = gather_memo.forage.skips(food_mask, tick.0);
1626 let skip_haul = gather_memo.haul.skips(0, tick.0);
1627 let skip_build = gather_memo.build.skips(0, tick.0);
1628 let build_failed = std::cell::Cell::new(false);
1629 let gather_failed = std::cell::Cell::new(false);
1630 let forage_failed = std::cell::Cell::new(false);
1631 let haul_failed = std::cell::Cell::new(false);
1632 let candidates: [(WorkType, Option<Skill>, &dyn Fn() -> Option<Task>); 17] = [
1633 (WorkType::Haul, None, &|| {
1634 find_expansion(&map, &zones, &reservations, &snapshot, &blueprints)
1635 }),
1636 (WorkType::Build, None, &|| find_refuel(&build, tile.0)),
1637 (WorkType::Doctor, Some(Skill::Medicine), &|| {
1638 find_tending(
1639 &map,
1640 &patients,
1641 &reservations,
1642 &reach,
1643 &snapshot,
1644 entity,
1645 tile.0,
1646 )
1647 }),
1648 (WorkType::Build, None, &|| {
1649 (dark || chilly)
1650 .then(|| find_lighting(&build, tile.0, dark))
1651 .flatten()
1652 }),
1653 (WorkType::Food, Some(Skill::Cooking), &cooking),
1654 (WorkType::Food, None, &|| {
1655 find_butchering(&map, &reservations, &snapshot, tile.0)
1656 }),
1657 (WorkType::Food, None, &|| {
1658 if food_mask == 0 || skip_forage {
1659 return None;
1660 }
1661 let food = |_, k: ItemKind| k.is_food() && wanted(k);
1662 let work =
1663 find_work_within(&map, &reservations, tile.0, FORAGE_SEARCH_TILES, food);
1664 forage_failed.set(work.is_none());
1665 work
1666 }),
1667 (WorkType::Gather, None, &designated),
1668 (WorkType::Food, None, &|| {
1669 (sowing && wanted(ItemKind::Vegetables))
1670 .then(|| find_sowing(&map, &zones, &reservations, tile.0))
1671 .flatten()
1672 }),
1673 (WorkType::Build, Some(Skill::Construction), &|| {
1674 find_bed_site(&build, adults, tile.0)
1675 }),
1676 (WorkType::Craft, Some(Skill::Crafting), &toolmaking),
1677 (WorkType::Build, Some(Skill::Construction), &|| {
1678 if skip_build {
1679 return None;
1680 }
1681 let work = find_blueprint_work(&build, tile.0);
1682 build_failed.set(work.is_none());
1683 work
1684 }),
1685 (WorkType::Gather, None, &|| {
1686 if gather_mask == 0 || skip_gather {
1687 return None;
1688 }
1689 let work = find_work(&map, &reservations, tile.0, |_, k| wanted(k));
1690 gather_failed.set(work.is_none());
1691 work
1692 }),
1693 (WorkType::Food, Some(Skill::Hunting), &|| {
1694 wanted(ItemKind::Meat)
1695 .then(|| find_hunt(&map, &reservations, &wildlife, tile.0))
1696 .flatten()
1697 }),
1698 (WorkType::Food, Some(Skill::Farming), &|| {
1699 sowing
1700 .then(|| find_sowing(&map, &zones, &reservations, tile.0))
1701 .flatten()
1702 }),
1703 (WorkType::Tame, Some(Skill::Taming), &|| {
1704 (!wanted(ItemKind::Meat))
1705 .then(|| find_taming(&map, &reservations, &wildlife, tile.0))
1706 .flatten()
1707 }),
1708 (WorkType::Haul, None, &|| {
1709 if skip_haul {
1710 return None;
1711 }
1712 let haul = find_haul(
1713 &map,
1714 &zones,
1715 &reservations,
1716 &reach,
1717 &homes,
1718 &snapshot,
1719 tile.0,
1720 );
1721 haul_failed.set(haul.is_none());
1722 haul
1723 }),
1724 ];
1725 let poor = |skill: &Option<Skill>| skill.is_some_and(|s| skills.level(s) < POOR_LEVEL);
1726 let allowed = candidates
1727 .iter()
1728 .filter(|(work, ..)| priorities.allows(*work));
1729 let task = allowed
1730 .clone()
1731 .filter(|(_, skill, _)| !poor(skill))
1732 .chain(allowed.filter(|(_, skill, _)| poor(skill)))
1733 .find_map(|(.., find)| find());
1734 let retry = |mask, ticks| Memo {
1735 mask,
1736 until: tick.0 + ticks,
1737 };
1738 if gather_failed.get() {
1739 gather_memo.gather = retry(gather_mask, GATHER_RETRY_TICKS);
1740 }
1741 if forage_failed.get() {
1742 gather_memo.forage = retry(food_mask, GATHER_RETRY_TICKS);
1743 }
1744 if haul_failed.get() {
1745 gather_memo.haul = retry(0, HAUL_RETRY_TICKS);
1746 }
1747 if build_failed.get() {
1748 gather_memo.build = retry(0, HAUL_RETRY_TICKS);
1749 }
1750 task
1751 };
1752 if let Some(task) = task {
1753 if let Task::Work { site, .. } = task
1754 && let Some(work) = site_work(&map, site)
1755 {
1756 *pending.entry(work.item).or_insert(0) += work.count;
1757 }
1758 match task {
1759 Task::Hunt { kind, .. } => {
1760 *pending.entry(ItemKind::Meat).or_insert(0) += kind.meat()
1761 }
1762 Task::Craft { recipe, .. } => *pending.entry(recipe.output()).or_insert(0) += 1,
1763 _ => {}
1764 }
1765 if let Task::Build {
1766 site,
1767 kind: BuildKind::Structure(kind),
1768 ..
1769 } = task
1770 {
1771 planned.push((site, kind));
1772 }
1773 reserve_task(&mut reservations, task, entity);
1774 job.0 = Some(Job {
1775 task,
1776 ordered: false,
1777 });
1778 continue;
1779 }
1780 let goal = tile.0.as_ivec2()
1781 + IVec2::new(
1782 rng.range_i32(-WANDER_RADIUS..WANDER_RADIUS + 1),
1783 rng.range_i32(-WANDER_RADIUS..WANDER_RADIUS + 1),
1784 );
1785 if map.in_bounds(goal)
1786 && let Some(wander_path) =
1787 find_nearby_path(&map, tile.0, goal.as_uvec2(), WANDER_SEARCH)
1788 {
1789 path.0 = wander_path.into();
1790 }
1791 idle.restart(rng.range_f32(IDLE_SECS));
1792 }
1793 }
1794
1795 enum Step {
1796 Continue,
1797 Done,
1798 }
1799
1800 fn walk_to(map: &WorldMap, from: UVec2, to: UVec2, path: &mut Path) -> Step {
1801 match find_path(map, from, to) {
1802 Some(p) if !p.is_empty() => {
1803 path.0 = p.into();
1804 Step::Continue
1805 }
1806 _ => Step::Done,
1807 }
1808 }
1809
1810 #[allow(clippy::too_many_arguments)]
1811 fn run_jobs(
1812 mut commands: Commands,
1813 mut map: ResMut<WorldMap>,
1814 mut zones: ResMut<Zones>,
1815 mut reservations: ResMut<Reservations>,
1816 mut rng: ResMut<SimRng>,
1817 item_assets: Res<ItemAssets>,
1818 structure_assets: Res<StructureAssets>,
1819 light: Res<LightMap>,
1820 tick: Res<SimTick>,
1821 mut blueprints: ResMut<Blueprints>,
1822 mut goals: ResMut<ColonyGoals>,
1823 mut items: ItemQuery,
1824 mut structures: Query<(Entity, &TilePos, &Structure, Option<&mut Fuel>), Without<Colonist>>,
1825 animals: Query<(&TilePos, &Path), (With<Animal>, Without<Colonist>)>,
1826 messages: (MessageWriter<TileChanged>, MessageWriter<Ate>),
1827 mut colonists: Query<
1828 (
1829 Entity,
1830 &TilePos,
1831 &mut Path,
1832 &mut CurrentJob,
1833 &mut IdleTimer,
1834 &mut Carrying,
1835 &Rest,
1836 &mut Hunger,
1837 Has<Sick>,
1838 Option<&mut Tool>,
1839 (&mut Skills, &mut WorkCarry, &Health),
1840 ),
1841 With<Colonist>,
1842 >,
1843 ) {
1844 let (mut changed, mut ate) = messages;
1845 let hour = hour_of_day(*tick);
1846 let positions: HashMap<Entity, UVec2> = colonists.iter().map(|c| (c.0, c.1.0)).collect();
1847 let walling = colonists.iter().any(|c| {
1848 c.3.0
1849 .is_some_and(|j| matches!(j.task, Task::Build { kind, .. } if kind.blocks_tile()))
1850 });
1851 let occupied: Vec<UVec2> = colonists
1852 .iter()
1853 .filter(|_| walling)
1854 .flat_map(|c| [Some(c.1.0), c.2.0.front().copied()])
1855 .chain(
1856 animals
1857 .iter()
1858 .filter(|_| walling)
1859 .flat_map(|(t, p)| [Some(t.0), p.0.front().copied()]),
1860 )
1861 .flatten()
1862 .collect();
1863 for (
1864 entity,
1865 tile,
1866 mut path,
1867 mut current,
1868 mut idle,
1869 mut carrying,
1870 rest,
1871 mut hunger,
1872 sick,
1873 mut tool,
1874 (mut skills, mut carry, health),
1875 ) in &mut colonists
1876 {
1877 let Some(mut job) = current.0 else {
1878 continue;
1879 };
1880 let rate = match work_rate(&light, tile.0) {
1881 rate if sick => rate.div_ceil(2),
1882 rate => rate,
1883 };
1884 let rate = tool_rate(rate, tool.is_some());
1885 if !path.0.is_empty() {
1886 continue;
1887 }
1888 let skill = Skill::of(&job.task);
1889 let rate = carry.apply(rate, skill.map_or(1.0, |s| speed(skills.level(s))));
1890 let progress_before = job.task.progress();
1891 if let Some(tool) = tool.as_mut()
1892 && !matches!(
1893 job.task,
1894 Task::MoveTo(_) | Task::Sleep { .. } | Task::Eat { .. } | Task::Equip { .. }
1895 )
1896 {
1897 tool.wear += 1;
1898 if tool.wear >= TOOL_LIFE_TICKS {
1899 commands.entity(entity).remove::<Tool>();
1900 }
1901 }
1902 let step = match &mut job.task {
1903 Task::MoveTo(_) => Step::Done,
1904 Task::Tend {
1905 patient,
1906 pickup,
1907 progress,
1908 } => {
1909 let patient = *patient;
1910 let ours = reservations.holder(Target::Patient(patient)) == Some(entity);
1911 match positions.get(&patient) {
1912 _ if !ours => Step::Done,
1913 None => Step::Done,
1914 Some(_) if pickup.is_some() => {
1915 let from = pickup.expect("checked above");
1916 if tile.0 != from {
1917 walk_to(&map, tile.0, from, &mut path)
1918 } else {
1919 reservations.release(Target::Pickup(from));
1920 carrying.0 = take_items(&mut commands, &mut items, from, 1)
1921 .filter(|load| load.kind == ItemKind::Herbs && load.count > 0);
1922 *pickup = None;
1923 Step::Continue
1924 }
1925 }
1926 Some(&at) if at != tile.0 && !is_adjacent(tile.0.as_ivec2(), at.as_ivec2()) => {
1927 match find_path_adjacent(&map, tile.0, at) {
1928 Some(p) if !p.is_empty() => {
1929 path.0 = p.into();
1930 Step::Continue
1931 }
1932 _ => Step::Done,
1933 }
1934 }
1935 Some(_) => {
1936 *progress += rate;
1937 if *progress < TEND_WORK {
1938 Step::Continue
1939 } else {
1940 let herb = carrying.0.is_some_and(|l| l.kind == ItemKind::Herbs);
1941 if herb {
1942 goals.record_use(ItemKind::Herbs, 1);
1943 carrying.0 = None;
1944 }
1945 let speed = speed(skills.level(Skill::Medicine));
1946 commands.entity(patient).insert(Tended {
1947 until: tick.0 + TENDED_TICKS,
1948 quality: tend_quality(speed, herb),
1949 });
1950 Step::Done
1951 }
1952 }
1953 }
1954 }
1955 Task::Equip { pickup } => {
1956 let from = *pickup;
1957 if reservations.holder(Target::Pickup(from)) != Some(entity) {
1958 Step::Done
1959 } else if tile.0 != from {
1960 walk_to(&map, tile.0, from, &mut path)
1961 } else {
1962 reservations.release(Target::Pickup(from));
1963 let stack = items
1964 .iter()
1965 .any(|(_, item, pos)| pos.0 == from && item.kind == ItemKind::Tools);
1966 if stack && take_items(&mut commands, &mut items, from, 1).is_some() {
1967 goals.record_use(ItemKind::Tools, 1);
1968 commands.entity(entity).insert(Tool::default());
1969 }
1970 Step::Done
1971 }
1972 }
1973 Task::Craft {
1974 recipe,
1975 pickup,
1976 station,
1977 progress,
1978 } => {
1979 let (recipe, station) = (*recipe, *station);
1980 let input = recipe.input();
1981 let fire = structures
1982 .iter()
1983 .any(|(_, pos, s, _)| pos.0 == station && recipe.stations().contains(&s.kind));
1984 let ours = reservations.holder(Target::Station(station)) == Some(entity);
1985 if !fire || !ours {
1986 Step::Done
1987 } else if let Some(from) = *pickup {
1988 if reservations.holder(Target::Pickup(from)) != Some(entity) {
1989 Step::Done
1990 } else if tile.0 != from {
1991 walk_to(&map, tile.0, from, &mut path)
1992 } else {
1993 let load = take_items(&mut commands, &mut items, from, input)
1994 .filter(|load| recipe.accepts(load.kind) && load.count > 0);
1995 carrying.0 = load;
1996 reservations.release(Target::Pickup(from));
1997 match load {
1998 Some(load) if load.count >= input => {
1999 *pickup = None;
2000 Step::Continue
2001 }
2002 _ => Step::Done,
2003 }
2004 }
2005 } else if !carrying
2006 .0
2007 .is_some_and(|load| recipe.accepts(load.kind) && load.count >= input)
2008 {
2009 Step::Done
2010 } else if !is_adjacent(tile.0.as_ivec2(), station.as_ivec2()) {
2011 match find_path_adjacent(&map, tile.0, station) {
2012 Some(p) if !p.is_empty() => {
2013 path.0 = p.into();
2014 Step::Continue
2015 }
2016 _ => Step::Done,
2017 }
2018 } else {
2019 *progress += rate;
2020 if *progress < recipe.work() {
2021 Step::Continue
2022 } else {
2023 if let Some(load) = carrying.0 {
2024 goals.record_use(load.kind, input);
2025 }
2026 carrying.0 = carrying.0.and_then(|load| {
2027 (load.count > input).then_some(Item {
2028 count: load.count - input,
2029 ..load
2030 })
2031 });
2032 drop_items(
2033 &mut commands,
2034 &mut items,
2035 &item_assets,
2036 tile.0,
2037 Item::new(recipe.output(), 1),
2038 );
2039 Step::Done
2040 }
2041 }
2042 }
2043 Task::Hunt {
2044 animal,
2045 kind,
2046 progress,
2047 } => match animals.get(*animal) {
2048 Ok((prey, _)) if reservations.holder(Target::Animal(*animal)) == Some(entity) => {
2049 let prey = prey.0;
2050 let in_range = tile.0.as_vec2().distance(prey.as_vec2()) <= HUNT_RANGE
2051 && line_of_sight(&map, tile.0, prey);
2052 let stale = path.0.back().is_none_or(|end| {
2053 end.as_vec2().distance(prey.as_vec2()) > HUNT_RANGE / 2.0
2054 });
2055 if !in_range && !stale {
2056 Step::Continue
2057 } else if !in_range {
2058 match find_path_adjacent(&map, tile.0, prey) {
2059 Some(p) if !p.is_empty() => {
2060 path.0 = p.into();
2061 Step::Continue
2062 }
2063 _ => Step::Done,
2064 }
2065 } else {
2066 path.0.truncate(1);
2067 *progress += rate;
2068 if *progress < kind.hunt_work() {
2069 Step::Continue
2070 } else {
2071 commands.entity(*animal).despawn();
2072 drop_items(
2073 &mut commands,
2074 &mut items,
2075 &item_assets,
2076 prey,
2077 Item::new(ItemKind::Carcass, kind.meat()),
2078 );
2079 Step::Done
2080 }
2081 }
2082 }
2083 _ => Step::Done,
2084 },
2085 Task::Tame {
2086 animal,
2087 kind,
2088 progress,
2089 } => match animals.get(*animal) {
2090 Ok((prey, _)) if reservations.holder(Target::Animal(*animal)) == Some(entity) => {
2091 let prey = prey.0;
2092 if prey != tile.0 && !is_adjacent(tile.0.as_ivec2(), prey.as_ivec2()) {
2093 if path.0.is_empty() {
2094 match find_path_adjacent(&map, tile.0, prey) {
2095 Some(p) if !p.is_empty() => {
2096 path.0 = p.into();
2097 Step::Continue
2098 }
2099 _ => Step::Done,
2100 }
2101 } else {
2102 Step::Continue
2103 }
2104 } else {
2105 *progress += rate;
2106 if *progress < kind.tame_work() {
2107 Step::Continue
2108 } else {
2109 commands.entity(*animal).insert(Tame);
2110 Step::Done
2111 }
2112 }
2113 }
2114 _ => Step::Done,
2115 },
2116 Task::Butcher { site, progress } => {
2117 let site = *site;
2118 if reservations.holder(Target::Pickup(site)) != Some(entity) {
2119 Step::Done
2120 } else if tile.0 != site {
2121 walk_to(&map, tile.0, site, &mut path)
2122 } else {
2123 *progress += rate;
2124 if *progress < BUTCHER_WORK {
2125 Step::Continue
2126 } else {
2127 let is_carcass = items
2128 .iter()
2129 .any(|(_, item, pos)| pos.0 == site && item.kind == ItemKind::Carcass);
2130 if is_carcass
2131 && let Some(carcass) =
2132 take_items(&mut commands, &mut items, site, u32::MAX)
2133 {
2134 let meat = Item {
2135 kind: ItemKind::Meat,
2136 ..carcass
2137 };
2138 drop_items(&mut commands, &mut items, &item_assets, site, meat);
2139 }
2140 Step::Done
2141 }
2142 }
2143 }
2144 Task::Eat { pickup, progress } => {
2145 if let Some(from) = *pickup {
2146 if reservations.holder(Target::Pickup(from)) != Some(entity) {
2147 Step::Done
2148 } else if tile.0 != from {
2149 walk_to(&map, tile.0, from, &mut path)
2150 } else {
2151 let kind = items
2152 .iter()
2153 .find(|(_, item, pos)| pos.0 == from && item.kind.is_food())
2154 .map(|(_, item, _)| item.kind);
2155 let meal = kind.and_then(|kind| {
2156 take_items(&mut commands, &mut items, from, portion(kind, hunger.0))
2157 });
2158 reservations.release(Target::Pickup(from));
2159 match meal.filter(|m| m.kind.is_food()) {
2160 Some(meal) => {
2161 carrying.0 = Some(meal);
2162 *pickup = None;
2163 Step::Continue
2164 }
2165 None => Step::Done,
2166 }
2167 }
2168 } else if let Some(meal) = carrying.0.filter(|m| m.kind.is_food()) {
2169 *progress += 1;
2170 if *progress < EAT_TICKS {
2171 Step::Continue
2172 } else {
2173 hunger.0 = (hunger.0 + meal.count as f32 * meal.kind.nutrition()).min(1.0);
2174 goals.record_use(meal.kind, meal.count);
2175 ate.write(Ate {
2176 colonist: entity,
2177 kind: meal.kind,
2178 });
2179 carrying.0 = None;
2180 Step::Done
2181 }
2182 } else {
2183 Step::Done
2184 }
2185 }
2186 Task::Sleep { spot } => {
2187 if reservations.holder(Target::SleepSpot(*spot)) != Some(entity) {
2188 Step::Done
2189 } else if tile.0 != *spot {
2190 let step = walk_to(&map, tile.0, *spot, &mut path);
2191 if matches!(step, Step::Done) {
2192 reservations.release(Target::SleepSpot(*spot));
2193 *spot = tile.0;
2194 reservations.reserve(Target::SleepSpot(*spot), entity);
2195 }
2196 Step::Continue
2197 } else if done_sleeping(rest.0, hour) && health.0 >= INJURED {
2198 Step::Done
2199 } else {
2200 Step::Continue
2201 }
2202 }
2203 Task::Work {
2204 site,
2205 kind,
2206 progress,
2207 } => {
2208 let (site, kind) = (*site, *kind);
2209 let work = site_work(&map, site).filter(|w| w.kind == kind);
2210 let valid =
2211 work.is_some() || (kind == WorkKind::Sow && sowable(&map, &zones, site));
2212 let ours = reservations.holder(Target::Site(site)) == Some(entity);
2213 if !valid || !ours {
2214 Step::Done
2215 } else if !is_adjacent(tile.0.as_ivec2(), site.as_ivec2()) {
2216 match find_path_adjacent(&map, tile.0, site) {
2217 Some(p) if !p.is_empty() => {
2218 path.0 = p.into();
2219 Step::Continue
2220 }
2221 _ => Step::Done,
2222 }
2223 } else {
2224 *progress += rate;
2225 if *progress < kind.work() {
2226 Step::Continue
2227 } else {
2228 kind.complete(&mut map, site);
2229 changed.write(TileChanged(site));
2230 let step = site.as_ivec2() - tile.0.as_ivec2();
2231 let drop_at = if can_step(&map, tile.0.as_ivec2(), step) {
2232 site
2233 } else {
2234 tile.0
2235 };
2236 if let Some(work) = work {
2237 drop_items(
2238 &mut commands,
2239 &mut items,
2240 &item_assets,
2241 drop_at,
2242 Item::new(work.item, work.count),
2243 );
2244 }
2245 Step::Done
2246 }
2247 }
2248 }
2249 Task::ExpandStockpile { site, progress } => {
2250 if tile.0 != *site {
2251 walk_to(&map, tile.0, *site, &mut path)
2252 } else {
2253 *progress += rate;
2254 if *progress >= EXPAND_WORK {
2255 if zones.has_stockpile() {
2256 let excluded = blueprints.tiles();
2257 let tiles = plan_expansion(&map, &zones, &excluded, EXPANSION_TILES);
2258 zones.add_tiles(&map, tiles);
2259 } else {
2260 zones.add_tiles(&map, new_stockpile_tiles(*site));
2261 }
2262 Step::Done
2263 } else {
2264 Step::Continue
2265 }
2266 }
2267 }
2268 Task::Build {
2269 site,
2270 kind,
2271 pickup,
2272 progress,
2273 } => {
2274 let (site, kind) = (*site, *kind);
2275 let ours = reservations.holder(Target::Site(site)) == Some(entity);
2276 let planned = match kind {
2277 BuildKind::Structure(
2278 StructureKind::Campfire
2279 | StructureKind::Torch
2280 | StructureKind::Bed
2281 | StructureKind::Workbench,
2282 )
2283 | BuildKind::Refuel(_) => true,
2284 _ => blueprints.get(site) == Some(kind),
2285 };
2286 let structure_there = structures.iter().any(|(_, pos, ..)| pos.0 == site);
2287 let blocked = map.plant(site.as_ivec2()).is_some()
2288 || match kind {
2289 BuildKind::Roof | BuildKind::Floor => false,
2290 BuildKind::Refuel(fire) => !structures
2291 .iter()
2292 .any(|(_, pos, s, _)| pos.0 == site && s.kind == fire),
2293 BuildKind::Deconstruct => {
2294 !structure_there && !map.is_roofed(site.as_ivec2())
2295 }
2296 _ => structure_there,
2297 };
2298 let (material, cost) = kind.cost();
2299 if !ours || blocked || !planned {
2300 Step::Done
2301 } else if let Some(from) = *pickup {
2302 if reservations.holder(Target::Pickup(from)) != Some(entity) {
2303 Step::Done
2304 } else if tile.0 != from {
2305 walk_to(&map, tile.0, from, &mut path)
2306 } else if carrying.0.is_some_and(|load| load.kind != material) {
2307 Step::Done
2308 } else {
2309 let have = carrying.0.map_or(0, |load| load.count);
2310 let taken = take_items(&mut commands, &mut items, from, cost - have)
2311 .filter(|load| load.kind == material)
2312 .map_or(0, |load| load.count);
2313 reservations.release(Target::Pickup(from));
2314 let total = have + taken;
2315 carrying.0 = (total > 0).then_some(Item::new(material, total));
2316 let next = nearest_tile(&map, tile.0, SEARCH_TILES, |p| {
2317 reservations.is_free(Target::Pickup(p))
2318 && items.iter().any(|(_, item, pos)| {
2319 pos.0 == p && item.kind == material && item.count > 0
2320 })
2321 });
2322 match next {
2323 _ if total >= cost => {
2324 *pickup = None;
2325 Step::Continue
2326 }
2327 Some(next) => {
2328 reservations.reserve(Target::Pickup(next), entity);
2329 *pickup = Some(next);
2330 Step::Continue
2331 }
2332 None => Step::Done,
2333 }
2334 }
2335 } else if cost > 0
2336 && !carrying
2337 .0
2338 .is_some_and(|load| load.kind == material && load.count >= cost)
2339 {
2340 Step::Done
2341 } else if !is_adjacent(tile.0.as_ivec2(), site.as_ivec2()) {
2342 match find_path_adjacent(&map, tile.0, site) {
2343 Some(p) if !p.is_empty() => {
2344 path.0 = p.into();
2345 Step::Continue
2346 }
2347 _ => Step::Done,
2348 }
2349 } else {
2350 *progress = (*progress + rate).min(kind.work());
2351 let someone_in_the_way = kind.blocks_tile() && occupied.contains(&site);
2352 if *progress < kind.work() || someone_in_the_way {
2353 Step::Continue
2354 } else {
2355 goals.record_use(material, cost);
2356 carrying.0 = carrying.0.and_then(|load| {
2357 (load.count > cost).then_some(Item {
2358 count: load.count - cost,
2359 ..load
2360 })
2361 });
2362 match kind {
2363 BuildKind::Structure(structure) => {
2364 spawn_structure(&mut commands, &structure_assets, site, structure);
2365 if kind.blocks_tile() {
2366 map.set_wall(site, true);
2367 changed.write(TileChanged(site));
2368 }
2369 }
2370 BuildKind::Roof => {
2371 map.set_roof(site, true);
2372 changed.write(TileChanged(site));
2373 }
2374 BuildKind::Pave => {
2375 map.set(site, Terrain::Paved);
2376 changed.write(TileChanged(site));
2377 }
2378 BuildKind::Floor => {
2379 map.set(site, Terrain::WoodFloor);
2380 changed.write(TileChanged(site));
2381 }
2382 BuildKind::Deconstruct => {
2383 for (entity, pos, structure, _) in &structures {
2384 if pos.0 != site {
2385 continue;
2386 }
2387 commands.entity(entity).despawn();
2388 let (refund, amount) = structure.kind.cost();
2389 if amount / 2 > 0 {
2390 drop_items(
2391 &mut commands,
2392 &mut items,
2393 &item_assets,
2394 site,
2395 Item::new(refund, amount / 2),
2396 );
2397 }
2398 }
2399 if map.is_wall(site.as_ivec2()) || map.is_roofed(site.as_ivec2()) {
2400 map.set_wall(site, false);
2401 map.set_roof(site, false);
2402 changed.write(TileChanged(site));
2403 }
2404 }
2405 BuildKind::Refuel(fire) => {
2406 for (_, pos, _, fuel) in &mut structures {
2407 if pos.0 == site
2408 && let Some(mut fuel) = fuel
2409 {
2410 fuel.0 = fire.fuel_ticks().unwrap_or(fuel.0);
2411 }
2412 }
2413 }
2414 }
2415 blueprints.0.remove(&site);
2416 Step::Done
2417 }
2418 }
2419 }
2420 Task::Haul { pickup, to } => {
2421 let to = *to;
2422 let destination_valid = zones.is_stockpile(to)
2423 && reservations.holder(Target::Storage(to)) == Some(entity);
2424 if !destination_valid {
2425 Step::Done
2426 } else if let Some(from) = *pickup {
2427 if reservations.holder(Target::Pickup(from)) != Some(entity) {
2428 Step::Done
2429 } else if tile.0 != from {
2430 walk_to(&map, tile.0, from, &mut path)
2431 } else {
2432 let kind = items
2433 .iter()
2434 .find(|(_, _, pos)| pos.0 == from)
2435 .map(|(_, item, _)| item.kind);
2436 let room = kind.map_or(0, |kind| {
2437 room_for(items.iter().map(|(_, i, p)| (i, p)), to, kind)
2438 });
2439 match take_items(&mut commands, &mut items, from, CARRY_CAPACITY.min(room))
2440 .filter(|load| load.count > 0)
2441 {
2442 Some(load) => {
2443 carrying.0 = Some(load);
2444 reservations.release(Target::Pickup(from));
2445 *pickup = None;
2446 Step::Continue
2447 }
2448 None => Step::Done,
2449 }
2450 }
2451 } else if let Some(load) = carrying.0 {
2452 if tile.0 != to {
2453 walk_to(&map, tile.0, to, &mut path)
2454 } else {
2455 let room = room_for(items.iter().map(|(_, i, p)| (i, p)), to, load.kind);
2456 let dropped = load.count.min(room);
2457 if dropped > 0 {
2458 drop_items(
2459 &mut commands,
2460 &mut items,
2461 &item_assets,
2462 to,
2463 load.with_count(dropped),
2464 );
2465 }
2466 carrying.0 = (load.count > dropped).then_some(Item {
2467 count: load.count - dropped,
2468 ..load
2469 });
2470 Step::Done
2471 }
2472 } else {
2473 Step::Done
2474 }
2475 }
2476 };
2477 if let Some(skill) = skill
2478 && job.task.progress() > progress_before
2479 {
2480 skills.practice(skill, 1.0);
2481 }
2482 match step {
2483 Step::Continue => current.0 = Some(job),
2484 Step::Done => {
2485 reservations.release_all(entity);
2486 current.0 = None;
2487 let ordered_move = matches!(
2488 job,
2489 Job {
2490 task: Task::MoveTo(_),
2491 ordered: true
2492 }
2493 );
2494 idle.restart(if ordered_move {
2495 IDLE_AFTER_ORDER_SECS
2496 } else {
2497 rng.range_f32(WORK_PAUSE_SECS)
2498 });
2499 }
2500 }
2501 }
2502 }
2503
2504 #[cfg(test)]
2505 mod tests;