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