src/jobs/mod.rs 80.9 K · 2336 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, 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 station(self) -> StructureKind {
213 match self {
214 Recipe::Meal => StructureKind::Campfire,
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
533 #[derive(Default, Clone, Copy)]
534 pub struct Memo {
535 mask: u32,
536 until: u64,
537 }
538
539 impl Memo {
540 fn skips(self, mask: u32, tick: u64) -> bool {
541 self.mask == mask && tick < self.until
542 }
543 }
544
545 #[derive(Default)]
546 pub struct GatherMemo {
547 gather: Memo,
548 forage: Memo,
549 haul: Memo,
550 build: Memo,
551 }
552
553 pub fn find_work(
554 map: &WorldMap,
555 reservations: &Reservations,
556 from: UVec2,
557 wanted: impl Fn(UVec2, ItemKind) -> bool,
558 ) -> Option<Task> {
559 find_work_within(map, reservations, from, SEARCH_TILES, wanted)
560 }
561
562 pub fn find_work_within(
563 map: &WorldMap,
564 reservations: &Reservations,
565 from: UVec2,
566 max_tiles: usize,
567 wanted: impl Fn(UVec2, ItemKind) -> bool,
568 ) -> Option<Task> {
569 let mut found = None;
570 nearest_tile(map, from, max_tiles, |stand| {
571 found = NEIGHBORS.iter().find_map(|&d| {
572 let site = stand.as_ivec2() + d;
573 if site.x < 0 || site.y < 0 {
574 return None;
575 }
576 let site = site.as_uvec2();
577 let work = site_work(map, site)?;
578 (wanted(site, work.item) && reservations.is_free(Target::Site(site))).then_some(
579 Task::Work {
580 site,
581 kind: work.kind,
582 progress: 0,
583 },
584 )
585 });
586 found.is_some()
587 })?;
588 found
589 }
590
591 fn stacks_by_tile<'a>(
592 items: impl IntoIterator<Item = (&'a Item, &'a TilePos)>,
593 ) -> HashMap<UVec2, (ItemKind, u32)> {
594 let mut stacks: HashMap<UVec2, (ItemKind, u32)> = HashMap::new();
595 for (item, pos) in items {
596 stacks.entry(pos.0).or_insert((item.kind, 0)).1 += item.count;
597 }
598 stacks
599 }
600
601 fn storage_candidates<'z>(
602 zones: &'z Zones,
603 reservations: &'z Reservations,
604 stacks: &'z HashMap<UVec2, (ItemKind, u32)>,
605 kind: ItemKind,
606 ) -> impl Iterator<Item = UVec2> + 'z {
607 zones.stockpile_tiles().filter(move |&t| {
608 zones.accepts(t, kind)
609 && reservations.is_free(Target::Storage(t))
610 && match stacks.get(&t) {
611 None => true,
612 Some(&(other, count)) => other == kind && count < STACK_LIMIT,
613 }
614 })
615 }
616
617 pub fn find_storage<'a>(
618 map: &WorldMap,
619 zones: &Zones,
620 reservations: &Reservations,
621 reach: &ColonyReach,
622 items: impl IntoIterator<Item = (&'a Item, &'a TilePos)> + Clone,
623 from: UVec2,
624 kind: ItemKind,
625 ) -> Option<UVec2> {
626 let stacks = stacks_by_tile(items);
627 StoragePriority::DESCENDING
628 .into_iter()
629 .find_map(|priority| {
630 let candidates: HashSet<UVec2> = storage_candidates(zones, reservations, &stacks, kind)
631 .filter(|&t| zones.rule(t).priority == priority)
632 .collect();
633 reach.nearest(map, from, SEARCH_TILES, &candidates)
634 })
635 }
636
637 pub fn find_haul(
638 map: &WorldMap,
639 zones: &Zones,
640 reservations: &Reservations,
641 reach: &ColonyReach,
642 items: &[(Item, TilePos)],
643 from: UVec2,
644 ) -> Option<Task> {
645 let stacks = stacks_by_tile(items.iter().map(|(i, p)| (i, p)));
646 let mut best: HashMap<ItemKind, StoragePriority> = HashMap::new();
647 let misplaced = |item: &Item, tile: UVec2, best: &mut HashMap<ItemKind, StoragePriority>| {
648 if !zones.accepts(tile, item.kind) {
649 return true;
650 }
651 let top = *best.entry(item.kind).or_insert_with(|| {
652 storage_candidates(zones, reservations, &stacks, item.kind)
653 .map(|t| zones.rule(t).priority)
654 .max()
655 .unwrap_or(StoragePriority::Low)
656 });
657 zones.rule(tile).priority < top
658 };
659 let mut room: HashMap<ItemKind, bool> = HashMap::new();
660 let loose: HashSet<UVec2> = items
661 .iter()
662 .filter(|(item, pos)| {
663 reservations.is_free(Target::Pickup(pos.0))
664 && reach.allows(pos.0)
665 && misplaced(item, pos.0, &mut best)
666 && *room.entry(item.kind).or_insert_with(|| {
667 storage_candidates(zones, reservations, &stacks, item.kind)
668 .next()
669 .is_some()
670 })
671 })
672 .map(|(_, pos)| pos.0)
673 .collect();
674 if loose.is_empty() {
675 return None;
676 }
677 let pickup = reach.nearest(map, from, SEARCH_TILES, &loose)?;
678 let (item, _) = items.iter().find(|(_, pos)| pos.0 == pickup)?;
679 let refs = items.iter().map(|(i, p)| (i, p));
680 let to = find_storage(map, zones, reservations, reach, refs, pickup, item.kind)?;
681 Some(Task::Haul {
682 pickup: Some(pickup),
683 to,
684 })
685 }
686
687 pub fn sowable(map: &WorldMap, zones: &Zones, site: UVec2) -> bool {
688 zones.is_farm(site) && map.plant(site.as_ivec2()).is_none()
689 }
690
691 pub fn find_sowing(
692 map: &WorldMap,
693 zones: &Zones,
694 reservations: &Reservations,
695 from: UVec2,
696 ) -> Option<Task> {
697 let open: HashSet<UVec2> = zones
698 .farm_tiles()
699 .filter(|&p| sowable(map, zones, p) && reservations.is_free(Target::Site(p)))
700 .collect();
701 let site = nearest_of(map, from, SEARCH_TILES, &open)?;
702 Some(Task::Work {
703 site,
704 kind: WorkKind::Sow,
705 progress: 0,
706 })
707 }
708
709 pub const HUNT_RADIUS: f32 = 60.0;
710 pub const HUNT_RANGE: f32 = 6.0;
711
712 pub type Wildlife = (Entity, UVec2, AnimalKind, bool);
713
714 fn nearest_reachable(
715 map: &WorldMap,
716 reservations: &Reservations,
717 animals: &[Wildlife],
718 from: UVec2,
719 pick: impl Fn(&Wildlife) -> bool,
720 ) -> Option<Wildlife> {
721 let mut candidates: Vec<&Wildlife> = animals
722 .iter()
723 .filter(|a| {
724 pick(a)
725 && reservations.is_free(Target::Animal(a.0))
726 && a.1.as_vec2().distance(from.as_vec2()) <= HUNT_RADIUS
727 })
728 .collect();
729 candidates.sort_by(|a, b| {
730 a.1.as_vec2()
731 .distance_squared(from.as_vec2())
732 .total_cmp(&b.1.as_vec2().distance_squared(from.as_vec2()))
733 });
734 candidates
735 .into_iter()
736 .find(|a| find_path_adjacent(map, from, a.1).is_some())
737 .copied()
738 }
739
740 pub fn find_hunt(
741 map: &WorldMap,
742 reservations: &Reservations,
743 animals: &[Wildlife],
744 from: UVec2,
745 ) -> Option<Task> {
746 let herd = |kind: AnimalKind| animals.iter().filter(|a| a.3 && a.2 == kind).count();
747 let spare_tame = |a: &Wildlife| a.3 && herd(a.2) > 2;
748 let (animal, _, kind, _) = nearest_reachable(map, reservations, animals, from, spare_tame)
749 .or_else(|| nearest_reachable(map, reservations, animals, from, |a| !a.3))?;
750 Some(Task::Hunt {
751 animal,
752 kind,
753 progress: 0,
754 })
755 }
756
757 pub fn find_taming(
758 map: &WorldMap,
759 reservations: &Reservations,
760 animals: &[Wildlife],
761 from: UVec2,
762 ) -> Option<Task> {
763 if animals.iter().filter(|a| a.3).count() >= MAX_TAME {
764 return None;
765 }
766 let tameable = |a: &Wildlife| !a.3 && !a.2.is_predator();
767 let (animal, _, kind, _) = nearest_reachable(map, reservations, animals, from, tameable)?;
768 Some(Task::Tame {
769 animal,
770 kind,
771 progress: 0,
772 })
773 }
774
775 pub fn stockpile_too_full(zones: &Zones, items: &[(Item, TilePos)]) -> bool {
776 let occupied: HashSet<UVec2> = items.iter().map(|(_, pos)| pos.0).collect();
777 zones.has_stockpile() && zones.empty_stockpile_tiles(&occupied) < MIN_EMPTY_STOCKPILE_TILES
778 }
779
780 pub fn find_expansion(
781 map: &WorldMap,
782 zones: &Zones,
783 reservations: &Reservations,
784 items: &[(Item, TilePos)],
785 blueprints: &Blueprints,
786 ) -> Option<Task> {
787 if !reservations.is_free(Target::StockpileExpansion) {
788 return None;
789 }
790 let site = if zones.has_stockpile() {
791 if !stockpile_too_full(zones, items) {
792 return None;
793 }
794 *plan_expansion(map, zones, &blueprints.tiles(), EXPANSION_TILES).first()?
795 } else {
796 find_stockpile_site(map, central_item_tile(items)?)?
797 };
798 Some(Task::ExpandStockpile { site, progress: 0 })
799 }
800
801 fn central_item_tile(items: &[(Item, TilePos)]) -> Option<UVec2> {
802 let tiles: Vec<Vec2> = items.iter().map(|(_, pos)| pos.0.as_vec2()).collect();
803 let centroid = tiles.iter().sum::<Vec2>() / tiles.len().max(1) as f32;
804 items.iter().map(|(_, pos)| pos.0).min_by(|a, b| {
805 a.as_vec2()
806 .distance_squared(centroid)
807 .total_cmp(&b.as_vec2().distance_squared(centroid))
808 })
809 }
810
811 pub struct BuildContext<'a> {
812 pub map: &'a WorldMap,
813 pub zones: &'a Zones,
814 pub reservations: &'a Reservations,
815 pub items: &'a [(Item, TilePos)],
816 pub structures: &'a [(UVec2, StructureKind, u32)],
817 pub planned: &'a [(UVec2, StructureKind)],
818 pub blueprints: &'a Blueprints,
819 pub occupied: &'a HashSet<UVec2>,
820 pub reach: &'a ColonyReach,
821 }
822
823 impl BuildContext<'_> {
824 fn buildable(&self, tile: UVec2) -> bool {
825 can_zone(self.map, tile.as_ivec2())
826 && !self.zones.is_stockpile(tile)
827 && !self.zones.is_farm(tile)
828 && self.map.plant(tile.as_ivec2()).is_none()
829 && !self.occupied.contains(&tile)
830 && self.blueprints.get(tile).is_none()
831 && self.reservations.is_free(Target::Site(tile))
832 }
833
834 fn lights(&self) -> impl Iterator<Item = (UVec2, StructureKind)> + Clone + '_ {
835 self.structures
836 .iter()
837 .map(|&(pos, kind, _)| (pos, kind))
838 .chain(self.planned.iter().copied())
839 }
840
841 fn campfire_needed(&self, home: UVec2) -> bool {
842 let burning = self.structures.iter().any(|&(pos, kind, fuel)| {
843 kind == StructureKind::Campfire
844 && fuel >= CAMPFIRE_MIN_FUEL
845 && pos.as_vec2().distance(home.as_vec2()) <= HOME_RADIUS
846 });
847 let planned = self
848 .planned
849 .iter()
850 .any(|(_, kind)| *kind == StructureKind::Campfire);
851 !burning && !planned
852 }
853
854 fn darkest_stockpile_tile(&self, home: UVec2) -> Option<UVec2> {
855 self.zones
856 .stockpile_tiles()
857 .filter(|&t| brightness(light_from(t, self.lights())) < LIT_BRIGHTNESS)
858 .min_by(|a, b| {
859 a.as_vec2()
860 .distance_squared(home.as_vec2())
861 .total_cmp(&b.as_vec2().distance_squared(home.as_vec2()))
862 .then((a.y, a.x).cmp(&(b.y, b.x)))
863 })
864 }
865
866 fn site_near(&self, target: UVec2, max_distance: f32) -> Option<UVec2> {
867 self.site_near_where(target, max_distance, |_| true)
868 }
869
870 fn site_near_where(
871 &self,
872 target: UVec2,
873 max_distance: f32,
874 extra: impl Fn(UVec2) -> bool,
875 ) -> Option<UVec2> {
876 nearest_tile(self.map, target, SEARCH_TILES, |p| {
877 self.buildable(p) && extra(p)
878 })
879 .filter(|site| site.as_vec2().distance(target.as_vec2()) <= max_distance)
880 }
881
882 fn clear_of_stockpile(&self, tile: UVec2) -> bool {
883 self.zones
884 .stockpile_tiles()
885 .chain(self.zones.farm_tiles())
886 .all(|s| {
887 (s.as_ivec2() - tile.as_ivec2()).abs().max_element() >= BED_STOCKPILE_CLEARANCE
888 })
889 }
890
891 fn count(&self, kind: StructureKind) -> usize {
892 let built = self.structures.iter().filter(|s| s.1 == kind).count();
893 built + self.planned.iter().filter(|p| p.1 == kind).count()
894 }
895
896 fn bed_anchor(&self, home: UVec2) -> UVec2 {
897 let positions = |kind: StructureKind| {
898 let built = self.structures.iter().map(|s| (s.0, s.1));
899 built
900 .chain(self.planned.iter().copied())
901 .filter(move |s| s.1 == kind)
902 .map(|s| s.0)
903 };
904 let nearest = |kind: StructureKind| {
905 positions(kind).min_by(|a, b| {
906 a.as_vec2()
907 .distance_squared(home.as_vec2())
908 .total_cmp(&b.as_vec2().distance_squared(home.as_vec2()))
909 })
910 };
911 nearest(StructureKind::Bed)
912 .or_else(|| nearest(StructureKind::Campfire))
913 .unwrap_or(home)
914 }
915
916 fn material_source(&self, from: UVec2, (kind, amount): (ItemKind, u32)) -> Option<UVec2> {
917 let stacks: Vec<(u32, UVec2)> = self
918 .items
919 .iter()
920 .filter(|(item, pos)| {
921 item.kind == kind && self.reservations.is_free(Target::Pickup(pos.0))
922 })
923 .map(|(item, pos)| (item.count, pos.0))
924 .collect();
925 if stacks.iter().map(|(count, _)| count).sum::<u32>() < amount {
926 return None;
927 }
928 let tiles: HashSet<UVec2> = stacks.into_iter().map(|(_, tile)| tile).collect();
929 self.reach.nearest(self.map, from, SEARCH_TILES, &tiles)
930 }
931
932 fn build_task(&self, from: UVec2, site: UVec2, kind: BuildKind) -> Option<Task> {
933 let pickup = match kind.cost() {
934 (_, 0) => None,
935 cost => Some(self.material_source(from, cost)?),
936 };
937 Some(Task::Build {
938 site,
939 kind,
940 pickup,
941 progress: 0,
942 })
943 }
944 }
945
946 pub const COOK_INPUT: u32 = 6;
947 pub const COOK_WORK: u32 = 300;
948 pub const COOK_MIN_FUEL: u32 = crate::sim::TICKS_PER_HOUR as u32;
949
950 pub fn find_campfire(ctx: &BuildContext, from: UVec2) -> Option<Task> {
951 let home = ctx.zones.home(from);
952 if !ctx.campfire_needed(home) {
953 return None;
954 }
955 let site = ctx.site_near(home, HOME_RADIUS)?;
956 ctx.build_task(from, site, BuildKind::Structure(StructureKind::Campfire))
957 }
958
959 pub fn food_to_fetch(reservations: &Reservations, food_tiles: &HashSet<UVec2>) -> bool {
960 food_tiles
961 .iter()
962 .any(|&p| reservations.is_free(Target::Pickup(p)))
963 }
964
965 pub fn input_source(ctx: &BuildContext, from: UVec2, recipe: Recipe) -> Option<UVec2> {
966 let sources: HashSet<UVec2> = ctx
967 .items
968 .iter()
969 .filter(|(item, pos)| {
970 recipe.accepts(item.kind)
971 && item.count >= recipe.input()
972 && ctx.reservations.is_free(Target::Pickup(pos.0))
973 })
974 .map(|(_, pos)| pos.0)
975 .collect();
976 ctx.reach.nearest(ctx.map, from, SEARCH_TILES, &sources)
977 }
978
979 pub const BUTCHER_WORK: u32 = 240;
980
981 pub fn find_butchering(
982 map: &WorldMap,
983 reservations: &Reservations,
984 items: &[(Item, TilePos)],
985 from: UVec2,
986 ) -> Option<Task> {
987 let carcasses: HashSet<UVec2> = items
988 .iter()
989 .filter(|(item, pos)| {
990 item.kind == ItemKind::Carcass && reservations.is_free(Target::Pickup(pos.0))
991 })
992 .map(|(_, pos)| pos.0)
993 .collect();
994 let site = nearest_of(map, from, SEARCH_TILES, &carcasses)?;
995 Some(Task::Butcher { site, progress: 0 })
996 }
997
998 pub fn find_workbench(ctx: &BuildContext, from: UVec2) -> Option<Task> {
999 if ctx.count(StructureKind::Workbench) > 0 {
1000 return None;
1001 }
1002 let home = ctx.zones.home(from);
1003 let site = ctx.site_near(home, HOME_RADIUS * 1.5)?;
1004 ctx.build_task(from, site, BuildKind::Structure(StructureKind::Workbench))
1005 }
1006
1007 pub fn tool_source(
1008 map: &WorldMap,
1009 reservations: &Reservations,
1010 items: &[(Item, TilePos)],
1011 from: UVec2,
1012 ) -> Option<UVec2> {
1013 let tools: HashSet<UVec2> = items
1014 .iter()
1015 .filter(|(item, pos)| {
1016 item.kind == ItemKind::Tools && reservations.is_free(Target::Pickup(pos.0))
1017 })
1018 .map(|(_, pos)| pos.0)
1019 .collect();
1020 if tools.is_empty() {
1021 return None;
1022 }
1023 nearest_of(map, from, SEARCH_TILES, &tools)
1024 }
1025
1026 pub fn find_crafting(ctx: &BuildContext, from: UVec2, recipe: Recipe) -> Option<Task> {
1027 let station = ctx
1028 .structures
1029 .iter()
1030 .filter(|&&(pos, kind, fuel)| {
1031 kind == recipe.station()
1032 && fuel >= COOK_MIN_FUEL
1033 && ctx.reservations.is_free(Target::Station(pos))
1034 })
1035 .map(|&(pos, ..)| pos)
1036 .min_by(|a, b| {
1037 a.as_vec2()
1038 .distance_squared(from.as_vec2())
1039 .total_cmp(&b.as_vec2().distance_squared(from.as_vec2()))
1040 })?;
1041 let pickup = input_source(ctx, from, recipe)?;
1042 Some(Task::Craft {
1043 recipe,
1044 pickup: Some(pickup),
1045 station,
1046 progress: 0,
1047 })
1048 }
1049
1050 pub fn find_lighting(ctx: &BuildContext, from: UVec2, dark: bool) -> Option<Task> {
1051 let home = ctx.zones.home(from);
1052 let (kind, site) = if ctx.campfire_needed(home) {
1053 (StructureKind::Campfire, ctx.site_near(home, HOME_RADIUS)?)
1054 } else if !dark {
1055 return None;
1056 } else {
1057 let dark = ctx.darkest_stockpile_tile(home)?;
1058 let reach = StructureKind::Torch.radius() * 0.6;
1059 (StructureKind::Torch, ctx.site_near(dark, reach)?)
1060 };
1061 ctx.build_task(from, site, BuildKind::Structure(kind))
1062 }
1063
1064 pub const DANGER_RANGE: i32 = 6;
1065 pub const DEFEND_RANGE: i32 = 15;
1066 const FLEE_STEPS: f32 = 10.0;
1067
1068 fn chebyshev(a: UVec2, b: UVec2) -> i32 {
1069 (a.as_ivec2() - b.as_ivec2()).abs().max_element()
1070 }
1071
1072 pub fn flee_from(map: &WorldMap, from: UVec2, threat: UVec2) -> Option<Vec<UVec2>> {
1073 let away = (from.as_ivec2() - threat.as_ivec2())
1074 .as_vec2()
1075 .normalize_or(Vec2::X);
1076 let goal = (from.as_ivec2() + (away * FLEE_STEPS).round().as_ivec2())
1077 .clamp(IVec2::ZERO, map.size.as_ivec2() - 1);
1078 let open = nearest_tile(map, goal.as_uvec2(), 200, |p| {
1079 map.move_cost(p.as_ivec2()).is_some()
1080 })?;
1081 find_path(map, from, open).filter(|route| !route.is_empty())
1082 }
1083
1084 pub fn chilly_soon(tick: SimTick) -> bool {
1085 let later = SimTick(tick.0 + LIGHT_LOOKAHEAD_TICKS);
1086 outdoor_temperature(tick) < COLD_TEMP || outdoor_temperature(later) < COLD_TEMP
1087 }
1088
1089 pub fn find_warm_spot(
1090 map: &WorldMap,
1091 structures: &[(UVec2, StructureKind, u32)],
1092 outdoor: f32,
1093 from: UVec2,
1094 ) -> Option<UVec2> {
1095 let fires: Vec<(UVec2, StructureKind)> = structures
1096 .iter()
1097 .filter(|(_, kind, _)| kind.heat() > 0.0)
1098 .map(|&(pos, kind, _)| (pos, kind))
1099 .collect();
1100 let taken: HashSet<UVec2> = structures.iter().map(|(pos, ..)| *pos).collect();
1101 let warm: HashSet<UVec2> = fires
1102 .iter()
1103 .flat_map(|&(pos, kind)| {
1104 let r = kind.radius().ceil() as i32;
1105 (-r..=r).flat_map(move |dy| (-r..=r).map(move |dx| pos.as_ivec2() + IVec2::new(dx, dy)))
1106 })
1107 .filter(|&p| map.in_bounds(p) && map.move_cost(p).is_some())
1108 .map(|p| p.as_uvec2())
1109 .filter(|p| !taken.contains(p))
1110 .filter(|&p| tile_temperature(map, outdoor, p, fires.iter().copied()) >= WARM_TEMP)
1111 .collect();
1112 nearest_of(map, from, SEARCH_TILES, &warm)
1113 }
1114
1115 pub fn find_refuel(ctx: &BuildContext, from: UVec2) -> Option<Task> {
1116 let home = ctx.zones.home(from);
1117 let (site, kind, _) = ctx
1118 .structures
1119 .iter()
1120 .filter(|&&(pos, kind, fuel)| {
1121 kind.fuel_ticks()
1122 .is_some_and(|full| fuel < full / REFUEL_BELOW_SHARE)
1123 && pos.as_vec2().distance(home.as_vec2()) <= HOME_RADIUS * 2.0
1124 && ctx.reservations.is_free(Target::Site(pos))
1125 })
1126 .min_by_key(|(.., fuel)| *fuel)?;
1127 ctx.build_task(from, *site, BuildKind::Refuel(*kind))
1128 }
1129
1130 pub fn find_bed_site(ctx: &BuildContext, colonists: usize, from: UVec2) -> Option<Task> {
1131 if ctx.count(StructureKind::Bed) >= colonists {
1132 return None;
1133 }
1134 let anchor = ctx.bed_anchor(ctx.zones.home(from));
1135 let site = ctx.site_near_where(anchor, HOME_RADIUS * 1.5, |p| ctx.clear_of_stockpile(p))?;
1136 ctx.build_task(from, site, BuildKind::Structure(StructureKind::Bed))
1137 }
1138
1139 pub fn find_blueprint_work(ctx: &BuildContext, from: UVec2) -> Option<Task> {
1140 let mut stock: HashMap<ItemKind, u32> = HashMap::new();
1141 for (item, pos) in ctx.items {
1142 if ctx.reservations.is_free(Target::Pickup(pos.0)) {
1143 *stock.entry(item.kind).or_insert(0) += item.count;
1144 }
1145 }
1146 let mut stands: HashSet<UVec2> = HashSet::new();
1147 for (&site, &kind) in &ctx.blueprints.0 {
1148 let (material, amount) = kind.cost();
1149 let affordable = amount == 0 || stock.get(&material).copied().unwrap_or(0) >= amount;
1150 let affordable = affordable && ctx.reach.allows_near(site);
1151 if !affordable || !ctx.reservations.is_free(Target::Site(site)) {
1152 continue;
1153 }
1154 for d in std::iter::once(IVec2::ZERO).chain(NEIGHBORS) {
1155 let stand = site.as_ivec2() + d;
1156 if (d == IVec2::ZERO || ctx.map.move_cost(site.as_ivec2()).is_none())
1157 && ctx.map.move_cost(stand).is_some()
1158 {
1159 stands.insert(stand.as_uvec2());
1160 }
1161 }
1162 }
1163 let reach = search_reach(SEARCH_TILES) as i32;
1164 let in_reach = |p: &UVec2| (p.as_ivec2() - from.as_ivec2()).abs().max_element() <= reach;
1165 if !stands.iter().any(in_reach) {
1166 return None;
1167 }
1168 let mut sources: HashMap<(ItemKind, u32), Option<UVec2>> = HashMap::new();
1169 let mut found = None;
1170 nearest_tile(ctx.map, from, SEARCH_TILES, |p| {
1171 if !stands.contains(&p) {
1172 return false;
1173 }
1174 found = std::iter::once(IVec2::ZERO)
1175 .chain(NEIGHBORS)
1176 .filter_map(|d| {
1177 let site = p.as_ivec2() + d;
1178 (site.min_element() >= 0).then(|| site.as_uvec2())
1179 })
1180 .filter(|&site| site == p || ctx.map.move_cost(site.as_ivec2()).is_none())
1181 .find_map(|site| {
1182 let kind = ctx.blueprints.get(site)?;
1183 if !ctx.reservations.is_free(Target::Site(site)) {
1184 return None;
1185 }
1186 let pickup = match kind.cost() {
1187 (_, 0) => None,
1188 cost => Some(
1189 (*sources
1190 .entry(cost)
1191 .or_insert_with(|| ctx.material_source(from, cost)))?,
1192 ),
1193 };
1194 Some(Task::Build {
1195 site,
1196 kind,
1197 pickup,
1198 progress: 0,
1199 })
1200 });
1201 found.is_some()
1202 })?;
1203 found
1204 }
1205
1206 fn on_the_way(map: &WorldMap, jobs: impl Iterator<Item = Option<Job>>) -> HashMap<ItemKind, u32> {
1207 let mut totals = HashMap::new();
1208 for job in jobs.flatten() {
1209 if let Task::Work { site, .. } = job.task
1210 && let Some(work) = site_work(map, site)
1211 {
1212 *totals.entry(work.item).or_insert(0) += work.count;
1213 }
1214 }
1215 totals
1216 }
1217
1218 #[allow(clippy::too_many_arguments)]
1219 fn choose_jobs(
1220 mut commands: Commands,
1221 time: Res<Time>,
1222 map: Res<WorldMap>,
1223 zones: Res<Zones>,
1224 goals: Res<ColonyGoals>,
1225 item_assets: Res<ItemAssets>,
1226 tick: Res<SimTick>,
1227 blueprints: Res<Blueprints>,
1228 mut rng: ResMut<SimRng>,
1229 mut reservations: ResMut<Reservations>,
1230 mut items: ItemQuery,
1231 structures: Query<(&TilePos, &Structure, Option<&Fuel>), Without<Colonist>>,
1232 animals: Query<(Entity, &TilePos, &Animal, Has<Tame>), Without<Colonist>>,
1233 mut colonists: Query<
1234 (
1235 Entity,
1236 &TilePos,
1237 &mut Path,
1238 &mut CurrentJob,
1239 &mut IdleTimer,
1240 &mut Carrying,
1241 &Rest,
1242 &Hunger,
1243 Option<&Family>,
1244 Has<Child>,
1245 &WorkPriorities,
1246 &Warmth,
1247 Has<Tool>,
1248 &Skills,
1249 &Health,
1250 ),
1251 With<Colonist>,
1252 >,
1253 plans: (Res<Homes>, Res<Designations>, Res<ColonyReach>),
1254 mut gather_memo: Local<GatherMemo>,
1255 ) {
1256 let (homes, designations, reach) = plans;
1257 if zones.is_changed() {
1258 gather_memo.haul = Memo::default();
1259 }
1260 if blueprints.is_changed() {
1261 gather_memo.build = Memo::default();
1262 }
1263 let wolves: Vec<(Entity, UVec2)> = animals
1264 .iter()
1265 .filter(|(_, _, animal, _)| animal.kind.is_predator())
1266 .map(|(entity, pos, ..)| (entity, pos.0))
1267 .collect();
1268 let nearest_wolf = |tile: UVec2, range: i32| {
1269 wolves
1270 .iter()
1271 .map(|&(entity, pos)| (entity, pos, chebyshev(pos, tile)))
1272 .filter(|&(.., d)| d <= range)
1273 .min_by_key(|&(_, pos, d)| (d, pos.y, pos.x))
1274 };
1275 let mut anyone_ready = false;
1276 for (_, tile, path, job, mut idle, _, rest, hunger, ..) in &mut colonists {
1277 let idle_now = job.0.is_none() && path.0.is_empty();
1278 if idle_now {
1279 idle.0.tick(time.delta());
1280 }
1281 anyone_ready |= (idle_now && idle.0.is_finished())
1282 || rest.0 < EXHAUSTED_REST
1283 || hunger.0 < STARVING
1284 || nearest_wolf(tile.0, DANGER_RANGE).is_some();
1285 }
1286 if !anyone_ready {
1287 return;
1288 }
1289 let wildlife: Vec<Wildlife> = animals
1290 .iter()
1291 .map(|(e, pos, a, tame)| (e, pos.0, a.kind, tame))
1292 .collect();
1293 let adults = colonists.iter().filter(|c| !c.9).count();
1294 let structures_list: Vec<(UVec2, StructureKind, u32)> = structures
1295 .iter()
1296 .map(|(pos, s, fuel)| (pos.0, s.kind, fuel.map_or(u32::MAX, |f| f.0)))
1297 .collect();
1298 let mut planned: Vec<(UVec2, StructureKind)> = colonists
1299 .iter()
1300 .filter_map(|c| match c.3.0?.task {
1301 Task::Build {
1302 site,
1303 kind: BuildKind::Structure(kind),
1304 ..
1305 } => Some((site, kind)),
1306 _ => None,
1307 })
1308 .collect();
1309 let dark = dark_soon(*tick);
1310 let chilly = chilly_soon(*tick);
1311 let outdoor = outdoor_temperature(*tick);
1312 let sowing = sowing_season(*tick);
1313 let carried: Vec<Item> = colonists.iter().filter_map(|c| c.5.0).collect();
1314 let home = zones.home(map.size / 2);
1315 let mut stocks: HashMap<ItemKind, u32> = ItemKind::ALL.iter().map(|&k| (k, 0)).collect();
1316 let in_store = items
1317 .iter()
1318 .filter(|(_, _, pos)| in_colony(home, pos.0))
1319 .map(|(_, item, _)| item)
1320 .chain(carried.iter());
1321 for item in in_store {
1322 *stocks.entry(item.kind).or_insert(0) += item.count;
1323 }
1324 let mut pending = on_the_way(&map, colonists.iter().map(|c| c.3.0));
1325 for job in colonists.iter().filter_map(|c| c.3.0) {
1326 match job.task {
1327 Task::Hunt { kind, .. } => *pending.entry(ItemKind::Meat).or_insert(0) += kind.meat(),
1328 Task::Craft { recipe, .. } => *pending.entry(recipe.output()).or_insert(0) += 1,
1329 _ => {}
1330 }
1331 }
1332 let mut food_tiles: Option<HashSet<UVec2>> = None;
1333 let hour = hour_of_day(*tick);
1334 let mut occupied: Option<HashSet<UVec2>> = None;
1335 for (
1336 entity,
1337 tile,
1338 mut path,
1339 mut job,
1340 mut idle,
1341 mut carrying,
1342 rest,
1343 hunger,
1344 family,
1345 child,
1346 priorities,
1347 warmth,
1348 has_tool,
1349 skills,
1350 health,
1351 ) in &mut colonists
1352 {
1353 let exhausted = rest.0 < EXHAUSTED_REST;
1354 let starving = hunger.0 < STARVING && {
1355 let tiles = food_tiles.get_or_insert_with(|| {
1356 items
1357 .iter()
1358 .filter(|(_, item, _)| item.kind.is_food())
1359 .map(|(_, _, pos)| pos.0)
1360 .collect()
1361 });
1362 food_to_fetch(&reservations, tiles)
1363 };
1364 let danger = nearest_wolf(tile.0, DANGER_RANGE);
1365 let urgent = exhausted || starving || danger.is_some();
1366 let fighting_wolf = |task: Task| {
1367 matches!(task, Task::Hunt { kind, .. } if kind.is_predator())
1368 || matches!(task, Task::MoveTo(_))
1369 };
1370 let interrupt = |j: Job| {
1371 !j.ordered
1372 && if danger.is_some() {
1373 !fighting_wolf(j.task)
1374 } else {
1375 !matches!(j.task, Task::Sleep { .. } | Task::Eat { .. })
1376 }
1377 };
1378 if urgent && job.0.is_some_and(interrupt) {
1379 reservations.release_all(entity);
1380 job.0 = None;
1381 path.0.truncate(1);
1382 }
1383 let rested = idle.0.is_finished();
1384 if job.0.is_some() || !path.0.is_empty() || !(rested || urgent) {
1385 continue;
1386 }
1387 let snapshot: Vec<(Item, TilePos)> =
1388 items.iter().map(|(_, item, pos)| (*item, *pos)).collect();
1389 let refs = snapshot.iter().map(|(i, p)| (i, p));
1390 let wanted_kinds = ItemKind::ALL.map(|kind| goals.wanted(kind, &stocks, &pending));
1391 let wanted = |kind: ItemKind| wanted_kinds[kind.index()];
1392 let sleep = SleepContext {
1393 map: &map,
1394 zones: &zones,
1395 reservations: &reservations,
1396 items: &snapshot,
1397 structures: &structures_list,
1398 blueprints: &blueprints,
1399 homes: &homes,
1400 family: family.copied(),
1401 child,
1402 };
1403 let food = wants_food(hunger.0)
1404 .then(|| find_food(&map, &reservations, &snapshot, tile.0))
1405 .flatten();
1406 let fighter = !child && health.0 >= INJURED && priorities.allows(WorkType::Food);
1407 let free_to_fight = fighter && !exhausted && !wants_food(hunger.0);
1408 let defend =
1409 danger
1410 .or_else(|| nearest_wolf(home, DEFEND_RANGE))
1411 .filter(|&(wolf, pos, _)| {
1412 free_to_fight
1413 && reach.allows_near(pos)
1414 && reservations.is_free(Target::Animal(wolf))
1415 });
1416 let flee = danger
1417 .filter(|_| !fighter)
1418 .and_then(|(_, wolf, _)| flee_from(&map, tile.0, wolf));
1419 let task = if let Some(route) = flee {
1420 let goal = route.last().copied().unwrap_or(tile.0);
1421 path.0 = route.into();
1422 Some(Task::MoveTo(goal))
1423 } else if let Some((wolf, ..)) = defend {
1424 Some(Task::Hunt {
1425 animal: wolf,
1426 kind: AnimalKind::Wolf,
1427 progress: 0,
1428 })
1429 } else if exhausted {
1430 Some(Task::Sleep {
1431 spot: sleep.spot(tile.0),
1432 })
1433 } else if let Some(pickup) = food {
1434 if let Some(load) = carrying.0.take() {
1435 drop_items(&mut commands, &mut items, &item_assets, tile.0, load);
1436 }
1437 Some(Task::Eat {
1438 pickup: Some(pickup),
1439 progress: 0,
1440 })
1441 } else if wants_sleep(rest.0, hour) {
1442 Some(Task::Sleep {
1443 spot: sleep.spot(tile.0),
1444 })
1445 } else if let Some(spot) = warmth
1446 .cold()
1447 .then(|| find_warm_spot(&map, &structures_list, outdoor, tile.0))
1448 .flatten()
1449 .filter(|&spot| spot != tile.0)
1450 {
1451 find_path(&map, tile.0, spot).map(|route| {
1452 path.0 = route.into();
1453 Task::MoveTo(spot)
1454 })
1455 } else if child {
1456 None
1457 } else if let Some(load) = carrying.0 {
1458 let storage =
1459 find_storage(&map, &zones, &reservations, &reach, refs, tile.0, load.kind);
1460 if storage.is_none() {
1461 drop_items(&mut commands, &mut items, &item_assets, tile.0, load);
1462 carrying.0 = None;
1463 }
1464 storage.map(|to| Task::Haul { pickup: None, to })
1465 } else if let Some(pickup) = (!has_tool)
1466 .then(|| tool_source(&map, &reservations, &snapshot, tile.0))
1467 .flatten()
1468 {
1469 Some(Task::Equip { pickup })
1470 } else {
1471 let occupied = occupied.get_or_insert_with(|| {
1472 structures_list
1473 .iter()
1474 .map(|(pos, ..)| *pos)
1475 .chain(items.iter().map(|(_, _, pos)| pos.0))
1476 .collect()
1477 });
1478 occupied.extend(planned.iter().map(|(pos, _)| *pos));
1479 let build = BuildContext {
1480 reach: &reach,
1481 map: &map,
1482 zones: &zones,
1483 reservations: &reservations,
1484 items: &snapshot,
1485 structures: &structures_list,
1486 planned: &planned,
1487 blueprints: &blueprints,
1488 occupied,
1489 };
1490 let has_input = |recipe| input_source(&build, tile.0, recipe).is_some();
1491 let cooking = || {
1492 if !wanted(ItemKind::Meal) {
1493 return None;
1494 }
1495 find_crafting(&build, tile.0, Recipe::Meal).or_else(|| {
1496 (build.campfire_needed(zones.home(tile.0)) && has_input(Recipe::Meal))
1497 .then(|| find_campfire(&build, tile.0))
1498 .flatten()
1499 })
1500 };
1501 let toolmaking = || {
1502 if !wanted(ItemKind::Tools) {
1503 return None;
1504 }
1505 find_crafting(&build, tile.0, Recipe::Tools).or_else(|| {
1506 (build.count(StructureKind::Workbench) == 0 && has_input(Recipe::Tools))
1507 .then(|| find_workbench(&build, tile.0))
1508 .flatten()
1509 })
1510 };
1511 let designated = || {
1512 (!designations.0.is_empty())
1513 .then(|| {
1514 find_work(&map, &reservations, tile.0, |site, _| {
1515 designations.0.contains(&site)
1516 })
1517 })
1518 .flatten()
1519 };
1520 let gather_mask = ItemKind::ALL
1521 .iter()
1522 .filter(|&&k| wanted(k))
1523 .fold(0u32, |mask, k| mask | 1 << k.index());
1524 let food_mask = gather_mask
1525 & ItemKind::ALL
1526 .iter()
1527 .filter(|k| k.is_food())
1528 .fold(0u32, |mask, k| mask | 1 << k.index());
1529 let skip_gather = gather_memo.gather.skips(gather_mask, tick.0);
1530 let skip_forage = gather_memo.forage.skips(food_mask, tick.0);
1531 let skip_haul = gather_memo.haul.skips(0, tick.0);
1532 let skip_build = gather_memo.build.skips(0, tick.0);
1533 let build_failed = std::cell::Cell::new(false);
1534 let gather_failed = std::cell::Cell::new(false);
1535 let forage_failed = std::cell::Cell::new(false);
1536 let haul_failed = std::cell::Cell::new(false);
1537 let candidates: [(WorkType, Option<Skill>, &dyn Fn() -> Option<Task>); 16] = [
1538 (WorkType::Haul, None, &|| {
1539 find_expansion(&map, &zones, &reservations, &snapshot, &blueprints)
1540 }),
1541 (WorkType::Build, None, &|| find_refuel(&build, tile.0)),
1542 (WorkType::Build, None, &|| {
1543 (dark || chilly)
1544 .then(|| find_lighting(&build, tile.0, dark))
1545 .flatten()
1546 }),
1547 (WorkType::Food, Some(Skill::Cooking), &cooking),
1548 (WorkType::Food, None, &|| {
1549 find_butchering(&map, &reservations, &snapshot, tile.0)
1550 }),
1551 (WorkType::Food, None, &|| {
1552 if food_mask == 0 || skip_forage {
1553 return None;
1554 }
1555 let food = |_, k: ItemKind| k.is_food() && wanted(k);
1556 let work =
1557 find_work_within(&map, &reservations, tile.0, FORAGE_SEARCH_TILES, food);
1558 forage_failed.set(work.is_none());
1559 work
1560 }),
1561 (WorkType::Gather, None, &designated),
1562 (WorkType::Food, None, &|| {
1563 (sowing && wanted(ItemKind::Vegetables))
1564 .then(|| find_sowing(&map, &zones, &reservations, tile.0))
1565 .flatten()
1566 }),
1567 (WorkType::Build, Some(Skill::Construction), &|| {
1568 find_bed_site(&build, adults, tile.0)
1569 }),
1570 (WorkType::Craft, Some(Skill::Crafting), &toolmaking),
1571 (WorkType::Build, Some(Skill::Construction), &|| {
1572 if skip_build {
1573 return None;
1574 }
1575 let work = find_blueprint_work(&build, tile.0);
1576 build_failed.set(work.is_none());
1577 work
1578 }),
1579 (WorkType::Gather, None, &|| {
1580 if gather_mask == 0 || skip_gather {
1581 return None;
1582 }
1583 let work = find_work(&map, &reservations, tile.0, |_, k| wanted(k));
1584 gather_failed.set(work.is_none());
1585 work
1586 }),
1587 (WorkType::Food, Some(Skill::Hunting), &|| {
1588 wanted(ItemKind::Meat)
1589 .then(|| find_hunt(&map, &reservations, &wildlife, tile.0))
1590 .flatten()
1591 }),
1592 (WorkType::Food, Some(Skill::Farming), &|| {
1593 sowing
1594 .then(|| find_sowing(&map, &zones, &reservations, tile.0))
1595 .flatten()
1596 }),
1597 (WorkType::Tame, Some(Skill::Taming), &|| {
1598 (!wanted(ItemKind::Meat))
1599 .then(|| find_taming(&map, &reservations, &wildlife, tile.0))
1600 .flatten()
1601 }),
1602 (WorkType::Haul, None, &|| {
1603 if skip_haul {
1604 return None;
1605 }
1606 let haul = find_haul(&map, &zones, &reservations, &reach, &snapshot, tile.0);
1607 haul_failed.set(haul.is_none());
1608 haul
1609 }),
1610 ];
1611 let poor = |skill: &Option<Skill>| skill.is_some_and(|s| skills.level(s) < POOR_LEVEL);
1612 let allowed = candidates
1613 .iter()
1614 .filter(|(work, ..)| priorities.allows(*work));
1615 let task = allowed
1616 .clone()
1617 .filter(|(_, skill, _)| !poor(skill))
1618 .chain(allowed.filter(|(_, skill, _)| poor(skill)))
1619 .find_map(|(.., find)| find());
1620 let retry = |mask, ticks| Memo {
1621 mask,
1622 until: tick.0 + ticks,
1623 };
1624 if gather_failed.get() {
1625 gather_memo.gather = retry(gather_mask, GATHER_RETRY_TICKS);
1626 }
1627 if forage_failed.get() {
1628 gather_memo.forage = retry(food_mask, GATHER_RETRY_TICKS);
1629 }
1630 if haul_failed.get() {
1631 gather_memo.haul = retry(0, HAUL_RETRY_TICKS);
1632 }
1633 if build_failed.get() {
1634 gather_memo.build = retry(0, HAUL_RETRY_TICKS);
1635 }
1636 task
1637 };
1638 if let Some(task) = task {
1639 if let Task::Work { site, .. } = task
1640 && let Some(work) = site_work(&map, site)
1641 {
1642 *pending.entry(work.item).or_insert(0) += work.count;
1643 }
1644 match task {
1645 Task::Hunt { kind, .. } => {
1646 *pending.entry(ItemKind::Meat).or_insert(0) += kind.meat()
1647 }
1648 Task::Craft { recipe, .. } => *pending.entry(recipe.output()).or_insert(0) += 1,
1649 _ => {}
1650 }
1651 if let Task::Build {
1652 site,
1653 kind: BuildKind::Structure(kind),
1654 ..
1655 } = task
1656 {
1657 planned.push((site, kind));
1658 }
1659 reserve_task(&mut reservations, task, entity);
1660 job.0 = Some(Job {
1661 task,
1662 ordered: false,
1663 });
1664 continue;
1665 }
1666 let goal = tile.0.as_ivec2()
1667 + IVec2::new(
1668 rng.range_i32(-WANDER_RADIUS..WANDER_RADIUS + 1),
1669 rng.range_i32(-WANDER_RADIUS..WANDER_RADIUS + 1),
1670 );
1671 if map.in_bounds(goal)
1672 && let Some(wander_path) =
1673 find_nearby_path(&map, tile.0, goal.as_uvec2(), WANDER_SEARCH)
1674 {
1675 path.0 = wander_path.into();
1676 }
1677 idle.restart(rng.range_f32(IDLE_SECS));
1678 }
1679 }
1680
1681 enum Step {
1682 Continue,
1683 Done,
1684 }
1685
1686 fn walk_to(map: &WorldMap, from: UVec2, to: UVec2, path: &mut Path) -> Step {
1687 match find_path(map, from, to) {
1688 Some(p) if !p.is_empty() => {
1689 path.0 = p.into();
1690 Step::Continue
1691 }
1692 _ => Step::Done,
1693 }
1694 }
1695
1696 #[allow(clippy::too_many_arguments)]
1697 fn run_jobs(
1698 mut commands: Commands,
1699 mut map: ResMut<WorldMap>,
1700 mut zones: ResMut<Zones>,
1701 mut reservations: ResMut<Reservations>,
1702 mut rng: ResMut<SimRng>,
1703 item_assets: Res<ItemAssets>,
1704 structure_assets: Res<StructureAssets>,
1705 light: Res<LightMap>,
1706 tick: Res<SimTick>,
1707 mut blueprints: ResMut<Blueprints>,
1708 mut goals: ResMut<ColonyGoals>,
1709 mut items: ItemQuery,
1710 mut structures: Query<(Entity, &TilePos, &Structure, Option<&mut Fuel>), Without<Colonist>>,
1711 animals: Query<(&TilePos, &Path), (With<Animal>, Without<Colonist>)>,
1712 messages: (MessageWriter<TileChanged>, MessageWriter<Ate>),
1713 mut colonists: Query<
1714 (
1715 Entity,
1716 &TilePos,
1717 &mut Path,
1718 &mut CurrentJob,
1719 &mut IdleTimer,
1720 &mut Carrying,
1721 &Rest,
1722 &mut Hunger,
1723 Has<Sick>,
1724 Option<&mut Tool>,
1725 (&mut Skills, &mut WorkCarry),
1726 ),
1727 With<Colonist>,
1728 >,
1729 ) {
1730 let (mut changed, mut ate) = messages;
1731 let hour = hour_of_day(*tick);
1732 let walling = colonists.iter().any(|c| {
1733 c.3.0
1734 .is_some_and(|j| matches!(j.task, Task::Build { kind, .. } if kind.blocks_tile()))
1735 });
1736 let occupied: Vec<UVec2> = colonists
1737 .iter()
1738 .filter(|_| walling)
1739 .flat_map(|c| [Some(c.1.0), c.2.0.front().copied()])
1740 .chain(
1741 animals
1742 .iter()
1743 .filter(|_| walling)
1744 .flat_map(|(t, p)| [Some(t.0), p.0.front().copied()]),
1745 )
1746 .flatten()
1747 .collect();
1748 for (
1749 entity,
1750 tile,
1751 mut path,
1752 mut current,
1753 mut idle,
1754 mut carrying,
1755 rest,
1756 mut hunger,
1757 sick,
1758 mut tool,
1759 (mut skills, mut carry),
1760 ) in &mut colonists
1761 {
1762 let Some(mut job) = current.0 else {
1763 continue;
1764 };
1765 let rate = match work_rate(&light, tile.0) {
1766 rate if sick => rate.div_ceil(2),
1767 rate => rate,
1768 };
1769 let rate = tool_rate(rate, tool.is_some());
1770 if !path.0.is_empty() {
1771 continue;
1772 }
1773 let skill = Skill::of(&job.task);
1774 let rate = carry.apply(rate, skill.map_or(1.0, |s| speed(skills.level(s))));
1775 let progress_before = job.task.progress();
1776 if let Some(tool) = tool.as_mut()
1777 && !matches!(
1778 job.task,
1779 Task::MoveTo(_) | Task::Sleep { .. } | Task::Eat { .. } | Task::Equip { .. }
1780 )
1781 {
1782 tool.wear += 1;
1783 if tool.wear >= TOOL_LIFE_TICKS {
1784 commands.entity(entity).remove::<Tool>();
1785 }
1786 }
1787 let step = match &mut job.task {
1788 Task::MoveTo(_) => Step::Done,
1789 Task::Equip { pickup } => {
1790 let from = *pickup;
1791 if reservations.holder(Target::Pickup(from)) != Some(entity) {
1792 Step::Done
1793 } else if tile.0 != from {
1794 walk_to(&map, tile.0, from, &mut path)
1795 } else {
1796 reservations.release(Target::Pickup(from));
1797 let stack = items
1798 .iter()
1799 .any(|(_, item, pos)| pos.0 == from && item.kind == ItemKind::Tools);
1800 if stack && take_items(&mut commands, &mut items, from, 1).is_some() {
1801 goals.record_use(ItemKind::Tools, 1);
1802 commands.entity(entity).insert(Tool::default());
1803 }
1804 Step::Done
1805 }
1806 }
1807 Task::Craft {
1808 recipe,
1809 pickup,
1810 station,
1811 progress,
1812 } => {
1813 let (recipe, station) = (*recipe, *station);
1814 let input = recipe.input();
1815 let fire = structures
1816 .iter()
1817 .any(|(_, pos, s, _)| pos.0 == station && s.kind == recipe.station());
1818 let ours = reservations.holder(Target::Station(station)) == Some(entity);
1819 if !fire || !ours {
1820 Step::Done
1821 } else if let Some(from) = *pickup {
1822 if reservations.holder(Target::Pickup(from)) != Some(entity) {
1823 Step::Done
1824 } else if tile.0 != from {
1825 walk_to(&map, tile.0, from, &mut path)
1826 } else {
1827 let load = take_items(&mut commands, &mut items, from, input)
1828 .filter(|load| recipe.accepts(load.kind) && load.count > 0);
1829 carrying.0 = load;
1830 reservations.release(Target::Pickup(from));
1831 match load {
1832 Some(load) if load.count >= input => {
1833 *pickup = None;
1834 Step::Continue
1835 }
1836 _ => Step::Done,
1837 }
1838 }
1839 } else if !carrying
1840 .0
1841 .is_some_and(|load| recipe.accepts(load.kind) && load.count >= input)
1842 {
1843 Step::Done
1844 } else if !is_adjacent(tile.0.as_ivec2(), station.as_ivec2()) {
1845 match find_path_adjacent(&map, tile.0, station) {
1846 Some(p) if !p.is_empty() => {
1847 path.0 = p.into();
1848 Step::Continue
1849 }
1850 _ => Step::Done,
1851 }
1852 } else {
1853 *progress += rate;
1854 if *progress < recipe.work() {
1855 Step::Continue
1856 } else {
1857 if let Some(load) = carrying.0 {
1858 goals.record_use(load.kind, input);
1859 }
1860 carrying.0 = carrying.0.and_then(|load| {
1861 (load.count > input).then_some(Item {
1862 count: load.count - input,
1863 ..load
1864 })
1865 });
1866 drop_items(
1867 &mut commands,
1868 &mut items,
1869 &item_assets,
1870 tile.0,
1871 Item::new(recipe.output(), 1),
1872 );
1873 Step::Done
1874 }
1875 }
1876 }
1877 Task::Hunt {
1878 animal,
1879 kind,
1880 progress,
1881 } => match animals.get(*animal) {
1882 Ok((prey, _)) if reservations.holder(Target::Animal(*animal)) == Some(entity) => {
1883 let prey = prey.0;
1884 let in_range = tile.0.as_vec2().distance(prey.as_vec2()) <= HUNT_RANGE
1885 && line_of_sight(&map, tile.0, prey);
1886 let stale = path.0.back().is_none_or(|end| {
1887 end.as_vec2().distance(prey.as_vec2()) > HUNT_RANGE / 2.0
1888 });
1889 if !in_range && !stale {
1890 Step::Continue
1891 } else if !in_range {
1892 match find_path_adjacent(&map, tile.0, prey) {
1893 Some(p) if !p.is_empty() => {
1894 path.0 = p.into();
1895 Step::Continue
1896 }
1897 _ => Step::Done,
1898 }
1899 } else {
1900 path.0.truncate(1);
1901 *progress += rate;
1902 if *progress < kind.hunt_work() {
1903 Step::Continue
1904 } else {
1905 commands.entity(*animal).despawn();
1906 drop_items(
1907 &mut commands,
1908 &mut items,
1909 &item_assets,
1910 prey,
1911 Item::new(ItemKind::Carcass, kind.meat()),
1912 );
1913 Step::Done
1914 }
1915 }
1916 }
1917 _ => Step::Done,
1918 },
1919 Task::Tame {
1920 animal,
1921 kind,
1922 progress,
1923 } => match animals.get(*animal) {
1924 Ok((prey, _)) if reservations.holder(Target::Animal(*animal)) == Some(entity) => {
1925 let prey = prey.0;
1926 if prey != tile.0 && !is_adjacent(tile.0.as_ivec2(), prey.as_ivec2()) {
1927 if path.0.is_empty() {
1928 match find_path_adjacent(&map, tile.0, prey) {
1929 Some(p) if !p.is_empty() => {
1930 path.0 = p.into();
1931 Step::Continue
1932 }
1933 _ => Step::Done,
1934 }
1935 } else {
1936 Step::Continue
1937 }
1938 } else {
1939 *progress += rate;
1940 if *progress < kind.tame_work() {
1941 Step::Continue
1942 } else {
1943 commands.entity(*animal).insert(Tame);
1944 Step::Done
1945 }
1946 }
1947 }
1948 _ => Step::Done,
1949 },
1950 Task::Butcher { site, progress } => {
1951 let site = *site;
1952 if reservations.holder(Target::Pickup(site)) != Some(entity) {
1953 Step::Done
1954 } else if tile.0 != site {
1955 walk_to(&map, tile.0, site, &mut path)
1956 } else {
1957 *progress += rate;
1958 if *progress < BUTCHER_WORK {
1959 Step::Continue
1960 } else {
1961 let is_carcass = items
1962 .iter()
1963 .any(|(_, item, pos)| pos.0 == site && item.kind == ItemKind::Carcass);
1964 if is_carcass
1965 && let Some(carcass) =
1966 take_items(&mut commands, &mut items, site, u32::MAX)
1967 {
1968 let meat = Item {
1969 kind: ItemKind::Meat,
1970 ..carcass
1971 };
1972 drop_items(&mut commands, &mut items, &item_assets, site, meat);
1973 }
1974 Step::Done
1975 }
1976 }
1977 }
1978 Task::Eat { pickup, progress } => {
1979 if let Some(from) = *pickup {
1980 if reservations.holder(Target::Pickup(from)) != Some(entity) {
1981 Step::Done
1982 } else if tile.0 != from {
1983 walk_to(&map, tile.0, from, &mut path)
1984 } else {
1985 let kind = items
1986 .iter()
1987 .find(|(_, item, pos)| pos.0 == from && item.kind.is_food())
1988 .map(|(_, item, _)| item.kind);
1989 let meal = kind.and_then(|kind| {
1990 take_items(&mut commands, &mut items, from, portion(kind, hunger.0))
1991 });
1992 reservations.release(Target::Pickup(from));
1993 match meal.filter(|m| m.kind.is_food()) {
1994 Some(meal) => {
1995 carrying.0 = Some(meal);
1996 *pickup = None;
1997 Step::Continue
1998 }
1999 None => Step::Done,
2000 }
2001 }
2002 } else if let Some(meal) = carrying.0.filter(|m| m.kind.is_food()) {
2003 *progress += 1;
2004 if *progress < EAT_TICKS {
2005 Step::Continue
2006 } else {
2007 hunger.0 = (hunger.0 + meal.count as f32 * meal.kind.nutrition()).min(1.0);
2008 goals.record_use(meal.kind, meal.count);
2009 ate.write(Ate {
2010 colonist: entity,
2011 kind: meal.kind,
2012 });
2013 carrying.0 = None;
2014 Step::Done
2015 }
2016 } else {
2017 Step::Done
2018 }
2019 }
2020 Task::Sleep { spot } => {
2021 if reservations.holder(Target::SleepSpot(*spot)) != Some(entity) {
2022 Step::Done
2023 } else if tile.0 != *spot {
2024 let step = walk_to(&map, tile.0, *spot, &mut path);
2025 if matches!(step, Step::Done) {
2026 reservations.release(Target::SleepSpot(*spot));
2027 *spot = tile.0;
2028 reservations.reserve(Target::SleepSpot(*spot), entity);
2029 }
2030 Step::Continue
2031 } else if done_sleeping(rest.0, hour) {
2032 Step::Done
2033 } else {
2034 Step::Continue
2035 }
2036 }
2037 Task::Work {
2038 site,
2039 kind,
2040 progress,
2041 } => {
2042 let (site, kind) = (*site, *kind);
2043 let work = site_work(&map, site).filter(|w| w.kind == kind);
2044 let valid =
2045 work.is_some() || (kind == WorkKind::Sow && sowable(&map, &zones, site));
2046 let ours = reservations.holder(Target::Site(site)) == Some(entity);
2047 if !valid || !ours {
2048 Step::Done
2049 } else if !is_adjacent(tile.0.as_ivec2(), site.as_ivec2()) {
2050 match find_path_adjacent(&map, tile.0, site) {
2051 Some(p) if !p.is_empty() => {
2052 path.0 = p.into();
2053 Step::Continue
2054 }
2055 _ => Step::Done,
2056 }
2057 } else {
2058 *progress += rate;
2059 if *progress < kind.work() {
2060 Step::Continue
2061 } else {
2062 kind.complete(&mut map, site);
2063 changed.write(TileChanged(site));
2064 let step = site.as_ivec2() - tile.0.as_ivec2();
2065 let drop_at = if can_step(&map, tile.0.as_ivec2(), step) {
2066 site
2067 } else {
2068 tile.0
2069 };
2070 if let Some(work) = work {
2071 drop_items(
2072 &mut commands,
2073 &mut items,
2074 &item_assets,
2075 drop_at,
2076 Item::new(work.item, work.count),
2077 );
2078 }
2079 Step::Done
2080 }
2081 }
2082 }
2083 Task::ExpandStockpile { site, progress } => {
2084 if tile.0 != *site {
2085 walk_to(&map, tile.0, *site, &mut path)
2086 } else {
2087 *progress += rate;
2088 if *progress >= EXPAND_WORK {
2089 if zones.has_stockpile() {
2090 let excluded = blueprints.tiles();
2091 let tiles = plan_expansion(&map, &zones, &excluded, EXPANSION_TILES);
2092 zones.add_tiles(&map, tiles);
2093 } else {
2094 zones.add_tiles(&map, new_stockpile_tiles(*site));
2095 }
2096 Step::Done
2097 } else {
2098 Step::Continue
2099 }
2100 }
2101 }
2102 Task::Build {
2103 site,
2104 kind,
2105 pickup,
2106 progress,
2107 } => {
2108 let (site, kind) = (*site, *kind);
2109 let ours = reservations.holder(Target::Site(site)) == Some(entity);
2110 let planned = match kind {
2111 BuildKind::Structure(
2112 StructureKind::Campfire
2113 | StructureKind::Torch
2114 | StructureKind::Bed
2115 | StructureKind::Workbench,
2116 )
2117 | BuildKind::Refuel(_) => true,
2118 _ => blueprints.get(site) == Some(kind),
2119 };
2120 let structure_there = structures.iter().any(|(_, pos, ..)| pos.0 == site);
2121 let blocked = map.plant(site.as_ivec2()).is_some()
2122 || match kind {
2123 BuildKind::Roof | BuildKind::Floor => false,
2124 BuildKind::Refuel(fire) => !structures
2125 .iter()
2126 .any(|(_, pos, s, _)| pos.0 == site && s.kind == fire),
2127 BuildKind::Deconstruct => !structure_there,
2128 _ => structure_there,
2129 };
2130 let (material, cost) = kind.cost();
2131 if !ours || blocked || !planned {
2132 Step::Done
2133 } else if let Some(from) = *pickup {
2134 if reservations.holder(Target::Pickup(from)) != Some(entity) {
2135 Step::Done
2136 } else if tile.0 != from {
2137 walk_to(&map, tile.0, from, &mut path)
2138 } else if carrying.0.is_some_and(|load| load.kind != material) {
2139 Step::Done
2140 } else {
2141 let have = carrying.0.map_or(0, |load| load.count);
2142 let taken = take_items(&mut commands, &mut items, from, cost - have)
2143 .filter(|load| load.kind == material)
2144 .map_or(0, |load| load.count);
2145 reservations.release(Target::Pickup(from));
2146 let total = have + taken;
2147 carrying.0 = (total > 0).then_some(Item::new(material, total));
2148 let next = nearest_tile(&map, tile.0, SEARCH_TILES, |p| {
2149 reservations.is_free(Target::Pickup(p))
2150 && items.iter().any(|(_, item, pos)| {
2151 pos.0 == p && item.kind == material && item.count > 0
2152 })
2153 });
2154 match next {
2155 _ if total >= cost => {
2156 *pickup = None;
2157 Step::Continue
2158 }
2159 Some(next) => {
2160 reservations.reserve(Target::Pickup(next), entity);
2161 *pickup = Some(next);
2162 Step::Continue
2163 }
2164 None => Step::Done,
2165 }
2166 }
2167 } else if cost > 0
2168 && !carrying
2169 .0
2170 .is_some_and(|load| load.kind == material && load.count >= cost)
2171 {
2172 Step::Done
2173 } else if !is_adjacent(tile.0.as_ivec2(), site.as_ivec2()) {
2174 match find_path_adjacent(&map, tile.0, site) {
2175 Some(p) if !p.is_empty() => {
2176 path.0 = p.into();
2177 Step::Continue
2178 }
2179 _ => Step::Done,
2180 }
2181 } else {
2182 *progress = (*progress + rate).min(kind.work());
2183 let someone_in_the_way = kind.blocks_tile() && occupied.contains(&site);
2184 if *progress < kind.work() || someone_in_the_way {
2185 Step::Continue
2186 } else {
2187 goals.record_use(material, cost);
2188 carrying.0 = carrying.0.and_then(|load| {
2189 (load.count > cost).then_some(Item {
2190 count: load.count - cost,
2191 ..load
2192 })
2193 });
2194 match kind {
2195 BuildKind::Structure(structure) => {
2196 spawn_structure(&mut commands, &structure_assets, site, structure);
2197 if kind.blocks_tile() {
2198 map.set_wall(site, true);
2199 changed.write(TileChanged(site));
2200 }
2201 }
2202 BuildKind::Roof => {
2203 map.set_roof(site, true);
2204 changed.write(TileChanged(site));
2205 }
2206 BuildKind::Pave => {
2207 map.set(site, Terrain::Paved);
2208 changed.write(TileChanged(site));
2209 }
2210 BuildKind::Floor => {
2211 map.set(site, Terrain::WoodFloor);
2212 changed.write(TileChanged(site));
2213 }
2214 BuildKind::Deconstruct => {
2215 for (entity, pos, structure, _) in &structures {
2216 if pos.0 != site {
2217 continue;
2218 }
2219 commands.entity(entity).despawn();
2220 let (refund, amount) = structure.kind.cost();
2221 if amount / 2 > 0 {
2222 drop_items(
2223 &mut commands,
2224 &mut items,
2225 &item_assets,
2226 site,
2227 Item::new(refund, amount / 2),
2228 );
2229 }
2230 }
2231 if map.is_wall(site.as_ivec2()) {
2232 map.set_wall(site, false);
2233 changed.write(TileChanged(site));
2234 }
2235 }
2236 BuildKind::Refuel(fire) => {
2237 for (_, pos, _, fuel) in &mut structures {
2238 if pos.0 == site
2239 && let Some(mut fuel) = fuel
2240 {
2241 fuel.0 = fire.fuel_ticks().unwrap_or(fuel.0);
2242 }
2243 }
2244 }
2245 }
2246 blueprints.0.remove(&site);
2247 Step::Done
2248 }
2249 }
2250 }
2251 Task::Haul { pickup, to } => {
2252 let to = *to;
2253 let destination_valid = zones.is_stockpile(to)
2254 && reservations.holder(Target::Storage(to)) == Some(entity);
2255 if !destination_valid {
2256 Step::Done
2257 } else if let Some(from) = *pickup {
2258 if reservations.holder(Target::Pickup(from)) != Some(entity) {
2259 Step::Done
2260 } else if tile.0 != from {
2261 walk_to(&map, tile.0, from, &mut path)
2262 } else {
2263 let kind = items
2264 .iter()
2265 .find(|(_, _, pos)| pos.0 == from)
2266 .map(|(_, item, _)| item.kind);
2267 let room = kind.map_or(0, |kind| {
2268 room_for(items.iter().map(|(_, i, p)| (i, p)), to, kind)
2269 });
2270 match take_items(&mut commands, &mut items, from, CARRY_CAPACITY.min(room))
2271 .filter(|load| load.count > 0)
2272 {
2273 Some(load) => {
2274 carrying.0 = Some(load);
2275 reservations.release(Target::Pickup(from));
2276 *pickup = None;
2277 Step::Continue
2278 }
2279 None => Step::Done,
2280 }
2281 }
2282 } else if let Some(load) = carrying.0 {
2283 if tile.0 != to {
2284 walk_to(&map, tile.0, to, &mut path)
2285 } else {
2286 let room = room_for(items.iter().map(|(_, i, p)| (i, p)), to, load.kind);
2287 let dropped = load.count.min(room);
2288 if dropped > 0 {
2289 drop_items(
2290 &mut commands,
2291 &mut items,
2292 &item_assets,
2293 to,
2294 load.with_count(dropped),
2295 );
2296 }
2297 carrying.0 = (load.count > dropped).then_some(Item {
2298 count: load.count - dropped,
2299 ..load
2300 });
2301 Step::Done
2302 }
2303 } else {
2304 Step::Done
2305 }
2306 }
2307 };
2308 if let Some(skill) = skill
2309 && job.task.progress() > progress_before
2310 {
2311 skills.practice(skill, 1.0);
2312 }
2313 match step {
2314 Step::Continue => current.0 = Some(job),
2315 Step::Done => {
2316 reservations.release_all(entity);
2317 current.0 = None;
2318 let ordered_move = matches!(
2319 job,
2320 Job {
2321 task: Task::MoveTo(_),
2322 ordered: true
2323 }
2324 );
2325 idle.restart(if ordered_move {
2326 IDLE_AFTER_ORDER_SECS
2327 } else {
2328 rng.range_f32(WORK_PAUSE_SECS)
2329 });
2330 }
2331 }
2332 }
2333 }
2334
2335 #[cfg(test)]
2336 mod tests;