Refuel fires before they burn out

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

author
Marijn Besseling <njirambem@gmail.com> · 2026-10-24 21:58 UTC
commit
5531aaf964782e7e99d5941852b95300e2ec58e8
parent
8c5f119097
tree
browse at this commit

3 files changed +63 -4

src/building.rs +4 -0

@@ -43,6 +43,7 @@ pub enum BuildKind {
43 43 Structure(StructureKind),
44 44 Roof,
45 45 Pave,
46 + Refuel(StructureKind),
46 47 }
47 48
48 49 impl BuildKind {
@@ -51,6 +52,7 @@ impl BuildKind {
51 52 BuildKind::Structure(kind) => kind.cost(),
52 53 BuildKind::Roof => (ItemKind::Wood, 1),
53 54 BuildKind::Pave => (ItemKind::Stone, 1),
55 + BuildKind::Refuel(kind) => kind.cost(),
54 56 }
55 57 }
56 58
@@ -63,6 +65,7 @@ impl BuildKind {
63 65 BuildKind::Structure(StructureKind::Door) => 300,
64 66 BuildKind::Roof => 120,
65 67 BuildKind::Pave => 120,
68 + BuildKind::Refuel(_) => 60,
66 69 }
67 70 }
68 71
@@ -71,6 +74,7 @@ impl BuildKind {
71 74 BuildKind::Structure(kind) => kind.name(),
72 75 BuildKind::Roof => "roof",
73 76 BuildKind::Pave => "paving",
77 + BuildKind::Refuel(_) => "refuelling",
74 78 }
75 79 }
76 80

src/jobs/mod.rs +38 -4

