Stop starving colonists from dropping work when there is no food
- author
- Marijn Besseling <njirambem@gmail.com> · 2026-10-24 21:11 UTC
- commit
- fc89a0407888bf8fce6893780e2742103b2f3bb5
- parent
- ee2c8ca353
- tree
- browse at this commit
3 files changed +54 -20
| src/food.rs | +14 -13 | |
| src/jobs/mod.rs | +26 -7 | |
| src/jobs/tests.rs | +14 -0 |
src/food.rs +14 -13
| @@ -1,3 +1,5 @@ | ||
| 1 | +use std::collections::HashSet; | |
| 2 | + | |
| 1 | 3 | use bevy::prelude::*; |
| 2 | 4 | |
| 3 | 5 | use crate::{ |
| @@ -75,20 +77,19 @@ pub fn find_food( | ||
| 75 | 77 | items: &[(Item, TilePos)], |
| 76 | 78 | from: UVec2, |
| 77 | 79 | ) -> Option<UVec2> { |
| 78 | - let has = |p: UVec2, accept: fn(ItemKind) -> bool| { | |
| 79 | - reservations.is_free(Target::Pickup(p)) | |
| 80 | - && items | |
| 81 | - .iter() | |
| 82 | - .any(|(item, pos)| pos.0 == p && accept(item.kind)) | |
| 80 | + let tiles = |accept: fn(ItemKind) -> bool| -> HashSet<UVec2> { | |
| 81 | + items | |
| 82 | + .iter() | |
| 83 | + .filter(|(item, pos)| accept(item.kind) && reservations.is_free(Target::Pickup(pos.0))) | |
| 84 | + .map(|(_, pos)| pos.0) | |
| 85 | + .collect() | |
| 86 | + }; | |
| 87 | + let search = |tiles: HashSet<UVec2>| { | |
| 88 | + (!tiles.is_empty()) | |
| 89 | + .then(|| nearest_tile(map, from, FOOD_SEARCH_TILES, |p| tiles.contains(&p))) | |
| 90 | + .flatten() | |
| 83 | 91 | }; |
| 84 | - nearest_tile(map, from, FOOD_SEARCH_TILES, |p| { | |
| 85 | - has(p, |k| k == ItemKind::Meal) | |
| 86 | - }) | |
| 87 | - .or_else(|| { | |
| 88 | - nearest_tile(map, from, FOOD_SEARCH_TILES, |p| { | |
| 89 | - has(p, ItemKind::is_raw_food) | |
| 90 | - }) | |
| 91 | - }) | |
| 92 | + search(tiles(|k| k == ItemKind::Meal)).or_else(|| search(tiles(ItemKind::is_raw_food))) | |
| 92 | 93 | } |
| 93 | 94 | |
| 94 | 95 | pub fn spoil_food( |
src/jobs/mod.rs +26 -7
| @@ -665,13 +665,27 @@ pub fn find_campfire(ctx: &BuildContext, from: UVec2) -> Option<Task> { | ||
| 665 | 665 | ctx.build_task(from, site, BuildKind::Structure(StructureKind::Campfire)) |
| 666 | 666 | } |
| 667 | 667 | |
| 668 | +pub fn food_to_fetch(reservations: &Reservations, food_tiles: &HashSet<UVec2>) -> bool { | |
| 669 | + food_tiles | |
| 670 | + .iter() | |
| 671 | + .any(|&p| reservations.is_free(Target::Pickup(p))) | |
| 672 | +} | |
| 673 | + | |
| 668 | 674 | pub fn raw_food_source(ctx: &BuildContext, from: UVec2) -> Option<UVec2> { |
| 669 | - nearest_tile(ctx.map, from, SEARCH_TILES, |p| { | |
| 670 | - ctx.reservations.is_free(Target::Pickup(p)) | |
| 671 | - && ctx.items.iter().any(|(item, pos)| { | |
| 672 | - pos.0 == p && item.kind.is_raw_food() && item.count >= COOK_INPUT | |
| 673 | - }) | |
| 674 | - }) | |
| 675 | + let sources: HashSet<UVec2> = ctx | |
| 676 | + .items | |
| 677 | + .iter() | |
| 678 | + .filter(|(item, pos)| { | |
| 679 | + item.kind.is_raw_food() | |
| 680 | + && item.count >= COOK_INPUT | |
| 681 | + && ctx.reservations.is_free(Target::Pickup(pos.0)) | |
| 682 | + }) | |
| 683 | + .map(|(_, pos)| pos.0) | |
| 684 | + .collect(); | |
| 685 | + if sources.is_empty() { | |
| 686 | + return None; | |
| 687 | + } | |
| 688 | + nearest_tile(ctx.map, from, SEARCH_TILES, |p| sources.contains(&p)) | |
| 675 | 689 | } |
| 676 | 690 | |
| 677 | 691 | pub fn find_cooking(ctx: &BuildContext, from: UVec2) -> Option<Task> { |
| @@ -811,10 +825,15 @@ fn choose_jobs( | ||
| 811 | 825 | _ => {} |
| 812 | 826 | } |
| 813 | 827 | } |
| 828 | + let food_tiles: HashSet<UVec2> = items | |
| 829 | + .iter() | |
| 830 | + .filter(|(_, item, _)| item.kind.is_food()) | |
| 831 | + .map(|(_, _, pos)| pos.0) | |
| 832 | + .collect(); | |
| 814 | 833 | let hour = hour_of_day(*tick); |
| 815 | 834 | for (entity, tile, mut path, mut job, mut idle, mut carrying, rest, hunger) in &mut colonists { |
| 816 | 835 | let exhausted = rest.0 < EXHAUSTED_REST; |
| 817 | - let starving = hunger.0 < STARVING; | |
| 836 | + let starving = hunger.0 < STARVING && food_to_fetch(&reservations, &food_tiles); | |
| 818 | 837 | let urgent = exhausted || starving; |
| 819 | 838 | if urgent |
| 820 | 839 | && job.0.is_some_and(|j| { |
src/jobs/tests.rs +14 -0
| @@ -809,6 +809,20 @@ fn starving_colonists_drop_their_work_to_eat() { | ||
| 809 | 809 | assert!(matches!(task(&app, colonist), Some(Task::Eat { .. }))); |
| 810 | 810 | } |
| 811 | 811 | |
| 812 | +// Starvation only interrupts work when there is food to fetch. Without that check a starving | |
| 813 | +// colonist dropped their job every tick, searched the whole map for food and picked the same | |
| 814 | +// job again, never finishing anything and stalling the simulation. | |
| 815 | +#[test] | |
| 816 | +fn starving_colonists_without_food_keep_working() { | |
| 817 | + let tree = UVec2::new(6, 5); | |
| 818 | + let mut app = test_app(map_with_trees(&[tree])); | |
| 819 | + set_goals(&mut app, &[(ItemKind::Wood, 50)]); | |
| 820 | + let colonist = spawn_colonist(&mut app, UVec2::new(2, 5)); | |
| 821 | + set_fed(&mut app, colonist, STARVING / 2.0); | |
| 822 | + run_ticks(&mut app, 60 + 60 * 4 + WorkKind::Chop.work()); | |
| 823 | + assert!(items_of(&mut app, ItemKind::Wood) > 0); | |
| 824 | +} | |
| 825 | + | |
| 812 | 826 | #[test] |
| 813 | 827 | fn colonists_forage_ripe_berries_when_the_colony_wants_them() { |
| 814 | 828 | let mut map = map_with_trees(&[]); |