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