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