src/jobs/mod.rs 89.1 K · 2557 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_bridged_path, find_nearby_path, find_path,
31 find_path_adjacent, 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 pub const DETOUR_MIN_TILES: usize = 24;
1816 const DETOUR_SEARCH: usize = 4000;
1817
1818 pub fn note_detour(map: &mut WorldMap, from: UVec2, to: UVec2, route: Option<&[UVec2]>) {
1819 let straight = (to.as_ivec2() - from.as_ivec2()).abs().max_element() as usize;
1820 if route.is_some_and(|r| r.len() < DETOUR_MIN_TILES || r.len() * 2 < straight * 3) {
1821 return;
1822 }
1823 let Some(bridged) = find_bridged_path(map, from, to, DETOUR_SEARCH) else {
1824 return;
1825 };
1826 let shorter = route.is_none_or(|r| bridged.len() * 3 / 2 < r.len());
1827 if !shorter {
1828 return;
1829 }
1830 for tile in bridged {
1831 let p = tile.as_ivec2();
1832 if map.get(p).is_some_and(|t| t.is_water()) {
1833 map.set_wear(tile, map.wear(p) + 1.0);
1834 }
1835 }
1836 }
1837
1838 fn walk_to(map: &mut WorldMap, from: UVec2, to: UVec2, path: &mut Path) -> Step {
1839 let route = find_path(map, from, to);
1840 note_detour(map, from, to, route.as_deref());
1841 match route {
1842 Some(p) if !p.is_empty() => {
1843 path.0 = p.into();
1844 Step::Continue
1845 }
1846 _ => Step::Done,
1847 }
1848 }
1849
1850 #[allow(clippy::too_many_arguments)]
1851 fn run_jobs(
1852 mut commands: Commands,
1853 mut map: ResMut<WorldMap>,
1854 mut zones: ResMut<Zones>,
1855 mut reservations: ResMut<Reservations>,
1856 mut rng: ResMut<SimRng>,
1857 item_assets: Res<ItemAssets>,
1858 structure_assets: Res<StructureAssets>,
1859 light: Res<LightMap>,
1860 tick: Res<SimTick>,
1861 mut blueprints: ResMut<Blueprints>,
1862 mut goals: ResMut<ColonyGoals>,
1863 mut items: ItemQuery,
1864 mut structures: Query<(Entity, &TilePos, &Structure, Option<&mut Fuel>), Without<Colonist>>,
1865 animals: Query<(&TilePos, &Path), (With<Animal>, Without<Colonist>)>,
1866 messages: (MessageWriter<TileChanged>, MessageWriter<Ate>),
1867 mut colonists: Query<
1868 (
1869 Entity,
1870 &TilePos,
1871 &mut Path,
1872 &mut CurrentJob,
1873 &mut IdleTimer,
1874 &mut Carrying,
1875 &Rest,
1876 &mut Hunger,
1877 Has<Sick>,
1878 Option<&mut Tool>,
1879 (&mut Skills, &mut WorkCarry, &Health),
1880 ),
1881 With<Colonist>,
1882 >,
1883 ) {
1884 let (mut changed, mut ate) = messages;
1885 let hour = hour_of_day(*tick);
1886 let tending = colonists
1887 .iter()
1888 .any(|c| c.3.0.is_some_and(|j| matches!(j.task, Task::Tend { .. })));
1889 let positions: HashMap<Entity, UVec2> = colonists
1890 .iter()
1891 .filter(|_| tending)
1892 .map(|c| (c.0, c.1.0))
1893 .collect();
1894 let walling = colonists.iter().any(|c| {
1895 c.3.0
1896 .is_some_and(|j| matches!(j.task, Task::Build { kind, .. } if kind.blocks_tile()))
1897 });
1898 let occupied: Vec<UVec2> = colonists
1899 .iter()
1900 .filter(|_| walling)
1901 .flat_map(|c| [Some(c.1.0), c.2.0.front().copied()])
1902 .chain(
1903 animals
1904 .iter()
1905 .filter(|_| walling)
1906 .flat_map(|(t, p)| [Some(t.0), p.0.front().copied()]),
1907 )
1908 .flatten()
1909 .collect();
1910 for (
1911 entity,
1912 tile,
1913 mut path,
1914 mut current,
1915 mut idle,
1916 mut carrying,
1917 rest,
1918 mut hunger,
1919 sick,
1920 mut tool,
1921 (mut skills, mut carry, health),
1922 ) in &mut colonists
1923 {
1924 let Some(mut job) = current.0 else {
1925 continue;
1926 };
1927 let rate = match work_rate(&light, tile.0) {
1928 rate if sick => rate.div_ceil(2),
1929 rate => rate,
1930 };
1931 let rate = tool_rate(rate, tool.is_some());
1932 if !path.0.is_empty() {
1933 continue;
1934 }
1935 let skill = Skill::of(&job.task);
1936 let rate = carry.apply(rate, skill.map_or(1.0, |s| speed(skills.level(s))));
1937 let progress_before = job.task.progress();
1938 if let Some(tool) = tool.as_mut()
1939 && !matches!(
1940 job.task,
1941 Task::MoveTo(_) | Task::Sleep { .. } | Task::Eat { .. } | Task::Equip { .. }
1942 )
1943 {
1944 tool.wear += 1;
1945 if tool.wear >= TOOL_LIFE_TICKS {
1946 commands.entity(entity).remove::<Tool>();
1947 }
1948 }
1949 let step = match &mut job.task {
1950 Task::MoveTo(_) => Step::Done,
1951 Task::Tend {
1952 patient,
1953 pickup,
1954 progress,
1955 } => {
1956 let patient = *patient;
1957 let ours = reservations.holder(Target::Patient(patient)) == Some(entity);
1958 match positions.get(&patient) {
1959 _ if !ours => Step::Done,
1960 None => Step::Done,
1961 Some(_) if pickup.is_some() => {
1962 let from = pickup.expect("checked above");
1963 if tile.0 != from {
1964 walk_to(&mut map, tile.0, from, &mut path)
1965 } else {
1966 reservations.release(Target::Pickup(from));
1967 carrying.0 = take_items(&mut commands, &mut items, from, 1)
1968 .filter(|load| load.kind == ItemKind::Herbs && load.count > 0);
1969 *pickup = None;
1970 Step::Continue
1971 }
1972 }
1973 Some(&at) if at != tile.0 && !is_adjacent(tile.0.as_ivec2(), at.as_ivec2()) => {
1974 match find_path_adjacent(&map, tile.0, at) {
1975 Some(p) if !p.is_empty() => {
1976 path.0 = p.into();
1977 Step::Continue
1978 }
1979 _ => Step::Done,
1980 }
1981 }
1982 Some(_) => {
1983 *progress += rate;
1984 if *progress < TEND_WORK {
1985 Step::Continue
1986 } else {
1987 let herb = carrying.0.is_some_and(|l| l.kind == ItemKind::Herbs);
1988 if herb {
1989 goals.record_use(ItemKind::Herbs, 1);
1990 carrying.0 = None;
1991 }
1992 let speed = speed(skills.level(Skill::Medicine));
1993 commands.entity(patient).insert(Tended {
1994 until: tick.0 + TENDED_TICKS,
1995 quality: tend_quality(speed, herb),
1996 });
1997 Step::Done
1998 }
1999 }
2000 }
2001 }
2002 Task::Equip { pickup } => {
2003 let from = *pickup;
2004 if reservations.holder(Target::Pickup(from)) != Some(entity) {
2005 Step::Done
2006 } else if tile.0 != from {
2007 walk_to(&mut map, tile.0, from, &mut path)
2008 } else {
2009 reservations.release(Target::Pickup(from));
2010 let stack = items
2011 .iter()
2012 .any(|(_, item, pos)| pos.0 == from && item.kind == ItemKind::Tools);
2013 if stack && take_items(&mut commands, &mut items, from, 1).is_some() {
2014 goals.record_use(ItemKind::Tools, 1);
2015 commands.entity(entity).insert(Tool::default());
2016 }
2017 Step::Done
2018 }
2019 }
2020 Task::Craft {
2021 recipe,
2022 pickup,
2023 station,
2024 progress,
2025 } => {
2026 let (recipe, station) = (*recipe, *station);
2027 let input = recipe.input();
2028 let fire = structures
2029 .iter()
2030 .any(|(_, pos, s, _)| pos.0 == station && recipe.stations().contains(&s.kind));
2031 let ours = reservations.holder(Target::Station(station)) == Some(entity);
2032 if !fire || !ours {
2033 Step::Done
2034 } else if let Some(from) = *pickup {
2035 if reservations.holder(Target::Pickup(from)) != Some(entity) {
2036 Step::Done
2037 } else if tile.0 != from {
2038 walk_to(&mut map, tile.0, from, &mut path)
2039 } else {
2040 let load = take_items(&mut commands, &mut items, from, input)
2041 .filter(|load| recipe.accepts(load.kind) && load.count > 0);
2042 carrying.0 = load;
2043 reservations.release(Target::Pickup(from));
2044 match load {
2045 Some(load) if load.count >= input => {
2046 *pickup = None;
2047 Step::Continue
2048 }
2049 _ => Step::Done,
2050 }
2051 }
2052 } else if !carrying
2053 .0
2054 .is_some_and(|load| recipe.accepts(load.kind) && load.count >= input)
2055 {
2056 Step::Done
2057 } else if !is_adjacent(tile.0.as_ivec2(), station.as_ivec2()) {
2058 match find_path_adjacent(&map, tile.0, station) {
2059 Some(p) if !p.is_empty() => {
2060 path.0 = p.into();
2061 Step::Continue
2062 }
2063 _ => Step::Done,
2064 }
2065 } else {
2066 *progress += rate;
2067 if *progress < recipe.work() {
2068 Step::Continue
2069 } else {
2070 if let Some(load) = carrying.0 {
2071 goals.record_use(load.kind, input);
2072 }
2073 carrying.0 = carrying.0.and_then(|load| {
2074 (load.count > input).then_some(Item {
2075 count: load.count - input,
2076 ..load
2077 })
2078 });
2079 drop_items(
2080 &mut commands,
2081 &mut items,
2082 &item_assets,
2083 tile.0,
2084 Item::new(recipe.output(), 1),
2085 );
2086 Step::Done
2087 }
2088 }
2089 }
2090 Task::Hunt {
2091 animal,
2092 kind,
2093 progress,
2094 } => match animals.get(*animal) {
2095 Ok((prey, _)) if reservations.holder(Target::Animal(*animal)) == Some(entity) => {
2096 let prey = prey.0;
2097 let in_range = tile.0.as_vec2().distance(prey.as_vec2()) <= HUNT_RANGE
2098 && line_of_sight(&map, tile.0, prey);
2099 let stale = path.0.back().is_none_or(|end| {
2100 end.as_vec2().distance(prey.as_vec2()) > HUNT_RANGE / 2.0
2101 });
2102 if !in_range && !stale {
2103 Step::Continue
2104 } else if !in_range {
2105 match find_path_adjacent(&map, tile.0, prey) {
2106 Some(p) if !p.is_empty() => {
2107 path.0 = p.into();
2108 Step::Continue
2109 }
2110 _ => Step::Done,
2111 }
2112 } else {
2113 path.0.truncate(1);
2114 *progress += rate;
2115 if *progress < kind.hunt_work() {
2116 Step::Continue
2117 } else {
2118 commands.entity(*animal).despawn();
2119 drop_items(
2120 &mut commands,
2121 &mut items,
2122 &item_assets,
2123 prey,
2124 Item::new(ItemKind::Carcass, kind.meat()),
2125 );
2126 Step::Done
2127 }
2128 }
2129 }
2130 _ => Step::Done,
2131 },
2132 Task::Tame {
2133 animal,
2134 kind,
2135 progress,
2136 } => match animals.get(*animal) {
2137 Ok((prey, _)) if reservations.holder(Target::Animal(*animal)) == Some(entity) => {
2138 let prey = prey.0;
2139 if prey != tile.0 && !is_adjacent(tile.0.as_ivec2(), prey.as_ivec2()) {
2140 if path.0.is_empty() {
2141 match find_path_adjacent(&map, tile.0, prey) {
2142 Some(p) if !p.is_empty() => {
2143 path.0 = p.into();
2144 Step::Continue
2145 }
2146 _ => Step::Done,
2147 }
2148 } else {
2149 Step::Continue
2150 }
2151 } else {
2152 *progress += rate;
2153 if *progress < kind.tame_work() {
2154 Step::Continue
2155 } else {
2156 commands.entity(*animal).insert(Tame);
2157 Step::Done
2158 }
2159 }
2160 }
2161 _ => Step::Done,
2162 },
2163 Task::Butcher { site, progress } => {
2164 let site = *site;
2165 if reservations.holder(Target::Pickup(site)) != Some(entity) {
2166 Step::Done
2167 } else if tile.0 != site {
2168 walk_to(&mut map, tile.0, site, &mut path)
2169 } else {
2170 *progress += rate;
2171 if *progress < BUTCHER_WORK {
2172 Step::Continue
2173 } else {
2174 let is_carcass = items
2175 .iter()
2176 .any(|(_, item, pos)| pos.0 == site && item.kind == ItemKind::Carcass);
2177 if is_carcass
2178 && let Some(carcass) =
2179 take_items(&mut commands, &mut items, site, u32::MAX)
2180 {
2181 let meat = Item {
2182 kind: ItemKind::Meat,
2183 ..carcass
2184 };
2185 drop_items(&mut commands, &mut items, &item_assets, site, meat);
2186 }
2187 Step::Done
2188 }
2189 }
2190 }
2191 Task::Eat { pickup, progress } => {
2192 if let Some(from) = *pickup {
2193 if reservations.holder(Target::Pickup(from)) != Some(entity) {
2194 Step::Done
2195 } else if tile.0 != from {
2196 walk_to(&mut map, tile.0, from, &mut path)
2197 } else {
2198 let kind = items
2199 .iter()
2200 .find(|(_, item, pos)| pos.0 == from && item.kind.is_food())
2201 .map(|(_, item, _)| item.kind);
2202 let meal = kind.and_then(|kind| {
2203 take_items(&mut commands, &mut items, from, portion(kind, hunger.0))
2204 });
2205 reservations.release(Target::Pickup(from));
2206 match meal.filter(|m| m.kind.is_food()) {
2207 Some(meal) => {
2208 carrying.0 = Some(meal);
2209 *pickup = None;
2210 Step::Continue
2211 }
2212 None => Step::Done,
2213 }
2214 }
2215 } else if let Some(meal) = carrying.0.filter(|m| m.kind.is_food()) {
2216 *progress += 1;
2217 if *progress < EAT_TICKS {
2218 Step::Continue
2219 } else {
2220 hunger.0 = (hunger.0 + meal.count as f32 * meal.kind.nutrition()).min(1.0);
2221 goals.record_use(meal.kind, meal.count);
2222 ate.write(Ate {
2223 colonist: entity,
2224 kind: meal.kind,
2225 });
2226 carrying.0 = None;
2227 Step::Done
2228 }
2229 } else {
2230 Step::Done
2231 }
2232 }
2233 Task::Sleep { spot } => {
2234 if reservations.holder(Target::SleepSpot(*spot)) != Some(entity) {
2235 Step::Done
2236 } else if tile.0 != *spot {
2237 let step = walk_to(&mut map, tile.0, *spot, &mut path);
2238 if matches!(step, Step::Done) {
2239 reservations.release(Target::SleepSpot(*spot));
2240 *spot = tile.0;
2241 reservations.reserve(Target::SleepSpot(*spot), entity);
2242 }
2243 Step::Continue
2244 } else if done_sleeping(rest.0, hour) && health.0 >= INJURED {
2245 Step::Done
2246 } else {
2247 Step::Continue
2248 }
2249 }
2250 Task::Work {
2251 site,
2252 kind,
2253 progress,
2254 } => {
2255 let (site, kind) = (*site, *kind);
2256 let work = site_work(&map, site).filter(|w| w.kind == kind);
2257 let valid =
2258 work.is_some() || (kind == WorkKind::Sow && sowable(&map, &zones, site));
2259 let ours = reservations.holder(Target::Site(site)) == Some(entity);
2260 if !valid || !ours {
2261 Step::Done
2262 } else if !is_adjacent(tile.0.as_ivec2(), site.as_ivec2()) {
2263 match find_path_adjacent(&map, tile.0, site) {
2264 Some(p) if !p.is_empty() => {
2265 path.0 = p.into();
2266 Step::Continue
2267 }
2268 _ => Step::Done,
2269 }
2270 } else {
2271 *progress += rate;
2272 if *progress < kind.work() {
2273 Step::Continue
2274 } else {
2275 kind.complete(&mut map, site);
2276 changed.write(TileChanged(site));
2277 let step = site.as_ivec2() - tile.0.as_ivec2();
2278 let drop_at = if can_step(&map, tile.0.as_ivec2(), step) {
2279 site
2280 } else {
2281 tile.0
2282 };
2283 if let Some(work) = work {
2284 drop_items(
2285 &mut commands,
2286 &mut items,
2287 &item_assets,
2288 drop_at,
2289 Item::new(work.item, work.count),
2290 );
2291 }
2292 Step::Done
2293 }
2294 }
2295 }
2296 Task::ExpandStockpile { site, progress } => {
2297 if tile.0 != *site {
2298 walk_to(&mut map, tile.0, *site, &mut path)
2299 } else {
2300 *progress += rate;
2301 if *progress >= EXPAND_WORK {
2302 if zones.has_stockpile() {
2303 let excluded = blueprints.tiles();
2304 let tiles = plan_expansion(&map, &zones, &excluded, EXPANSION_TILES);
2305 zones.add_tiles(&map, tiles);
2306 } else {
2307 zones.add_tiles(&map, new_stockpile_tiles(*site));
2308 }
2309 Step::Done
2310 } else {
2311 Step::Continue
2312 }
2313 }
2314 }
2315 Task::Build {
2316 site,
2317 kind,
2318 pickup,
2319 progress,
2320 } => {
2321 let (site, kind) = (*site, *kind);
2322 let ours = reservations.holder(Target::Site(site)) == Some(entity);
2323 let planned = match kind {
2324 BuildKind::Structure(
2325 StructureKind::Campfire
2326 | StructureKind::Torch
2327 | StructureKind::Bed
2328 | StructureKind::Workbench,
2329 )
2330 | BuildKind::Refuel(_) => true,
2331 _ => blueprints.get(site) == Some(kind),
2332 };
2333 let structure_there = structures.iter().any(|(_, pos, ..)| pos.0 == site);
2334 let blocked = map.plant(site.as_ivec2()).is_some()
2335 || match kind {
2336 BuildKind::Roof | BuildKind::Floor | BuildKind::Bridge => false,
2337 BuildKind::Refuel(fire) => !structures
2338 .iter()
2339 .any(|(_, pos, s, _)| pos.0 == site && s.kind == fire),
2340 BuildKind::Deconstruct => {
2341 !structure_there && !map.is_roofed(site.as_ivec2())
2342 }
2343 _ => structure_there,
2344 };
2345 let (material, cost) = kind.cost();
2346 if !ours || blocked || !planned {
2347 Step::Done
2348 } else if let Some(from) = *pickup {
2349 if reservations.holder(Target::Pickup(from)) != Some(entity) {
2350 Step::Done
2351 } else if tile.0 != from {
2352 walk_to(&mut map, tile.0, from, &mut path)
2353 } else if carrying.0.is_some_and(|load| load.kind != material) {
2354 Step::Done
2355 } else {
2356 let have = carrying.0.map_or(0, |load| load.count);
2357 let taken = take_items(&mut commands, &mut items, from, cost - have)
2358 .filter(|load| load.kind == material)
2359 .map_or(0, |load| load.count);
2360 reservations.release(Target::Pickup(from));
2361 let total = have + taken;
2362 carrying.0 = (total > 0).then_some(Item::new(material, total));
2363 let next = nearest_tile(&map, tile.0, SEARCH_TILES, |p| {
2364 reservations.is_free(Target::Pickup(p))
2365 && items.iter().any(|(_, item, pos)| {
2366 pos.0 == p && item.kind == material && item.count > 0
2367 })
2368 });
2369 match next {
2370 _ if total >= cost => {
2371 *pickup = None;
2372 Step::Continue
2373 }
2374 Some(next) => {
2375 reservations.reserve(Target::Pickup(next), entity);
2376 *pickup = Some(next);
2377 Step::Continue
2378 }
2379 None => Step::Done,
2380 }
2381 }
2382 } else if cost > 0
2383 && !carrying
2384 .0
2385 .is_some_and(|load| load.kind == material && load.count >= cost)
2386 {
2387 Step::Done
2388 } else if !is_adjacent(tile.0.as_ivec2(), site.as_ivec2()) {
2389 match find_path_adjacent(&map, tile.0, site) {
2390 Some(p) if !p.is_empty() => {
2391 path.0 = p.into();
2392 Step::Continue
2393 }
2394 _ => Step::Done,
2395 }
2396 } else {
2397 *progress = (*progress + rate).min(kind.work());
2398 let someone_in_the_way = kind.blocks_tile() && occupied.contains(&site);
2399 if *progress < kind.work() || someone_in_the_way {
2400 Step::Continue
2401 } else {
2402 goals.record_use(material, cost);
2403 carrying.0 = carrying.0.and_then(|load| {
2404 (load.count > cost).then_some(Item {
2405 count: load.count - cost,
2406 ..load
2407 })
2408 });
2409 match kind {
2410 BuildKind::Structure(structure) => {
2411 spawn_structure(&mut commands, &structure_assets, site, structure);
2412 if kind.blocks_tile() {
2413 map.set_wall(site, true);
2414 changed.write(TileChanged(site));
2415 }
2416 }
2417 BuildKind::Roof => {
2418 map.set_roof(site, true);
2419 changed.write(TileChanged(site));
2420 }
2421 BuildKind::Pave => {
2422 map.set(site, Terrain::Paved);
2423 changed.write(TileChanged(site));
2424 }
2425 BuildKind::Floor => {
2426 map.set(site, Terrain::WoodFloor);
2427 changed.write(TileChanged(site));
2428 }
2429 BuildKind::Bridge => {
2430 map.set(site, Terrain::Bridge);
2431 map.set_wear(site, 0.0);
2432 changed.write(TileChanged(site));
2433 }
2434 BuildKind::Deconstruct => {
2435 for (entity, pos, structure, _) in &structures {
2436 if pos.0 != site {
2437 continue;
2438 }
2439 commands.entity(entity).despawn();
2440 let (refund, amount) = structure.kind.cost();
2441 if amount / 2 > 0 {
2442 drop_items(
2443 &mut commands,
2444 &mut items,
2445 &item_assets,
2446 site,
2447 Item::new(refund, amount / 2),
2448 );
2449 }
2450 }
2451 if map.is_wall(site.as_ivec2()) || map.is_roofed(site.as_ivec2()) {
2452 map.set_wall(site, false);
2453 map.set_roof(site, false);
2454 changed.write(TileChanged(site));
2455 }
2456 }
2457 BuildKind::Refuel(fire) => {
2458 for (_, pos, _, fuel) in &mut structures {
2459 if pos.0 == site
2460 && let Some(mut fuel) = fuel
2461 {
2462 fuel.0 = fire.fuel_ticks().unwrap_or(fuel.0);
2463 }
2464 }
2465 }
2466 }
2467 blueprints.0.remove(&site);
2468 Step::Done
2469 }
2470 }
2471 }
2472 Task::Haul { pickup, to } => {
2473 let to = *to;
2474 let destination_valid = zones.is_stockpile(to)
2475 && reservations.holder(Target::Storage(to)) == Some(entity);
2476 if !destination_valid {
2477 Step::Done
2478 } else if let Some(from) = *pickup {
2479 if reservations.holder(Target::Pickup(from)) != Some(entity) {
2480 Step::Done
2481 } else if tile.0 != from {
2482 walk_to(&mut map, tile.0, from, &mut path)
2483 } else {
2484 let kind = items
2485 .iter()
2486 .find(|(_, _, pos)| pos.0 == from)
2487 .map(|(_, item, _)| item.kind);
2488 let room = kind.map_or(0, |kind| {
2489 room_for(items.iter().map(|(_, i, p)| (i, p)), to, kind)
2490 });
2491 match take_items(&mut commands, &mut items, from, CARRY_CAPACITY.min(room))
2492 .filter(|load| load.count > 0)
2493 {
2494 Some(load) => {
2495 carrying.0 = Some(load);
2496 reservations.release(Target::Pickup(from));
2497 *pickup = None;
2498 Step::Continue
2499 }
2500 None => Step::Done,
2501 }
2502 }
2503 } else if let Some(load) = carrying.0 {
2504 if tile.0 != to {
2505 walk_to(&mut map, tile.0, to, &mut path)
2506 } else {
2507 let room = room_for(items.iter().map(|(_, i, p)| (i, p)), to, load.kind);
2508 let dropped = load.count.min(room);
2509 if dropped > 0 {
2510 drop_items(
2511 &mut commands,
2512 &mut items,
2513 &item_assets,
2514 to,
2515 load.with_count(dropped),
2516 );
2517 }
2518 carrying.0 = (load.count > dropped).then_some(Item {
2519 count: load.count - dropped,
2520 ..load
2521 });
2522 Step::Done
2523 }
2524 } else {
2525 Step::Done
2526 }
2527 }
2528 };
2529 if let Some(skill) = skill
2530 && job.task.progress() > progress_before
2531 {
2532 skills.practice(skill, 1.0);
2533 }
2534 match step {
2535 Step::Continue => current.0 = Some(job),
2536 Step::Done => {
2537 reservations.release_all(entity);
2538 current.0 = None;
2539 let ordered_move = matches!(
2540 job,
2541 Job {
2542 task: Task::MoveTo(_),
2543 ordered: true
2544 }
2545 );
2546 idle.restart(if ordered_move {
2547 IDLE_AFTER_ORDER_SECS
2548 } else {
2549 rng.range_f32(WORK_PAUSE_SECS)
2550 });
2551 }
2552 }
2553 }
2554 }
2555
2556 #[cfg(test)]
2557 mod tests;