src/jobs/mod.rs 82.3 K · 2374 lines · raw · history

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