@@ -49,6 +49,7 @@ impl Plugin for JobsPlugin {
49 49 }
50 50
51 51 pub const EXPAND_WORK: u32 = 240;
52 +pub const REFUEL_BELOW_SHARE: u32 = 3;
52 53 pub const CAMPFIRE_MIN_FUEL: u32 = 3 * crate::sim::TICKS_PER_HOUR as u32;
53 54 pub const HOME_RADIUS: f32 = 10.0;
54 55 pub const BED_STOCKPILE_CLEARANCE: i32 = 3;
@@ -717,6 +718,21 @@ pub fn find_lighting(ctx: &BuildContext, from: UVec2) -> Option<Task> {
717 718 ctx.build_task(from, site, BuildKind::Structure(kind))
718 719 }
719 720
721 +pub fn find_refuel(ctx: &BuildContext, from: UVec2) -> Option<Task> {
722 + let home = ctx.zones.home(from);
723 + let (site, kind, _) = ctx
724 + .structures
725 + .iter()
726 + .filter(|&&(pos, kind, fuel)| {
727 + kind.fuel_ticks()
728 + .is_some_and(|full| fuel < full / REFUEL_BELOW_SHARE)
729 + && pos.as_vec2().distance(home.as_vec2()) <= HOME_RADIUS * 2.0
730 + && ctx.reservations.is_free(Target::Site(pos))
731 + })
732 + .min_by_key(|(.., fuel)| *fuel)?;
733 + ctx.build_task(from, *site, BuildKind::Refuel(*kind))
734 +}
735 +
720 736 pub fn find_bed_site(ctx: &BuildContext, colonists: usize, from: UVec2) -> Option<Task> {
721 737 if ctx.count(StructureKind::Bed) >= colonists {
722 738 return None;
@@ -906,6 +922,7 @@ fn choose_jobs(
906 922 blueprints: &blueprints,
907 923 };
908 924 find_expansion(&map, &zones, &reservations, &snapshot, &blueprints)
925 + .or_else(|| find_refuel(&build, tile.0))
909 926 .or_else(|| dark.then(|| find_lighting(&build, tile.0)).flatten())
910 927 .or_else(|| {
911 928 let cooking =
@@ -1004,7 +1021,7 @@ fn run_jobs(
1004 1021 mut blueprints: ResMut<Blueprints>,
1005 1022 mut goals: ResMut<ColonyGoals>,
1006 1023 mut items: ItemQuery,
1007 - structures: Query<(&TilePos, &Structure), Without<Colonist>>,
1024 + mut structures: Query<(&TilePos, &Structure, Option<&mut Fuel>), Without<Colonist>>,
1008 1025 animals: Query<(&TilePos, &Path), (With<Animal>, Without<Colonist>)>,
1009 1026 mut changed: MessageWriter<TileChanged>,
1010 1027 mut colonists: Query<
@@ -1051,7 +1068,7 @@ fn run_jobs(
1051 1068 let station = *station;
1052 1069 let fire = structures
1053 1070 .iter()
1054 - .any(|(pos, s)| pos.0 == station && s.kind == StructureKind::Campfire);
1071 + .any(|(pos, s, _)| pos.0 == station && s.kind == StructureKind::Campfire);
1055 1072 let ours = reservations.holder(Target::Station(station)) == Some(entity);
1056 1073 if !fire || !ours {
1057 1074 Step::Done
@@ -1270,11 +1287,19 @@ fn run_jobs(
1270 1287 let planned = match kind {
1271 1288 BuildKind::Structure(
1272 1289 StructureKind::Campfire | StructureKind::Torch | StructureKind::Bed,
1273 - ) => true,
1290 + )
1291 + | BuildKind::Refuel(_) => true,
1274 1292 _ => blueprints.get(site) == Some(kind),
1275 1293 };
1294 + let structure_there = structures.iter().any(|(pos, ..)| pos.0 == site);
1276 1295 let blocked = map.plant(site.as_ivec2()).is_some()
1277 - || (kind != BuildKind::Roof && structures.iter().any(|(pos, _)| pos.0 == site));
1296 + || match kind {
1297 + BuildKind::Roof => false,
1298 + BuildKind::Refuel(fire) => !structures
1299 + .iter()
1300 + .any(|(pos, s, _)| pos.0 == site && s.kind == fire),
1301 + _ => structure_there,
1302 + };
1278 1303 let (material, cost) = kind.cost();
1279 1304 if !ours || blocked || !planned {
1280 1305 Step::Done
@@ -1354,6 +1379,15 @@ fn run_jobs(
1354 1379 map.set(site, Terrain::Paved);
1355 1380 changed.write(TileChanged(site));
1356 1381 }
1382 + BuildKind::Refuel(fire) => {
1383 + for (pos, _, fuel) in &mut structures {
1384 + if pos.0 == site
1385 + && let Some(mut fuel) = fuel
1386 + {
1387 + fuel.0 = fire.fuel_ticks().unwrap_or(fuel.0);
1388 + }
1389 + }
1390 + }
1357 1391 }
1358 1392 blueprints.0.remove(&site);
1359 1393 Step::Done

src/jobs/tests.rs +21 -0

@@ -1159,3 +1159,24 @@ fn children_play_instead_of_working() {
1159 1159 assert!(!matches!(task(&app, child), Some(Task::Work { .. })));
1160 1160 assert_eq!(items_of(&mut app, ItemKind::Wood), 0);
1161 1161 }
1162 +
1163 +#[test]
1164 +fn colonists_refuel_a_dying_campfire_instead_of_building_a_new_one() {
1165 + let mut app = test_app(map_with_trees(&[]));
1166 + set_goals(&mut app, &[]);
1167 + put_beds(&mut app, 1);
1168 + put_campfire(&mut app, UVec2::new(10, 10));
1169 + let full = StructureKind::Campfire.fuel_ticks().unwrap();
1170 + {
1171 + let world = app.world_mut();
1172 + let mut fuel = world.query::<&mut Fuel>();
1173 + fuel.single_mut(world).unwrap().0 = full / 10;
1174 + }
1175 + put_items(&mut app, UVec2::new(5, 5), ItemKind::Wood, 20);
1176 + spawn_colonist(&mut app, UVec2::new(2, 2));
1177 + run_ticks(&mut app, 60 * 15);
1178 + let world = app.world_mut();
1179 + let fuel: Vec<u32> = world.query::<&Fuel>().iter(world).map(|f| f.0).collect();
1180 + assert_eq!(fuel.len(), 1);
1181 + assert!(fuel[0] > full / 2, "{fuel:?}");
1182 +}