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