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