Hunts leave carcasses that colonists butcher into meat

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

author
Marijn Besseling <njirambem@gmail.com> · 2026-10-24 22:11 UTC
commit
137cf1843fb299dab6f19eadc73c61e7a67ec24d
parent
1bacc518e6
tree
browse at this commit

6 files changed +77 -5

src/economy.rs +3 -1

@@ -47,7 +47,9 @@ impl Stock {
47 47 ItemKind::Stone => Stock::Stone,
48 48 ItemKind::Iron => Stock::Iron,
49 49 ItemKind::Gold => Stock::Gold,
50 - ItemKind::Berries | ItemKind::Meat | ItemKind::Vegetables => Stock::RawFood,
50 + ItemKind::Berries | ItemKind::Meat | ItemKind::Vegetables | ItemKind::Carcass => {
51 + Stock::RawFood
52 + }
51 53 ItemKind::Meal => Stock::Meals,
52 54 }
53 55 }

src/food.rs +1 -1

@@ -33,7 +33,7 @@ const SPOIL_INTERVAL_TICKS: u64 = 60;
33 33
34 34 pub fn shelf_life_hours(kind: ItemKind) -> Option<f32> {
35 35 match kind {
36 - ItemKind::Meat => Some(48.0),
36 + ItemKind::Meat | ItemKind::Carcass => Some(48.0),
37 37 ItemKind::Berries => Some(72.0),
38 38 ItemKind::Meal => Some(96.0),
39 39 ItemKind::Vegetables => Some(120.0),

src/items.rs +4 -1

@@ -29,10 +29,11 @@ pub enum ItemKind {
29 29 Meat,
30 30 Vegetables,
31 31 Meal,
32 + Carcass,
32 33 }
33 34
34 35 impl ItemKind {
35 - pub const ALL: [ItemKind; 8] = [
36 + pub const ALL: [ItemKind; 9] = [
36 37 ItemKind::Wood,
37 38 ItemKind::Stone,
38 39 ItemKind::Iron,
@@ -41,6 +42,7 @@ impl ItemKind {
41 42 ItemKind::Meat,
42 43 ItemKind::Vegetables,
43 44 ItemKind::Meal,
45 + ItemKind::Carcass,
44 46 ];
45 47
46 48 pub fn nutrition(self) -> f32 {
@@ -69,6 +71,7 @@ impl ItemKind {
69 71 ItemKind::Meat => Color::srgb_u8(200, 80, 70),
70 72 ItemKind::Vegetables => Color::srgb_u8(120, 170, 60),
71 73 ItemKind::Meal => Color::srgb_u8(240, 210, 150),
74 + ItemKind::Carcass => Color::srgb_u8(120, 60, 50),
72 75 }
73 76 }
74 77 }

src/jobs/mod.rs +54 -1

@@ -122,6 +122,10 @@ pub enum Task {
122 122 station: UVec2,
123 123 progress: u32,
124 124 },
125 + Butcher {
126 + site: UVec2,
127 + progress: u32,
128 + },
125 129 }
126 130
127 131 pub fn work_rate(light: &LightMap, tile: UVec2) -> u32 {
@@ -348,6 +352,7 @@ fn reserve_task(reservations: &mut Reservations, task: Task, by: Entity) {
348 352 }
349 353 }
350 354 Task::Hunt { animal, .. } => reservations.reserve(Target::Animal(animal), by),
355 + Task::Butcher { site, .. } => reservations.reserve(Target::Pickup(site), by),
351 356 Task::Cook {
352 357 pickup, station, ..
353 358 } => {
@@ -689,6 +694,25 @@ pub fn raw_food_source(ctx: &BuildContext, from: UVec2) -> Option<UVec2> {
689 694 nearest_of(ctx.map, from, SEARCH_TILES, &sources)
690 695 }
691 696
697 +pub const BUTCHER_WORK: u32 = 240;
698 +
699 +pub fn find_butchering(
700 + map: &WorldMap,
701 + reservations: &Reservations,
702 + items: &[(Item, TilePos)],
703 + from: UVec2,
704 +) -> Option<Task> {
705 + let carcasses: HashSet<UVec2> = items
706 + .iter()
707 + .filter(|(item, pos)| {
708 + item.kind == ItemKind::Carcass && reservations.is_free(Target::Pickup(pos.0))
709 + })
710 + .map(|(_, pos)| pos.0)
711 + .collect();
712 + let site = nearest_of(map, from, SEARCH_TILES, &carcasses)?;
713 + Some(Task::Butcher { site, progress: 0 })
714 +}
715 +
692 716 pub fn find_cooking(ctx: &BuildContext, from: UVec2) -> Option<Task> {
693 717 let station = ctx
694 718 .structures
@@ -950,6 +974,7 @@ fn choose_jobs(
950 974 })
951 975 .flatten()
952 976 })
977 + .or_else(|| find_butchering(&map, &reservations, &snapshot, tile.0))
953 978 .or_else(|| find_work(&map, &reservations, tile.0, |_, k| k.is_food() && wanted(k)))
954 979 .or_else(|| {
955 980 (!designations.0.is_empty())
@@ -1180,7 +1205,7 @@ fn run_jobs(
1180 1205 &mut items,
1181 1206 &item_assets,
1182 1207 prey,
1183 - Item::new(ItemKind::Meat, kind.meat()),
1208 + Item::new(ItemKind::Carcass, kind.meat()),
1184 1209 );
1185 1210 Step::Done
1186 1211 }
@@ -1188,6 +1213,34 @@ fn run_jobs(
1188 1213 }
1189 1214 _ => Step::Done,
1190 1215 },
1216 + Task::Butcher { site, progress } => {
1217 + let site = *site;
1218 + if reservations.holder(Target::Pickup(site)) != Some(entity) {
1219 + Step::Done
1220 + } else if tile.0 != site {
1221 + walk_to(&map, tile.0, site, &mut path)
1222 + } else {
1223 + *progress += work_rate(&light, tile.0);
1224 + if *progress < BUTCHER_WORK {
1225 + Step::Continue
1226 + } else {
1227 + let is_carcass = items
1228 + .iter()
1229 + .any(|(_, item, pos)| pos.0 == site && item.kind == ItemKind::Carcass);
1230 + if is_carcass
1231 + && let Some(carcass) =
1232 + take_items(&mut commands, &mut items, site, u32::MAX)
1233 + {
1234 + let meat = Item {
1235 + kind: ItemKind::Meat,
1236 + ..carcass
1237 + };
1238 + drop_items(&mut commands, &mut items, &item_assets, site, meat);
1239 + }
1240 + Step::Done
1241 + }
1242 + }
1243 + }
1191 1244 Task::Eat { pickup, progress } => {
1192 1245 if let Some(from) = *pickup {
1193 1246 if reservations.holder(Target::Pickup(from)) != Some(entity) {

src/jobs/tests.rs +11 -0

@@ -1224,3 +1224,14 @@ fn colonists_deconstruct_marked_structures_and_recover_some_material() {
1224 1224 assert!(app.world().resource::<Blueprints>().0.is_empty());
1225 1225 assert_eq!(items_of(&mut app, ItemKind::Stone), 1);
1226 1226 }
1227 +
1228 +#[test]
1229 +fn carcasses_are_butchered_into_meat() {
1230 + let mut app = test_app(map_with_trees(&[]));
1231 + set_goals(&mut app, &[]);
1232 + put_items(&mut app, UVec2::new(8, 5), ItemKind::Carcass, 8);
1233 + spawn_colonist(&mut app, UVec2::new(2, 5));
1234 + run_ticks(&mut app, 61 + 60 * 3 + BUTCHER_WORK);
1235 + assert_eq!(items_of(&mut app, ItemKind::Carcass), 0);
1236 + assert_eq!(items_of(&mut app, ItemKind::Meat), 8);
1237 +}

src/selection.rs +4 -1

@@ -11,7 +11,7 @@ use crate::{
11 11 family::{Child, Partner},
12 12 food::Hunger,
13 13 items::{Carrying, Item, stock},
14 - jobs::{COOK_WORK, CurrentJob, EXPAND_WORK, Job, Order, Task, WorkKind},
14 + jobs::{BUTCHER_WORK, COOK_WORK, CurrentJob, EXPAND_WORK, Job, Order, Task, WorkKind},
15 15 light::daylight,
16 16 needs::Rest,
17 17 population::ColonyLog,
@@ -466,6 +466,7 @@ pub fn progress_fraction(task: Task) -> Option<f32> {
466 466 Task::Build { kind, progress, .. } => Some(progress as f32 / kind.work() as f32),
467 467 Task::Hunt { kind, progress, .. } => Some(progress as f32 / kind.hunt_work() as f32),
468 468 Task::Cook { progress, .. } => Some(progress as f32 / COOK_WORK as f32),
469 + Task::Butcher { progress, .. } => Some(progress as f32 / BUTCHER_WORK as f32),
469 470 _ => None,
470 471 }
471 472 }
@@ -565,6 +566,8 @@ pub fn describe_colonist(view: &ColonistView) -> String {
565 566 ) => "fetching ingredients".to_string(),
566 567 (Some(Task::Cook { .. }), n) if n > 0 => "carrying ingredients to the fire".to_string(),
567 568 (Some(task @ Task::Cook { .. }), _) => format!("cooking a meal, {}%", percent(task)),
569 + (Some(Task::Butcher { .. }), n) if n > 0 => "walking to a carcass".to_string(),
570 + (Some(task @ Task::Butcher { .. }), _) => format!("butchering, {}%", percent(task)),
568 571 (Some(task @ Task::Build { kind, .. }), _) => {
569 572 format!("building a {}, {}%", kind.name(), percent(task))
570 573 }