Stop starving colonists from dropping work when there is no food

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

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

@@ -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(&[]);