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