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