src/jobs/mod.rs 48.7 K · 1409 lines · raw · history

1 use std::{
2 collections::{HashMap, HashSet},
3 ops::Range,
4 time::Duration,
5 };
6
7 use bevy::prelude::*;
8
9 use crate::{
10 animals::{Animal, AnimalKind},
11 building::{Blueprints, BuildKind},
12 colonist::{Colonist, Path, repath},
13 economy::ColonyGoals,
14 food::{EAT_TICKS, Hunger, STARVING, find_food, portion, wants_food},
15 items::{
16 CARRY_CAPACITY, Carrying, Item, ItemAssets, ItemKind, ItemQuery, drop_items, room_for,
17 stock, take_items,
18 },
19 light::{LIT_BRIGHTNESS, LightMap, brightness, dark_soon, light_from},
20 needs::{EXHAUSTED_REST, Rest, SleepContext, done_sleeping, wants_sleep},
21 pathfinding::{find_path, find_path_adjacent, is_adjacent, nearest_tile},
22 rng::SimRng,
23 sim::{SimSystems, SimTick, hour_of_day},
24 structures::{Fuel, Structure, StructureAssets, StructureKind, spawn_structure},
25 world::{Plant, Terrain, TileChanged, TilePos, WorldMap},
26 worldgen::WorldGenerated,
27 zones::{Zones, can_zone, find_stockpile_site, new_stockpile_tiles, plan_expansion},
28 };
29
30 pub struct JobsPlugin;
31
32 impl Plugin for JobsPlugin {
33 fn build(&self, app: &mut App) {
34 app.insert_resource(SimRng::new(0x5EED))
35 .init_resource::<Reservations>()
36 .add_message::<Order>()
37 .add_message::<TileChanged>()
38 .add_systems(
39 Update,
40 (
41 clear_reservations.run_if(on_message::<WorldGenerated>),
42 handle_orders,
43 ),
44 )
45 .add_systems(FixedUpdate, choose_jobs.in_set(SimSystems::Decide))
46 .add_systems(FixedUpdate, run_jobs.in_set(SimSystems::Act));
47 }
48 }
49
50 pub const EXPAND_WORK: u32 = 240;
51 pub const CAMPFIRE_MIN_FUEL: u32 = 3 * crate::sim::TICKS_PER_HOUR as u32;
52 pub const HOME_RADIUS: f32 = 10.0;
53 pub const BED_STOCKPILE_CLEARANCE: i32 = 3;
54 const LIT_WORK_RATE: u32 = 2;
55 const DARK_WORK_RATE: u32 = 1;
56 pub const EXPANSION_TILES: usize = 8;
57 pub const MIN_EMPTY_STOCKPILE_TILES: usize = 3;
58 const SEARCH_TILES: usize = 15000;
59 const WANDER_RADIUS: i32 = 8;
60 const IDLE_SECS: Range<f32> = 1.0..4.0;
61 const IDLE_AFTER_ORDER_SECS: f32 = 10.0;
62 const WORK_PAUSE_SECS: Range<f32> = 0.2..0.8;
63 const NEIGHBORS: [IVec2; 8] = [
64 IVec2::new(1, 0),
65 IVec2::new(-1, 0),
66 IVec2::new(0, 1),
67 IVec2::new(0, -1),
68 IVec2::new(1, 1),
69 IVec2::new(1, -1),
70 IVec2::new(-1, 1),
71 IVec2::new(-1, -1),
72 ];
73
74 #[derive(Component, Default, Debug, Clone, PartialEq)]
75 pub struct CurrentJob(pub Option<Job>);
76
77 #[derive(Debug, Clone, Copy, PartialEq)]
78 pub struct Job {
79 pub task: Task,
80 pub ordered: bool,
81 }
82
83 #[derive(Debug, Clone, Copy, PartialEq)]
84 pub enum Task {
85 MoveTo(UVec2),
86 Work {
87 site: UVec2,
88 kind: WorkKind,
89 progress: u32,
90 },
91 Haul {
92 pickup: Option<UVec2>,
93 to: UVec2,
94 },
95 ExpandStockpile {
96 site: UVec2,
97 progress: u32,
98 },
99 Build {
100 site: UVec2,
101 kind: BuildKind,
102 pickup: Option<UVec2>,
103 progress: u32,
104 },
105 Sleep {
106 spot: UVec2,
107 },
108 Eat {
109 pickup: Option<UVec2>,
110 progress: u32,
111 },
112 Hunt {
113 animal: Entity,
114 kind: AnimalKind,
115 progress: u32,
116 },
117 Cook {
118 pickup: Option<UVec2>,
119 station: UVec2,
120 progress: u32,
121 },
122 }
123
124 pub fn work_rate(light: &LightMap, tile: UVec2) -> u32 {
125 if light.is_lit(tile) {
126 LIT_WORK_RATE
127 } else {
128 DARK_WORK_RATE
129 }
130 }
131
132 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
133 pub enum WorkKind {
134 Chop,
135 Mine,
136 Harvest,
137 Sow,
138 }
139
140 impl WorkKind {
141 pub fn work(self) -> u32 {
142 match self {
143 WorkKind::Chop => 480,
144 WorkKind::Mine => 720,
145 WorkKind::Harvest => 240,
146 WorkKind::Sow => 160,
147 }
148 }
149
150 fn complete(self, map: &mut WorldMap, site: UVec2) {
151 match self {
152 WorkKind::Chop => map.set_plant(site, None),
153 WorkKind::Mine => map.set(site, Terrain::Gravel),
154 WorkKind::Harvest => {
155 if map.plant(site.as_ivec2()) == Some(Plant::Crop) {
156 map.set_plant(site, None);
157 }
158 map.set_growth(site, 0.0);
159 }
160 WorkKind::Sow => {
161 map.set_plant(site, Some(Plant::Crop));
162 map.set_growth(site, 0.0);
163 }
164 }
165 }
166 }
167
168 pub struct SiteWork {
169 pub kind: WorkKind,
170 pub item: ItemKind,
171 pub count: u32,
172 }
173
174 pub fn site_work(map: &WorldMap, site: UVec2) -> Option<SiteWork> {
175 let p = site.as_ivec2();
176 let harvest = |item, count| SiteWork {
177 kind: WorkKind::Harvest,
178 item,
179 count,
180 };
181 match map.plant(p) {
182 Some(Plant::Tree) => {
183 return Some(SiteWork {
184 kind: WorkKind::Chop,
185 item: ItemKind::Wood,
186 count: 10,
187 });
188 }
189 Some(Plant::BerryBush) if map.ripe_plant(p).is_some() => {
190 return Some(harvest(ItemKind::Berries, 8));
191 }
192 Some(Plant::Crop) if map.ripe_plant(p).is_some() => {
193 return Some(harvest(ItemKind::Vegetables, 10));
194 }
195 _ => {}
196 }
197 let (item, count) = match map.get(p)? {
198 Terrain::Stone => (ItemKind::Stone, 10),
199 Terrain::IronOre => (ItemKind::Iron, 10),
200 Terrain::GoldOre => (ItemKind::Gold, 5),
201 _ => return None,
202 };
203 Some(SiteWork {
204 kind: WorkKind::Mine,
205 item,
206 count,
207 })
208 }
209
210 #[derive(Component, Debug)]
211 pub struct IdleTimer(Timer);
212
213 impl Default for IdleTimer {
214 fn default() -> Self {
215 Self(Timer::from_seconds(IDLE_SECS.start, TimerMode::Once))
216 }
217 }
218
219 impl IdleTimer {
220 fn restart(&mut self, secs: f32) {
221 self.0.set_duration(Duration::from_secs_f32(secs));
222 self.0.reset();
223 }
224 }
225
226 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
227 pub enum Target {
228 Site(UVec2),
229 Pickup(UVec2),
230 Storage(UVec2),
231 StockpileExpansion,
232 SleepSpot(UVec2),
233 Animal(Entity),
234 Station(UVec2),
235 }
236
237 #[derive(Resource, Default, Debug)]
238 pub struct Reservations(HashMap<Target, Entity>);
239
240 impl Reservations {
241 pub fn reserve(&mut self, target: Target, by: Entity) {
242 self.0.insert(target, by);
243 }
244
245 pub fn holder(&self, target: Target) -> Option<Entity> {
246 self.0.get(&target).copied()
247 }
248
249 pub fn is_free(&self, target: Target) -> bool {
250 self.holder(target).is_none()
251 }
252
253 pub fn release(&mut self, target: Target) {
254 self.0.remove(&target);
255 }
256
257 pub fn release_all(&mut self, by: Entity) {
258 self.0.retain(|_, holder| *holder != by);
259 }
260 }
261
262 #[derive(Message, Debug, Clone, Copy)]
263 pub struct Order {
264 pub colonist: Entity,
265 pub tile: UVec2,
266 }
267
268 fn clear_reservations(mut reservations: ResMut<Reservations>) {
269 reservations.0.clear();
270 }
271
272 fn handle_orders(
273 mut orders: MessageReader<Order>,
274 map: Res<WorldMap>,
275 zones: Res<Zones>,
276 items: Query<(&Item, &TilePos)>,
277 animals: Query<(Entity, &TilePos, &Animal), Without<Colonist>>,
278 mut reservations: ResMut<Reservations>,
279 mut colonists: Query<(&TilePos, &mut Path, &mut CurrentJob), With<Colonist>>,
280 ) {
281 for order in orders.read() {
282 let Ok((tile, mut path, mut job)) = colonists.get_mut(order.colonist) else {
283 continue;
284 };
285 let loose_item = items
286 .iter()
287 .find(|(_, pos)| pos.0 == order.tile && !zones.is_stockpile(pos.0));
288 let animal = animals.iter().find(|(_, pos, _)| pos.0 == order.tile);
289 let task = if let Some((animal, _, a)) = animal {
290 Some(Task::Hunt {
291 animal,
292 kind: a.kind,
293 progress: 0,
294 })
295 } else if let Some(work) = site_work(&map, order.tile) {
296 Some(Task::Work {
297 site: order.tile,
298 kind: work.kind,
299 progress: 0,
300 })
301 } else if let Some((item, _)) = loose_item {
302 find_storage(&map, &zones, &reservations, &items, order.tile, item.kind).map(|to| {
303 Task::Haul {
304 pickup: Some(order.tile),
305 to,
306 }
307 })
308 } else {
309 None
310 };
311 if let Some(task) = task {
312 reservations.release_all(order.colonist);
313 reserve_task(&mut reservations, task, order.colonist);
314 path.0.truncate(1);
315 job.0 = Some(Job {
316 task,
317 ordered: true,
318 });
319 } else if let Some(new_path) = repath(&map, tile.0, &path.0, order.tile) {
320 reservations.release_all(order.colonist);
321 path.0 = new_path;
322 job.0 = Some(Job {
323 task: Task::MoveTo(order.tile),
324 ordered: true,
325 });
326 }
327 }
328 }
329
330 fn reserve_task(reservations: &mut Reservations, task: Task, by: Entity) {
331 match task {
332 Task::MoveTo(_) => {}
333 Task::Work { site, .. } => reservations.reserve(Target::Site(site), by),
334 Task::Haul { pickup, to } => {
335 if let Some(pickup) = pickup {
336 reservations.reserve(Target::Pickup(pickup), by);
337 }
338 reservations.reserve(Target::Storage(to), by);
339 }
340 Task::ExpandStockpile { .. } => reservations.reserve(Target::StockpileExpansion, by),
341 Task::Sleep { spot } => reservations.reserve(Target::SleepSpot(spot), by),
342 Task::Eat { pickup, .. } => {
343 if let Some(pickup) = pickup {
344 reservations.reserve(Target::Pickup(pickup), by);
345 }
346 }
347 Task::Hunt { animal, .. } => reservations.reserve(Target::Animal(animal), by),
348 Task::Cook {
349 pickup, station, ..
350 } => {
351 reservations.reserve(Target::Station(station), by);
352 if let Some(pickup) = pickup {
353 reservations.reserve(Target::Pickup(pickup), by);
354 }
355 }
356 Task::Build { site, pickup, .. } => {
357 reservations.reserve(Target::Site(site), by);
358 if let Some(pickup) = pickup {
359 reservations.reserve(Target::Pickup(pickup), by);
360 }
361 }
362 }
363 }
364
365 pub fn find_work(
366 map: &WorldMap,
367 reservations: &Reservations,
368 from: UVec2,
369 wanted: impl Fn(ItemKind) -> bool,
370 ) -> Option<Task> {
371 let mut found = None;
372 nearest_tile(map, from, SEARCH_TILES, |stand| {
373 found = NEIGHBORS.iter().find_map(|&d| {
374 let site = stand.as_ivec2() + d;
375 if site.x < 0 || site.y < 0 {
376 return None;
377 }
378 let site = site.as_uvec2();
379 let work = site_work(map, site)?;
380 (wanted(work.item) && reservations.is_free(Target::Site(site))).then_some(Task::Work {
381 site,
382 kind: work.kind,
383 progress: 0,
384 })
385 });
386 found.is_some()
387 })?;
388 found
389 }
390
391 pub fn find_storage<'a>(
392 map: &WorldMap,
393 zones: &Zones,
394 reservations: &Reservations,
395 items: impl IntoIterator<Item = (&'a Item, &'a TilePos)> + Clone,
396 from: UVec2,
397 kind: ItemKind,
398 ) -> Option<UVec2> {
399 nearest_tile(map, from, SEARCH_TILES, |p| {
400 zones.is_stockpile(p)
401 && reservations.is_free(Target::Storage(p))
402 && room_for(items.clone().into_iter(), p, kind) > 0
403 })
404 }
405
406 pub fn find_haul(
407 map: &WorldMap,
408 zones: &Zones,
409 reservations: &Reservations,
410 items: &[(Item, TilePos)],
411 from: UVec2,
412 ) -> Option<Task> {
413 let loose: HashSet<UVec2> = items
414 .iter()
415 .map(|(_, pos)| pos.0)
416 .filter(|&p| !zones.is_stockpile(p) && reservations.is_free(Target::Pickup(p)))
417 .collect();
418 if loose.is_empty() {
419 return None;
420 }
421 let pickup = nearest_tile(map, from, SEARCH_TILES, |p| loose.contains(&p))?;
422 let (item, _) = items.iter().find(|(_, pos)| pos.0 == pickup)?;
423 let refs = items.iter().map(|(i, p)| (i, p));
424 let to = find_storage(map, zones, reservations, refs, pickup, item.kind)?;
425 Some(Task::Haul {
426 pickup: Some(pickup),
427 to,
428 })
429 }
430
431 pub fn sowable(map: &WorldMap, zones: &Zones, site: UVec2) -> bool {
432 zones.is_farm(site) && map.plant(site.as_ivec2()).is_none()
433 }
434
435 pub fn find_sowing(
436 map: &WorldMap,
437 zones: &Zones,
438 reservations: &Reservations,
439 from: UVec2,
440 ) -> Option<Task> {
441 if !zones.has_farm() {
442 return None;
443 }
444 let site = nearest_tile(map, from, SEARCH_TILES, |p| {
445 sowable(map, zones, p) && reservations.is_free(Target::Site(p))
446 })?;
447 Some(Task::Work {
448 site,
449 kind: WorkKind::Sow,
450 progress: 0,
451 })
452 }
453
454 pub const HUNT_RADIUS: f32 = 60.0;
455
456 pub fn find_hunt(
457 map: &WorldMap,
458 reservations: &Reservations,
459 animals: &[(Entity, UVec2, AnimalKind)],
460 from: UVec2,
461 ) -> Option<Task> {
462 let mut prey: Vec<_> = animals
463 .iter()
464 .filter(|(e, pos, _)| {
465 reservations.is_free(Target::Animal(*e))
466 && pos.as_vec2().distance(from.as_vec2()) <= HUNT_RADIUS
467 })
468 .collect();
469 prey.sort_by(|a, b| {
470 a.1.as_vec2()
471 .distance_squared(from.as_vec2())
472 .total_cmp(&b.1.as_vec2().distance_squared(from.as_vec2()))
473 });
474 prey.into_iter()
475 .find(|(_, pos, _)| find_path_adjacent(map, from, *pos).is_some())
476 .map(|&(animal, _, kind)| Task::Hunt {
477 animal,
478 kind,
479 progress: 0,
480 })
481 }
482
483 pub fn stockpile_too_full(zones: &Zones, items: &[(Item, TilePos)]) -> bool {
484 let occupied: HashSet<UVec2> = items.iter().map(|(_, pos)| pos.0).collect();
485 zones.has_stockpile() && zones.empty_stockpile_tiles(&occupied) < MIN_EMPTY_STOCKPILE_TILES
486 }
487
488 pub fn find_expansion(
489 map: &WorldMap,
490 zones: &Zones,
491 reservations: &Reservations,
492 items: &[(Item, TilePos)],
493 blueprints: &Blueprints,
494 ) -> Option<Task> {
495 if !reservations.is_free(Target::StockpileExpansion) {
496 return None;
497 }
498 let site = if zones.has_stockpile() {
499 if !stockpile_too_full(zones, items) {
500 return None;
501 }
502 *plan_expansion(map, zones, &blueprints.tiles(), EXPANSION_TILES).first()?
503 } else {
504 find_stockpile_site(map, central_item_tile(items)?)?
505 };
506 Some(Task::ExpandStockpile { site, progress: 0 })
507 }
508
509 fn central_item_tile(items: &[(Item, TilePos)]) -> Option<UVec2> {
510 let tiles: Vec<Vec2> = items.iter().map(|(_, pos)| pos.0.as_vec2()).collect();
511 let centroid = tiles.iter().sum::<Vec2>() / tiles.len().max(1) as f32;
512 items.iter().map(|(_, pos)| pos.0).min_by(|a, b| {
513 a.as_vec2()
514 .distance_squared(centroid)
515 .total_cmp(&b.as_vec2().distance_squared(centroid))
516 })
517 }
518
519 pub struct BuildContext<'a> {
520 pub map: &'a WorldMap,
521 pub zones: &'a Zones,
522 pub reservations: &'a Reservations,
523 pub items: &'a [(Item, TilePos)],
524 pub structures: &'a [(UVec2, StructureKind, u32)],
525 pub planned: &'a [(UVec2, StructureKind)],
526 pub blueprints: &'a Blueprints,
527 }
528
529 impl BuildContext<'_> {
530 fn buildable(&self, tile: UVec2) -> bool {
531 can_zone(self.map, tile.as_ivec2())
532 && !self.zones.is_stockpile(tile)
533 && !self.zones.is_farm(tile)
534 && self.map.plant(tile.as_ivec2()).is_none()
535 && !self.structures.iter().any(|(pos, ..)| *pos == tile)
536 && !self.planned.iter().any(|(pos, _)| *pos == tile)
537 && self.blueprints.get(tile).is_none()
538 && !self.items.iter().any(|(_, pos)| pos.0 == tile)
539 && self.reservations.is_free(Target::Site(tile))
540 }
541
542 fn lights(&self) -> impl Iterator<Item = (UVec2, StructureKind)> + Clone + '_ {
543 self.structures
544 .iter()
545 .map(|&(pos, kind, _)| (pos, kind))
546 .chain(self.planned.iter().copied())
547 }
548
549 fn campfire_needed(&self, home: UVec2) -> bool {
550 let burning = self.structures.iter().any(|&(pos, kind, fuel)| {
551 kind == StructureKind::Campfire
552 && fuel >= CAMPFIRE_MIN_FUEL
553 && pos.as_vec2().distance(home.as_vec2()) <= HOME_RADIUS
554 });
555 let planned = self
556 .planned
557 .iter()
558 .any(|(_, kind)| *kind == StructureKind::Campfire);
559 !burning && !planned
560 }
561
562 fn darkest_stockpile_tile(&self, home: UVec2) -> Option<UVec2> {
563 self.zones
564 .stockpile_tiles()
565 .filter(|&t| brightness(light_from(t, self.lights())) < LIT_BRIGHTNESS)
566 .min_by(|a, b| {
567 a.as_vec2()
568 .distance_squared(home.as_vec2())
569 .total_cmp(&b.as_vec2().distance_squared(home.as_vec2()))
570 .then((a.y, a.x).cmp(&(b.y, b.x)))
571 })
572 }
573
574 fn site_near(&self, target: UVec2, max_distance: f32) -> Option<UVec2> {
575 self.site_near_where(target, max_distance, |_| true)
576 }
577
578 fn site_near_where(
579 &self,
580 target: UVec2,
581 max_distance: f32,
582 extra: impl Fn(UVec2) -> bool,
583 ) -> Option<UVec2> {
584 nearest_tile(self.map, target, SEARCH_TILES, |p| {
585 self.buildable(p) && extra(p)
586 })
587 .filter(|site| site.as_vec2().distance(target.as_vec2()) <= max_distance)
588 }
589
590 fn clear_of_stockpile(&self, tile: UVec2) -> bool {
591 self.zones
592 .stockpile_tiles()
593 .chain(self.zones.farm_tiles())
594 .all(|s| {
595 (s.as_ivec2() - tile.as_ivec2()).abs().max_element() >= BED_STOCKPILE_CLEARANCE
596 })
597 }
598
599 fn count(&self, kind: StructureKind) -> usize {
600 let built = self.structures.iter().filter(|s| s.1 == kind).count();
601 built + self.planned.iter().filter(|p| p.1 == kind).count()
602 }
603
604 fn bed_anchor(&self, home: UVec2) -> UVec2 {
605 let positions = |kind: StructureKind| {
606 let built = self.structures.iter().map(|s| (s.0, s.1));
607 built
608 .chain(self.planned.iter().copied())
609 .filter(move |s| s.1 == kind)
610 .map(|s| s.0)
611 };
612 let nearest = |kind: StructureKind| {
613 positions(kind).min_by(|a, b| {
614 a.as_vec2()
615 .distance_squared(home.as_vec2())
616 .total_cmp(&b.as_vec2().distance_squared(home.as_vec2()))
617 })
618 };
619 nearest(StructureKind::Bed)
620 .or_else(|| nearest(StructureKind::Campfire))
621 .unwrap_or(home)
622 }
623
624 fn material_source(&self, from: UVec2, (kind, amount): (ItemKind, u32)) -> Option<UVec2> {
625 let free = |pos: UVec2| self.reservations.is_free(Target::Pickup(pos));
626 let available: u32 = self
627 .items
628 .iter()
629 .filter(|(item, pos)| item.kind == kind && free(pos.0))
630 .map(|(item, _)| item.count)
631 .sum();
632 if available < amount {
633 return None;
634 }
635 nearest_tile(self.map, from, SEARCH_TILES, |p| {
636 free(p)
637 && self
638 .items
639 .iter()
640 .any(|(item, pos)| pos.0 == p && item.kind == kind)
641 })
642 }
643
644 fn build_task(&self, from: UVec2, site: UVec2, kind: BuildKind) -> Option<Task> {
645 let pickup = self.material_source(from, kind.cost())?;
646 Some(Task::Build {
647 site,
648 kind,
649 pickup: Some(pickup),
650 progress: 0,
651 })
652 }
653 }
654
655 pub const COOK_INPUT: u32 = 6;
656 pub const COOK_WORK: u32 = 300;
657 pub const COOK_MIN_FUEL: u32 = crate::sim::TICKS_PER_HOUR as u32;
658
659 pub fn find_campfire(ctx: &BuildContext, from: UVec2) -> Option<Task> {
660 let home = ctx.zones.home(from);
661 if !ctx.campfire_needed(home) {
662 return None;
663 }
664 let site = ctx.site_near(home, HOME_RADIUS)?;
665 ctx.build_task(from, site, BuildKind::Structure(StructureKind::Campfire))
666 }
667
668 pub fn raw_food_source(ctx: &BuildContext, from: UVec2) -> Option<UVec2> {
669 nearest_tile(ctx.map, from, SEARCH_TILES, |p| {
670 ctx.reservations.is_free(Target::Pickup(p))
671 && ctx.items.iter().any(|(item, pos)| {
672 pos.0 == p && item.kind.is_raw_food() && item.count >= COOK_INPUT
673 })
674 })
675 }
676
677 pub fn find_cooking(ctx: &BuildContext, from: UVec2) -> Option<Task> {
678 let station = ctx
679 .structures
680 .iter()
681 .filter(|&&(pos, kind, fuel)| {
682 kind == StructureKind::Campfire
683 && fuel >= COOK_MIN_FUEL
684 && ctx.reservations.is_free(Target::Station(pos))
685 })
686 .map(|&(pos, ..)| pos)
687 .min_by(|a, b| {
688 a.as_vec2()
689 .distance_squared(from.as_vec2())
690 .total_cmp(&b.as_vec2().distance_squared(from.as_vec2()))
691 })?;
692 let pickup = raw_food_source(ctx, from)?;
693 Some(Task::Cook {
694 pickup: Some(pickup),
695 station,
696 progress: 0,
697 })
698 }
699
700 pub fn find_lighting(ctx: &BuildContext, from: UVec2) -> Option<Task> {
701 let home = ctx.zones.home(from);
702 let (kind, site) = if ctx.campfire_needed(home) {
703 (StructureKind::Campfire, ctx.site_near(home, HOME_RADIUS)?)
704 } else {
705 let dark = ctx.darkest_stockpile_tile(home)?;
706 let reach = StructureKind::Torch.radius() * 0.6;
707 (StructureKind::Torch, ctx.site_near(dark, reach)?)
708 };
709 ctx.build_task(from, site, BuildKind::Structure(kind))
710 }
711
712 pub fn find_bed_site(ctx: &BuildContext, colonists: usize, from: UVec2) -> Option<Task> {
713 if ctx.count(StructureKind::Bed) >= colonists {
714 return None;
715 }
716 let anchor = ctx.bed_anchor(ctx.zones.home(from));
717 let site = ctx.site_near_where(anchor, HOME_RADIUS * 1.5, |p| ctx.clear_of_stockpile(p))?;
718 ctx.build_task(from, site, BuildKind::Structure(StructureKind::Bed))
719 }
720
721 pub fn find_blueprint_work(ctx: &BuildContext, from: UVec2) -> Option<Task> {
722 let mut found = None;
723 nearest_tile(ctx.map, from, SEARCH_TILES, |p| {
724 let Some(kind) = ctx.blueprints.get(p) else {
725 return false;
726 };
727 if !ctx.reservations.is_free(Target::Site(p)) {
728 return false;
729 }
730 found = ctx.build_task(from, p, kind);
731 found.is_some()
732 })?;
733 found
734 }
735
736 fn on_the_way(map: &WorldMap, jobs: impl Iterator<Item = Option<Job>>) -> HashMap<ItemKind, u32> {
737 let mut totals = HashMap::new();
738 for job in jobs.flatten() {
739 if let Task::Work { site, .. } = job.task
740 && let Some(work) = site_work(map, site)
741 {
742 *totals.entry(work.item).or_insert(0) += work.count;
743 }
744 }
745 totals
746 }
747
748 #[allow(clippy::too_many_arguments)]
749 fn choose_jobs(
750 mut commands: Commands,
751 time: Res<Time>,
752 map: Res<WorldMap>,
753 zones: Res<Zones>,
754 goals: Res<ColonyGoals>,
755 item_assets: Res<ItemAssets>,
756 tick: Res<SimTick>,
757 blueprints: Res<Blueprints>,
758 mut rng: ResMut<SimRng>,
759 mut reservations: ResMut<Reservations>,
760 mut items: ItemQuery,
761 structures: Query<(&TilePos, &Structure, Option<&Fuel>), Without<Colonist>>,
762 animals: Query<(Entity, &TilePos, &Animal), Without<Colonist>>,
763 mut colonists: Query<
764 (
765 Entity,
766 &TilePos,
767 &mut Path,
768 &mut CurrentJob,
769 &mut IdleTimer,
770 &mut Carrying,
771 &Rest,
772 &Hunger,
773 ),
774 With<Colonist>,
775 >,
776 ) {
777 let wildlife: Vec<(Entity, UVec2, AnimalKind)> = animals
778 .iter()
779 .map(|(e, pos, a)| (e, pos.0, a.kind))
780 .collect();
781 let colonist_count = colonists.iter().count();
782 let structures_list: Vec<(UVec2, StructureKind, u32)> = structures
783 .iter()
784 .map(|(pos, s, fuel)| (pos.0, s.kind, fuel.map_or(u32::MAX, |f| f.0)))
785 .collect();
786 let mut planned: Vec<(UVec2, StructureKind)> = colonists
787 .iter()
788 .filter_map(|c| match c.3.0?.task {
789 Task::Build {
790 site,
791 kind: BuildKind::Structure(kind),
792 ..
793 } => Some((site, kind)),
794 _ => None,
795 })
796 .collect();
797 let dark = dark_soon(*tick);
798 let carried: Vec<Item> = colonists.iter().filter_map(|c| c.5.0).collect();
799 let stocks: HashMap<ItemKind, u32> = ItemKind::ALL
800 .iter()
801 .map(|&kind| {
802 let all = items.iter().map(|(_, item, _)| item).chain(carried.iter());
803 (kind, stock(all, kind))
804 })
805 .collect();
806 let mut pending = on_the_way(&map, colonists.iter().map(|c| c.3.0));
807 for job in colonists.iter().filter_map(|c| c.3.0) {
808 match job.task {
809 Task::Hunt { kind, .. } => *pending.entry(ItemKind::Meat).or_insert(0) += kind.meat(),
810 Task::Cook { .. } => *pending.entry(ItemKind::Meal).or_insert(0) += 1,
811 _ => {}
812 }
813 }
814 let hour = hour_of_day(*tick);
815 for (entity, tile, mut path, mut job, mut idle, mut carrying, rest, hunger) in &mut colonists {
816 let exhausted = rest.0 < EXHAUSTED_REST;
817 let starving = hunger.0 < STARVING;
818 let urgent = exhausted || starving;
819 if urgent
820 && job.0.is_some_and(|j| {
821 !j.ordered && !matches!(j.task, Task::Sleep { .. } | Task::Eat { .. })
822 })
823 {
824 reservations.release_all(entity);
825 job.0 = None;
826 path.0.truncate(1);
827 }
828 let rested = idle.0.tick(time.delta()).is_finished();
829 if job.0.is_some() || !path.0.is_empty() || !(rested || urgent) {
830 continue;
831 }
832 let snapshot: Vec<(Item, TilePos)> =
833 items.iter().map(|(_, item, pos)| (*item, *pos)).collect();
834 let refs = snapshot.iter().map(|(i, p)| (i, p));
835 let wanted = |kind: ItemKind| goals.wanted(kind, &stocks, &pending);
836 let sleep = SleepContext {
837 map: &map,
838 zones: &zones,
839 reservations: &reservations,
840 items: &snapshot,
841 structures: &structures_list,
842 blueprints: &blueprints,
843 };
844 let food = wants_food(hunger.0)
845 .then(|| find_food(&map, &reservations, &snapshot, tile.0))
846 .flatten();
847 let task = if exhausted {
848 Some(Task::Sleep {
849 spot: sleep.spot(tile.0),
850 })
851 } else if let Some(pickup) = food {
852 if let Some(load) = carrying.0.take() {
853 drop_items(&mut commands, &mut items, &item_assets, tile.0, load);
854 }
855 Some(Task::Eat {
856 pickup: Some(pickup),
857 progress: 0,
858 })
859 } else if wants_sleep(rest.0, hour) {
860 Some(Task::Sleep {
861 spot: sleep.spot(tile.0),
862 })
863 } else if let Some(load) = carrying.0 {
864 let storage = find_storage(&map, &zones, &reservations, refs, tile.0, load.kind);
865 if storage.is_none() {
866 drop_items(&mut commands, &mut items, &item_assets, tile.0, load);
867 carrying.0 = None;
868 }
869 storage.map(|to| Task::Haul { pickup: None, to })
870 } else {
871 let build = BuildContext {
872 map: &map,
873 zones: &zones,
874 reservations: &reservations,
875 items: &snapshot,
876 structures: &structures_list,
877 planned: &planned,
878 blueprints: &blueprints,
879 };
880 find_expansion(&map, &zones, &reservations, &snapshot, &blueprints)
881 .or_else(|| dark.then(|| find_lighting(&build, tile.0)).flatten())
882 .or_else(|| {
883 let cooking =
884 wanted(ItemKind::Meal) && raw_food_source(&build, tile.0).is_some();
885 cooking
886 .then(|| {
887 find_cooking(&build, tile.0).or_else(|| find_campfire(&build, tile.0))
888 })
889 .flatten()
890 })
891 .or_else(|| find_bed_site(&build, colonist_count, tile.0))
892 .or_else(|| find_blueprint_work(&build, tile.0))
893 .or_else(|| find_work(&map, &reservations, tile.0, wanted))
894 .or_else(|| {
895 wanted(ItemKind::Meat)
896 .then(|| find_hunt(&map, &reservations, &wildlife, tile.0))
897 .flatten()
898 })
899 .or_else(|| find_sowing(&map, &zones, &reservations, tile.0))
900 .or_else(|| find_haul(&map, &zones, &reservations, &snapshot, tile.0))
901 };
902 if let Some(task) = task {
903 if let Task::Work { site, .. } = task
904 && let Some(work) = site_work(&map, site)
905 {
906 *pending.entry(work.item).or_insert(0) += work.count;
907 }
908 match task {
909 Task::Hunt { kind, .. } => {
910 *pending.entry(ItemKind::Meat).or_insert(0) += kind.meat()
911 }
912 Task::Cook { .. } => *pending.entry(ItemKind::Meal).or_insert(0) += 1,
913 _ => {}
914 }
915 if let Task::Build {
916 site,
917 kind: BuildKind::Structure(kind),
918 ..
919 } = task
920 {
921 planned.push((site, kind));
922 }
923 reserve_task(&mut reservations, task, entity);
924 job.0 = Some(Job {
925 task,
926 ordered: false,
927 });
928 continue;
929 }
930 let goal = tile.0.as_ivec2()
931 + IVec2::new(
932 rng.range_i32(-WANDER_RADIUS..WANDER_RADIUS + 1),
933 rng.range_i32(-WANDER_RADIUS..WANDER_RADIUS + 1),
934 );
935 if map.in_bounds(goal)
936 && let Some(wander_path) = find_path(&map, tile.0, goal.as_uvec2())
937 {
938 path.0 = wander_path.into();
939 idle.restart(rng.range_f32(IDLE_SECS));
940 }
941 }
942 }
943
944 enum Step {
945 Continue,
946 Done,
947 }
948
949 fn walk_to(map: &WorldMap, from: UVec2, to: UVec2, path: &mut Path) -> Step {
950 match find_path(map, from, to) {
951 Some(p) if !p.is_empty() => {
952 path.0 = p.into();
953 Step::Continue
954 }
955 _ => Step::Done,
956 }
957 }
958
959 #[allow(clippy::too_many_arguments)]
960 fn run_jobs(
961 mut commands: Commands,
962 mut map: ResMut<WorldMap>,
963 mut zones: ResMut<Zones>,
964 mut reservations: ResMut<Reservations>,
965 mut rng: ResMut<SimRng>,
966 item_assets: Res<ItemAssets>,
967 structure_assets: Res<StructureAssets>,
968 light: Res<LightMap>,
969 tick: Res<SimTick>,
970 mut blueprints: ResMut<Blueprints>,
971 mut goals: ResMut<ColonyGoals>,
972 mut items: ItemQuery,
973 structures: Query<(&TilePos, &Structure), Without<Colonist>>,
974 animals: Query<(&TilePos, &Path), (With<Animal>, Without<Colonist>)>,
975 mut changed: MessageWriter<TileChanged>,
976 mut colonists: Query<
977 (
978 Entity,
979 &TilePos,
980 &mut Path,
981 &mut CurrentJob,
982 &mut IdleTimer,
983 &mut Carrying,
984 &Rest,
985 &mut Hunger,
986 ),
987 With<Colonist>,
988 >,
989 ) {
990 let hour = hour_of_day(*tick);
991 let occupied: Vec<UVec2> = colonists
992 .iter()
993 .flat_map(|c| [Some(c.1.0), c.2.0.front().copied()])
994 .chain(
995 animals
996 .iter()
997 .flat_map(|(t, p)| [Some(t.0), p.0.front().copied()]),
998 )
999 .flatten()
1000 .collect();
1001 for (entity, tile, mut path, mut current, mut idle, mut carrying, rest, mut hunger) in
1002 &mut colonists
1003 {
1004 let Some(mut job) = current.0 else {
1005 continue;
1006 };
1007 if !path.0.is_empty() {
1008 continue;
1009 }
1010 let step = match &mut job.task {
1011 Task::MoveTo(_) => Step::Done,
1012 Task::Cook {
1013 pickup,
1014 station,
1015 progress,
1016 } => {
1017 let station = *station;
1018 let fire = structures
1019 .iter()
1020 .any(|(pos, s)| pos.0 == station && s.kind == StructureKind::Campfire);
1021 let ours = reservations.holder(Target::Station(station)) == Some(entity);
1022 if !fire || !ours {
1023 Step::Done
1024 } else if let Some(from) = *pickup {
1025 if reservations.holder(Target::Pickup(from)) != Some(entity) {
1026 Step::Done
1027 } else if tile.0 != from {
1028 walk_to(&map, tile.0, from, &mut path)
1029 } else {
1030 let load = take_items(&mut commands, &mut items, from, COOK_INPUT)
1031 .filter(|load| load.kind.is_raw_food() && load.count > 0);
1032 carrying.0 = load;
1033 reservations.release(Target::Pickup(from));
1034 match load {
1035 Some(load) if load.count >= COOK_INPUT => {
1036 *pickup = None;
1037 Step::Continue
1038 }
1039 _ => Step::Done,
1040 }
1041 }
1042 } else if !carrying
1043 .0
1044 .is_some_and(|load| load.kind.is_raw_food() && load.count >= COOK_INPUT)
1045 {
1046 Step::Done
1047 } else if !is_adjacent(tile.0.as_ivec2(), station.as_ivec2()) {
1048 match find_path_adjacent(&map, tile.0, station) {
1049 Some(p) if !p.is_empty() => {
1050 path.0 = p.into();
1051 Step::Continue
1052 }
1053 _ => Step::Done,
1054 }
1055 } else {
1056 *progress += work_rate(&light, tile.0);
1057 if *progress < COOK_WORK {
1058 Step::Continue
1059 } else {
1060 if let Some(load) = carrying.0 {
1061 goals.record_use(load.kind, COOK_INPUT);
1062 }
1063 carrying.0 = carrying.0.and_then(|load| {
1064 (load.count > COOK_INPUT).then_some(Item {
1065 count: load.count - COOK_INPUT,
1066 ..load
1067 })
1068 });
1069 drop_items(
1070 &mut commands,
1071 &mut items,
1072 &item_assets,
1073 tile.0,
1074 Item::new(ItemKind::Meal, 1),
1075 );
1076 Step::Done
1077 }
1078 }
1079 }
1080 Task::Hunt {
1081 animal,
1082 kind,
1083 progress,
1084 } => match animals.get(*animal) {
1085 Ok((prey, _)) if reservations.holder(Target::Animal(*animal)) == Some(entity) => {
1086 let prey = prey.0;
1087 if prey != tile.0 && !is_adjacent(tile.0.as_ivec2(), prey.as_ivec2()) {
1088 match find_path_adjacent(&map, tile.0, prey) {
1089 Some(p) if !p.is_empty() => {
1090 path.0 = p.into();
1091 Step::Continue
1092 }
1093 _ => Step::Done,
1094 }
1095 } else {
1096 *progress += work_rate(&light, tile.0);
1097 if *progress < kind.hunt_work() {
1098 Step::Continue
1099 } else {
1100 commands.entity(*animal).despawn();
1101 drop_items(
1102 &mut commands,
1103 &mut items,
1104 &item_assets,
1105 prey,
1106 Item::new(ItemKind::Meat, kind.meat()),
1107 );
1108 Step::Done
1109 }
1110 }
1111 }
1112 _ => Step::Done,
1113 },
1114 Task::Eat { pickup, progress } => {
1115 if let Some(from) = *pickup {
1116 if reservations.holder(Target::Pickup(from)) != Some(entity) {
1117 Step::Done
1118 } else if tile.0 != from {
1119 walk_to(&map, tile.0, from, &mut path)
1120 } else {
1121 let kind = items
1122 .iter()
1123 .find(|(_, item, pos)| pos.0 == from && item.kind.is_food())
1124 .map(|(_, item, _)| item.kind);
1125 let meal = kind.and_then(|kind| {
1126 take_items(&mut commands, &mut items, from, portion(kind, hunger.0))
1127 });
1128 reservations.release(Target::Pickup(from));
1129 match meal.filter(|m| m.kind.is_food()) {
1130 Some(meal) => {
1131 carrying.0 = Some(meal);
1132 *pickup = None;
1133 Step::Continue
1134 }
1135 None => Step::Done,
1136 }
1137 }
1138 } else if let Some(meal) = carrying.0.filter(|m| m.kind.is_food()) {
1139 *progress += 1;
1140 if *progress < EAT_TICKS {
1141 Step::Continue
1142 } else {
1143 hunger.0 = (hunger.0 + meal.count as f32 * meal.kind.nutrition()).min(1.0);
1144 goals.record_use(meal.kind, meal.count);
1145 carrying.0 = None;
1146 Step::Done
1147 }
1148 } else {
1149 Step::Done
1150 }
1151 }
1152 Task::Sleep { spot } => {
1153 if reservations.holder(Target::SleepSpot(*spot)) != Some(entity) {
1154 Step::Done
1155 } else if tile.0 != *spot {
1156 let step = walk_to(&map, tile.0, *spot, &mut path);
1157 if matches!(step, Step::Done) {
1158 reservations.release(Target::SleepSpot(*spot));
1159 *spot = tile.0;
1160 reservations.reserve(Target::SleepSpot(*spot), entity);
1161 }
1162 Step::Continue
1163 } else if done_sleeping(rest.0, hour) {
1164 Step::Done
1165 } else {
1166 Step::Continue
1167 }
1168 }
1169 Task::Work {
1170 site,
1171 kind,
1172 progress,
1173 } => {
1174 let (site, kind) = (*site, *kind);
1175 let work = site_work(&map, site).filter(|w| w.kind == kind);
1176 let valid =
1177 work.is_some() || (kind == WorkKind::Sow && sowable(&map, &zones, site));
1178 let ours = reservations.holder(Target::Site(site)) == Some(entity);
1179 if !valid || !ours {
1180 Step::Done
1181 } else if !is_adjacent(tile.0.as_ivec2(), site.as_ivec2()) {
1182 match find_path_adjacent(&map, tile.0, site) {
1183 Some(p) if !p.is_empty() => {
1184 path.0 = p.into();
1185 Step::Continue
1186 }
1187 _ => Step::Done,
1188 }
1189 } else {
1190 *progress += work_rate(&light, tile.0);
1191 if *progress < kind.work() {
1192 Step::Continue
1193 } else {
1194 kind.complete(&mut map, site);
1195 changed.write(TileChanged(site));
1196 if let Some(work) = work {
1197 drop_items(
1198 &mut commands,
1199 &mut items,
1200 &item_assets,
1201 site,
1202 Item::new(work.item, work.count),
1203 );
1204 }
1205 Step::Done
1206 }
1207 }
1208 }
1209 Task::ExpandStockpile { site, progress } => {
1210 if tile.0 != *site {
1211 walk_to(&map, tile.0, *site, &mut path)
1212 } else {
1213 *progress += work_rate(&light, tile.0);
1214 if *progress >= EXPAND_WORK {
1215 if zones.has_stockpile() {
1216 let excluded = blueprints.tiles();
1217 let tiles = plan_expansion(&map, &zones, &excluded, EXPANSION_TILES);
1218 zones.add_tiles(&map, tiles);
1219 } else {
1220 zones.add_tiles(&map, new_stockpile_tiles(*site));
1221 }
1222 Step::Done
1223 } else {
1224 Step::Continue
1225 }
1226 }
1227 }
1228 Task::Build {
1229 site,
1230 kind,
1231 pickup,
1232 progress,
1233 } => {
1234 let (site, kind) = (*site, *kind);
1235 let ours = reservations.holder(Target::Site(site)) == Some(entity);
1236 let planned = match kind {
1237 BuildKind::Structure(
1238 StructureKind::Campfire | StructureKind::Torch | StructureKind::Bed,
1239 ) => true,
1240 _ => blueprints.get(site) == Some(kind),
1241 };
1242 let blocked = map.plant(site.as_ivec2()).is_some()
1243 || (kind != BuildKind::Roof && structures.iter().any(|(pos, _)| pos.0 == site));
1244 let (material, cost) = kind.cost();
1245 if !ours || blocked || !planned {
1246 Step::Done
1247 } else if let Some(from) = *pickup {
1248 if reservations.holder(Target::Pickup(from)) != Some(entity) {
1249 Step::Done
1250 } else if tile.0 != from {
1251 walk_to(&map, tile.0, from, &mut path)
1252 } else if carrying.0.is_some_and(|load| load.kind != material) {
1253 Step::Done
1254 } else {
1255 let have = carrying.0.map_or(0, |load| load.count);
1256 let taken = take_items(&mut commands, &mut items, from, cost - have)
1257 .filter(|load| load.kind == material)
1258 .map_or(0, |load| load.count);
1259 reservations.release(Target::Pickup(from));
1260 let total = have + taken;
1261 carrying.0 = (total > 0).then_some(Item::new(material, total));
1262 let next = nearest_tile(&map, tile.0, SEARCH_TILES, |p| {
1263 reservations.is_free(Target::Pickup(p))
1264 && items.iter().any(|(_, item, pos)| {
1265 pos.0 == p && item.kind == material && item.count > 0
1266 })
1267 });
1268 match next {
1269 _ if total >= cost => {
1270 *pickup = None;
1271 Step::Continue
1272 }
1273 Some(next) => {
1274 reservations.reserve(Target::Pickup(next), entity);
1275 *pickup = Some(next);
1276 Step::Continue
1277 }
1278 None => Step::Done,
1279 }
1280 }
1281 } else if !carrying
1282 .0
1283 .is_some_and(|load| load.kind == material && load.count >= cost)
1284 {
1285 Step::Done
1286 } else if !is_adjacent(tile.0.as_ivec2(), site.as_ivec2()) {
1287 match find_path_adjacent(&map, tile.0, site) {
1288 Some(p) if !p.is_empty() => {
1289 path.0 = p.into();
1290 Step::Continue
1291 }
1292 _ => Step::Done,
1293 }
1294 } else {
1295 *progress = (*progress + work_rate(&light, tile.0)).min(kind.work());
1296 let someone_in_the_way = kind.blocks_tile() && occupied.contains(&site);
1297 if *progress < kind.work() || someone_in_the_way {
1298 Step::Continue
1299 } else {
1300 goals.record_use(material, cost);
1301 carrying.0 = carrying.0.and_then(|load| {
1302 (load.count > cost).then_some(Item {
1303 count: load.count - cost,
1304 ..load
1305 })
1306 });
1307 match kind {
1308 BuildKind::Structure(structure) => {
1309 spawn_structure(&mut commands, &structure_assets, site, structure);
1310 if kind.blocks_tile() {
1311 map.set_wall(site, true);
1312 changed.write(TileChanged(site));
1313 }
1314 }
1315 BuildKind::Roof => {
1316 map.set_roof(site, true);
1317 changed.write(TileChanged(site));
1318 }
1319 BuildKind::Pave => {
1320 map.set(site, Terrain::Paved);
1321 changed.write(TileChanged(site));
1322 }
1323 }
1324 blueprints.0.remove(&site);
1325 Step::Done
1326 }
1327 }
1328 }
1329 Task::Haul { pickup, to } => {
1330 let to = *to;
1331 let destination_valid = zones.is_stockpile(to)
1332 && reservations.holder(Target::Storage(to)) == Some(entity);
1333 if !destination_valid {
1334 Step::Done
1335 } else if let Some(from) = *pickup {
1336 if reservations.holder(Target::Pickup(from)) != Some(entity) {
1337 Step::Done
1338 } else if tile.0 != from {
1339 walk_to(&map, tile.0, from, &mut path)
1340 } else {
1341 let kind = items
1342 .iter()
1343 .find(|(_, _, pos)| pos.0 == from)
1344 .map(|(_, item, _)| item.kind);
1345 let room = kind.map_or(0, |kind| {
1346 room_for(items.iter().map(|(_, i, p)| (i, p)), to, kind)
1347 });
1348 match take_items(&mut commands, &mut items, from, CARRY_CAPACITY.min(room))
1349 .filter(|load| load.count > 0)
1350 {
1351 Some(load) => {
1352 carrying.0 = Some(load);
1353 reservations.release(Target::Pickup(from));
1354 *pickup = None;
1355 Step::Continue
1356 }
1357 None => Step::Done,
1358 }
1359 }
1360 } else if let Some(load) = carrying.0 {
1361 if tile.0 != to {
1362 walk_to(&map, tile.0, to, &mut path)
1363 } else {
1364 let room = room_for(items.iter().map(|(_, i, p)| (i, p)), to, load.kind);
1365 let dropped = load.count.min(room);
1366 if dropped > 0 {
1367 drop_items(
1368 &mut commands,
1369 &mut items,
1370 &item_assets,
1371 to,
1372 load.with_count(dropped),
1373 );
1374 }
1375 carrying.0 = (load.count > dropped).then_some(Item {
1376 count: load.count - dropped,
1377 ..load
1378 });
1379 Step::Done
1380 }
1381 } else {
1382 Step::Done
1383 }
1384 }
1385 };
1386 match step {
1387 Step::Continue => current.0 = Some(job),
1388 Step::Done => {
1389 reservations.release_all(entity);
1390 current.0 = None;
1391 let ordered_move = matches!(
1392 job,
1393 Job {
1394 task: Task::MoveTo(_),
1395 ordered: true
1396 }
1397 );
1398 idle.restart(if ordered_move {
1399 IDLE_AFTER_ORDER_SECS
1400 } else {
1401 rng.range_f32(WORK_PAUSE_SECS)
1402 });
1403 }
1404 }
1405 }
1406 }
1407
1408 #[cfg(test)]
1409 mod tests;