src/jobs/mod.rs 88 K · 2527 lines · raw · history

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