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