Move jobs into a module with tests in their own file

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

author
Marijn Besseling <njirambem@gmail.com> · 2026-10-24 11:01 UTC
commit
5a322f3fb79958b84e75a6abaa27f25e2a8033e4
parent
8c7d843711
tree
browse at this commit

3 files changed +779 -783

CLAUDE.md +1 -1
src/jobs/mod.rs renamed +1 -782
src/jobs/tests.rs added +777 -0

CLAUDE.md +1 -1

@@ -22,7 +22,7 @@ DF/RimWorld-style colony sim, Rust + Bevy 0.20 (newer than training data — che
22 22 - `src/selection.rs` (`update_cursor` is pub; other modules order `.after(update_cursor)`) `Cursor` resource (screen/world/tile) and `Selection` enum (None/Tile/Colonist(Entity)). PostUpdate after `TransformSystems::Propagate` (fresh camera GlobalTransform). LMB: pick nearest colonist within `PICK_RADIUS` of cursor world pos, else tile, off-map clears; Esc clears. RMB click (press/release within `CLICK_SLOP_PX`, else it was a drag-pan) writes `MoveOrder` for selected colonist. Gizmos: hover rect, selected rect / colonist circle + remaining path linestrip. Top-left UI text (clock + speed/PAUSED, seed, per-material stock/goal, zone tool hint, hover incl. stockpile/items, selection incl. job/carrying). Progress bars for Work/ExpandStockpile/Build (`progress_fraction`). Clock shows day/dawn/dusk/night; hover shows structures with remaining fuel. Chop progress bars (gizmo lines) above working colonists; selected colonist's target tree outlined. Tests use headless `App` + `ButtonInput`/`Cursor` resources, `Messages<MoveOrder>::drain`.
23 23 - `src/pathfinding.rs` generic `astar(map, start, is_goal, heuristic)`, 8-dir, no corner cutting, cost = (10|14) * `map.step_cost` (100 = normal flat grass), octile heuristic. `find_path(start, goal)`, `find_path_adjacent(start, target)` (ends on a tile Chebyshev-adjacent to target; works for impassable targets; empty if already adjacent), `nearest_tile(start, max_tiles, pred)` BFS over walkable tiles. Paths exclude start, include goal.
24 24 - `src/colonist.rs` `Colonist{name}` (spawned/respawned on `WorldGenerated` at `spawn_tiles`) requires `TilePos` (world.rs; last reached tile), `Path(VecDeque)`, `CurrentJob`, `IdleTimer`. `follow_path` (SimSystems::Move) moves `SimPos`, speed / step cost. `repath` keeps the tile being entered at the front.
25 -- `src/jobs.rs` job system. `CurrentJob(Option<Job{task, ordered}>)`, `Task::{MoveTo, Work{site, kind: WorkKind::{Chop, Mine}, progress}, Haul{pickup: Option<tile>, to}, ExpandStockpile{site, progress}, Build{site, kind: StructureKind, pickup: Option<tile>, progress}}`. Progress is in work units: `work_rate` = 2/tick when the colonist's tile is lit, 1 in the dark (`WorkKind::work`, `EXPAND_WORK`, `build_work`). `site_work(map, tile)` is the single source of what a tile yields (tree → 10 wood; Stone/IronOre/GoldOre → 10 stone / 10 iron / 5 gold, mined tile becomes Gravel). `Reservations` keyed by `Target::{Site, Pickup, Storage}(tile)` and `Target::StockpileExpansion` (one expander at a time); cleared on WorldGenerated. `ColonyGoals(HashMap<ItemKind, u32>)` = demand (wood 50, stone 40, iron 20, gold 10); `wanted(kind, stock, on_the_way)`. `choose_jobs` (SimSystems::Decide), idle colonists (exhausted ones skip the idle timer), priority: sleep if `wants_sleep` → deliver carried load (or drop if no storage) → `find_expansion` (no stockpile + loose items → found a NEW_STOCKPILE_SIZE² stockpile at `find_stockpile_site` near the central loose item; or stockpile has < MIN_EMPTY_STOCKPILE_TILES empty tiles → grow EXPANSION_TILES via `plan_expansion`) → if `dark_soon`: `find_lighting` (`BuildContext` (shared by lighting and beds; `buildable` = dry, not stockpile/plant/structure/planned/item, unreserved): campfire within HOME_RADIUS of home = stockpile tile nearest its centroid unless one burns with ≥ CAMPFIRE_MIN_FUEL or is planned; else a torch next to the stockpile tile nearest home that existing + planned fires leave dark; needs a reachable unreserved wood stack ≥ cost; Build fetches wood, walks adjacent, builds, consumes wood) → `find_work` (nearest stand tile adjacent to an unreserved site whose yield is wanted, counting yields of work in progress) → `find_haul` → `find_bed_site` (one bed per colonist incl. planned; next to nearest bed, else campfire, else home; ≥ BED_STOCKPILE_CLEARANCE from stockpile tiles so a wall ring fits) → `find_blueprint_work` (nearest unreserved blueprint with material available) → wander. `run_jobs` (SimSystems::Act) per-task state machine → `Step::{Continue, Done}`; Done releases reservations, restarts idle timer. Orders: work site (tree/rock) → ordered Work; loose item → ordered Haul; else MoveTo. Orders keep `Carrying`. New job = Task variant + reserve_task arm + choose_jobs candidate + run_jobs arm (+ `site_work` entry for site work).
25 +- `src/jobs/mod.rs` job system (tests in `src/jobs/tests.rs`). `CurrentJob(Option<Job{task, ordered}>)`, `Task::{MoveTo, Work{site, kind: WorkKind::{Chop, Mine}, progress}, Haul{pickup: Option<tile>, to}, ExpandStockpile{site, progress}, Build{site, kind: StructureKind, pickup: Option<tile>, progress}}`. Progress is in work units: `work_rate` = 2/tick when the colonist's tile is lit, 1 in the dark (`WorkKind::work`, `EXPAND_WORK`, `build_work`). `site_work(map, tile)` is the single source of what a tile yields (tree → 10 wood; Stone/IronOre/GoldOre → 10 stone / 10 iron / 5 gold, mined tile becomes Gravel). `Reservations` keyed by `Target::{Site, Pickup, Storage}(tile)` and `Target::StockpileExpansion` (one expander at a time); cleared on WorldGenerated. `ColonyGoals(HashMap<ItemKind, u32>)` = demand (wood 50, stone 40, iron 20, gold 10); `wanted(kind, stock, on_the_way)`. `choose_jobs` (SimSystems::Decide), idle colonists (exhausted ones skip the idle timer), priority: sleep if `wants_sleep` → deliver carried load (or drop if no storage) → `find_expansion` (no stockpile + loose items → found a NEW_STOCKPILE_SIZE² stockpile at `find_stockpile_site` near the central loose item; or stockpile has < MIN_EMPTY_STOCKPILE_TILES empty tiles → grow EXPANSION_TILES via `plan_expansion`) → if `dark_soon`: `find_lighting` (`BuildContext` (shared by lighting and beds; `buildable` = dry, not stockpile/plant/structure/planned/item, unreserved): campfire within HOME_RADIUS of home = stockpile tile nearest its centroid unless one burns with ≥ CAMPFIRE_MIN_FUEL or is planned; else a torch next to the stockpile tile nearest home that existing + planned fires leave dark; needs a reachable unreserved wood stack ≥ cost; Build fetches wood, walks adjacent, builds, consumes wood) → `find_work` (nearest stand tile adjacent to an unreserved site whose yield is wanted, counting yields of work in progress) → `find_haul` → `find_bed_site` (one bed per colonist incl. planned; next to nearest bed, else campfire, else home; ≥ BED_STOCKPILE_CLEARANCE from stockpile tiles so a wall ring fits) → `find_blueprint_work` (nearest unreserved blueprint with material available) → wander. `run_jobs` (SimSystems::Act) per-task state machine → `Step::{Continue, Done}`; Done releases reservations, restarts idle timer. Orders: work site (tree/rock) → ordered Work; loose item → ordered Haul; else MoveTo. Orders keep `Carrying`. New job = Task variant + reserve_task arm + choose_jobs candidate + run_jobs arm (+ `site_work` entry for site work).
26 26 - `src/items.rs` `Item{kind: ItemKind::{Wood, Stone, Iron, Gold}, count}` + `TilePos` entities (Mesh2d rect, z=0.7), one stack per tile, `STACK_LIMIT`=50; `ItemQuery` alias; `drop_items` stacks, `take_items`, `room_for(tile, kind)` (0 if other kind present), `stock()`. `Carrying(Option<Item>)` on colonists, shown via hidden/visible `CarriedVisual` child. Items cleared on WorldGenerated.
27 27 - `src/zones.rs` `Zones` resource (stockpile tile set; only walkable dry land), cleared on WorldGenerated — worlds start without stockpiles, colonists found one when needed. `plan_expansion(map, zones, excluded, count)` (excluded = blueprint tiles) grows one tile at a time next to stockpile/planned tiles, preferring tree-free tiles near the stockpile centroid (deterministic tiebreak). `find_stockpile_site` = BFS for nearest fully open (zonable, tree-free) square, fallback any zonable tile. `ZoneTool` (Z/button toggles, Cancel exits): left-drag adds, shift-drag removes (`zone_tool` input + `draw_zone_preview` gizmo, PostUpdate after `update_cursor`). Rendered as tilemap `Layer::Zones` (z=0.3), refreshed on `resource_changed::<Zones>`.
28 28 - `src/sim.rs` `SimPlugin`: `SimSystems::{Decide, Act, Move}` chained sets in FixedUpdate; fixed timestep `Time<Fixed>` 60 Hz = sim ticks; all sim logic in `FixedUpdate` (Res<Time> there = fixed delta). `SimTick` counter (FixedFirst), `format_clock` (600 ticks/hour = 10 s real at 1x, starts Day 1 06:00). `SimSpeed` Normal/Fast/Fastest = 1x/3x/6x via `Time<Virtual>::set_relative_speed`; keys 1/2/3 set speed + unpause, Space toggles pause. Render interpolation: sim entities hold `SimPos` (+ `PrevSimPos`, copied in FixedFirst); `interpolate_transforms` lerps Transform by `overstep_fraction` in `RunFixedMainLoop`/AfterFixedMainLoop. Spawn with `sim_pos(v)` so prev==cur. Never write Transform of sim entities directly.

src/jobs.rs → src/jobs/mod.rs +1 -782

@@ -1038,785 +1038,4 @@ fn run_jobs(
1038 1038 }
1039 1039
1040 1040 #[cfg(test)]
1041 -mod tests {
1042 - use super::*;
1043 - use crate::{
1044 - colonist::follow_path,
1045 - items::STACK_LIMIT,
1046 - sim::{SimPos, sim_pos},
1047 - world::Terrain,
1048 - zones::NEW_STOCKPILE_SIZE,
1049 - };
1050 -
1051 - const TICK: Duration = Duration::from_nanos(1_000_000_000 / 60);
1052 -
1053 - fn test_app(map: WorldMap) -> App {
1054 - let mut app = App::new();
1055 - app.insert_resource(map)
1056 - .insert_resource(SimRng::new(1))
1057 - .init_resource::<Time>()
1058 - .init_resource::<Reservations>()
1059 - .init_resource::<ColonyGoals>()
1060 - .init_resource::<ItemAssets>()
1061 - .init_resource::<StructureAssets>()
1062 - .init_resource::<Blueprints>()
1063 - .init_resource::<LightMap>()
1064 - .init_resource::<SimTick>()
1065 - .init_resource::<Zones>()
1066 - .add_message::<Order>()
1067 - .add_message::<TileChanged>()
1068 - .add_systems(
1069 - Update,
1070 - (
1071 - handle_orders,
1072 - choose_jobs,
1073 - run_jobs,
1074 - follow_path,
1075 - crate::needs::update_rest,
1076 - )
1077 - .chain(),
1078 - );
1079 - app
1080 - }
1081 -
1082 - fn spawn_colonist(app: &mut App, tile: UVec2) -> Entity {
1083 - app.world_mut()
1084 - .spawn((
1085 - Colonist {
1086 - name: "Test".into(),
1087 - },
1088 - TilePos(tile),
1089 - sim_pos(WorldMap::tile_center(tile)),
1090 - ))
1091 - .id()
1092 - }
1093 -
1094 - fn run_ticks(app: &mut App, ticks: u32) {
1095 - for _ in 0..ticks {
1096 - app.world_mut().resource_mut::<Time>().advance_by(TICK);
1097 - app.update();
1098 - }
1099 - }
1100 -
1101 - fn map_with_trees(trees: &[UVec2]) -> WorldMap {
1102 - let mut map = WorldMap::filled(UVec2::new(20, 20), Terrain::Grass);
1103 - for &t in trees {
1104 - map.set_plant(t, Some(Plant::Tree));
1105 - }
1106 - map
1107 - }
1108 -
1109 - fn wood_on_map(app: &mut App) -> u32 {
1110 - let world = app.world_mut();
1111 - let items: Vec<Item> = world.query::<&Item>().iter(world).copied().collect();
1112 - stock(items.iter(), ItemKind::Wood)
1113 - }
1114 -
1115 - fn items_at(app: &mut App) -> Vec<(UVec2, u32)> {
1116 - let world = app.world_mut();
1117 - let mut items: Vec<(UVec2, u32)> = world
1118 - .query::<(&Item, &TilePos)>()
1119 - .iter(world)
1120 - .map(|(item, pos)| (pos.0, item.count))
1121 - .collect();
1122 - items.sort_by_key(|(p, _)| (p.y, p.x));
1123 - items
1124 - }
1125 -
1126 - fn put_wood(app: &mut App, tile: UVec2, count: u32) {
1127 - app.world_mut().spawn((
1128 - Item {
1129 - kind: ItemKind::Wood,
1130 - count,
1131 - },
1132 - TilePos(tile),
1133 - ));
1134 - }
1135 -
1136 - fn set_goals(app: &mut App, goals: &[(ItemKind, u32)]) {
1137 - *app.world_mut().resource_mut::<ColonyGoals>() =
1138 - ColonyGoals(goals.iter().copied().collect());
1139 - }
1140 -
1141 - fn stockpile_size(app: &App) -> usize {
1142 - let map = app.world().resource::<WorldMap>();
1143 - let zones = app.world().resource::<Zones>();
1144 - map.positions().filter(|&p| zones.is_stockpile(p)).count()
1145 - }
1146 -
1147 - fn stored(app: &mut App) -> u32 {
1148 - let zones = app.world().resource::<Zones>().clone();
1149 - items_at(app)
1150 - .iter()
1151 - .filter(|(p, _)| zones.is_stockpile(*p))
1152 - .map(|(_, c)| c)
1153 - .sum()
1154 - }
1155 -
1156 - fn put_beds(app: &mut App, count: u32) {
1157 - for x in 0..count {
1158 - app.world_mut().spawn((
1159 - Structure {
1160 - kind: StructureKind::Bed,
1161 - },
1162 - TilePos(UVec2::new(19 - x, 19)),
1163 - ));
1164 - }
1165 - }
1166 -
1167 - fn hauling_app(stockpile: (IVec2, IVec2)) -> App {
1168 - let mut app = test_app(map_with_trees(&[]));
1169 - put_beds(&mut app, 2);
1170 - set_goals(&mut app, &[]);
1171 - let map = app.world().resource::<WorldMap>().clone();
1172 - app.world_mut()
1173 - .resource_mut::<Zones>()
1174 - .add_stockpile(&map, stockpile.0, stockpile.1);
1175 - app
1176 - }
1177 -
1178 - fn carrying(app: &App, colonist: Entity) -> Option<Item> {
1179 - app.world().get::<Carrying>(colonist).unwrap().0
1180 - }
1181 -
1182 - fn job(app: &App, colonist: Entity) -> Option<Job> {
1183 - app.world().get::<CurrentJob>(colonist).unwrap().0
1184 - }
1185 -
1186 - #[test]
1187 - fn idle_colonists_cut_trees_when_the_colony_needs_wood() {
1188 - let tree = UVec2::new(8, 5);
1189 - let mut app = test_app(map_with_trees(&[tree]));
1190 - spawn_colonist(&mut app, UVec2::new(2, 5));
1191 - run_ticks(&mut app, 60 * 15);
1192 - assert_eq!(
1193 - app.world().resource::<WorldMap>().plant(tree.as_ivec2()),
1194 - None
1195 - );
1196 - assert_eq!(wood_on_map(&mut app), 10);
1197 - assert!(app.world().resource::<Reservations>().0.is_empty());
1198 - }
1199 -
1200 - #[test]
1201 - fn colonists_leave_trees_alone_when_there_is_enough_wood() {
1202 - let tree = UVec2::new(8, 5);
1203 - let mut app = test_app(map_with_trees(&[tree]));
1204 - set_goals(&mut app, &[]);
1205 - spawn_colonist(&mut app, UVec2::new(2, 5));
1206 - run_ticks(&mut app, 60 * 15);
1207 - assert_eq!(
1208 - app.world().resource::<WorldMap>().plant(tree.as_ivec2()),
1209 - Some(Plant::Tree)
1210 - );
1211 - }
1212 -
1213 - #[test]
1214 - fn colonists_never_pick_the_same_tree() {
1215 - let trees = [UVec2::new(10, 10), UVec2::new(10, 11)];
1216 - let mut app = test_app(map_with_trees(&trees));
1217 - let a = spawn_colonist(&mut app, UVec2::new(5, 10));
1218 - let b = spawn_colonist(&mut app, UVec2::new(5, 11));
1219 - run_ticks(&mut app, 61);
1220 - let target = |c| match job(&app, c) {
1221 - Some(Job {
1222 - task: Task::Work { site, .. },
1223 - ..
1224 - }) => site,
1225 - other => panic!("expected chop job, got {other:?}"),
1226 - };
1227 - assert_ne!(target(a), target(b));
1228 - }
1229 -
1230 - // Each tree or rock being worked will add its yield; counting that as material on the way
1231 - // keeps the whole colony from felling a forest when only a little more wood is needed.
1232 - #[test]
1233 - fn work_in_progress_counts_towards_goals() {
1234 - let goals = ColonyGoals(HashMap::from([(ItemKind::Wood, 25)]));
1235 - assert!(goals.wanted(ItemKind::Wood, 10, 10));
1236 - assert!(!goals.wanted(ItemKind::Wood, 10, 20));
1237 - assert!(!goals.wanted(ItemKind::Wood, 30, 0));
1238 - assert!(!goals.wanted(ItemKind::Gold, 0, 0));
1239 - }
1240 -
1241 - #[test]
1242 - fn ordered_chop_ignores_the_wood_goal() {
1243 - let tree = UVec2::new(8, 5);
1244 - let mut app = test_app(map_with_trees(&[tree]));
1245 - set_goals(&mut app, &[]);
1246 - let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
1247 - app.world_mut().write_message(Order {
1248 - colonist,
1249 - tile: tree,
1250 - });
1251 - run_ticks(&mut app, 60 * 15);
1252 - assert_eq!(
1253 - app.world().resource::<WorldMap>().plant(tree.as_ivec2()),
1254 - None
1255 - );
1256 - }
1257 -
1258 - #[test]
1259 - fn ordering_a_move_cancels_the_current_job_and_frees_its_tree() {
1260 - let tree = UVec2::new(15, 5);
1261 - let mut app = test_app(map_with_trees(&[tree]));
1262 - let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
1263 - run_ticks(&mut app, 61);
1264 - assert!(matches!(
1265 - job(&app, colonist),
1266 - Some(Job {
1267 - task: Task::Work { .. },
1268 - ..
1269 - })
1270 - ));
1271 - app.world_mut().write_message(Order {
1272 - colonist,
1273 - tile: UVec2::new(2, 15),
1274 - });
1275 - run_ticks(&mut app, 1);
1276 - assert_eq!(
1277 - job(&app, colonist).map(|j| j.task),
1278 - Some(Task::MoveTo(UVec2::new(2, 15)))
1279 - );
1280 - assert!(
1281 - app.world()
1282 - .resource::<Reservations>()
1283 - .is_free(Target::Site(tree))
1284 - );
1285 - }
1286 -
1287 - #[test]
1288 - fn idle_colonists_haul_loose_items_into_the_stockpile() {
1289 - let mut app = hauling_app((IVec2::new(15, 15), IVec2::new(17, 15)));
1290 - put_wood(&mut app, UVec2::new(5, 5), 20);
1291 - spawn_colonist(&mut app, UVec2::new(2, 5));
1292 - run_ticks(&mut app, 60 * 15);
1293 - assert_eq!(items_at(&mut app), vec![(UVec2::new(15, 15), 20)]);
1294 - }
1295 -
1296 - #[test]
1297 - fn large_stacks_take_several_trips_and_spill_into_more_stockpile_tiles() {
1298 - let mut app = hauling_app((IVec2::new(15, 15), IVec2::new(17, 15)));
1299 - put_wood(&mut app, UVec2::new(5, 5), STACK_LIMIT + 10);
1300 - spawn_colonist(&mut app, UVec2::new(2, 5));
1301 - run_ticks(&mut app, 60 * 40);
1302 - let stacks = items_at(&mut app);
1303 - assert_eq!(stored(&mut app), STACK_LIMIT + 10);
1304 - assert!(stacks.len() >= 2);
1305 - assert!(stacks.iter().all(|(_, count)| *count <= STACK_LIMIT));
1306 - }
1307 -
1308 - #[test]
1309 - fn items_already_in_a_stockpile_stay_put() {
1310 - let mut app = hauling_app((IVec2::new(5, 5), IVec2::new(16, 16)));
1311 - put_wood(&mut app, UVec2::new(5, 5), 20);
1312 - let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
1313 - run_ticks(&mut app, 60 * 5);
1314 - assert_eq!(items_at(&mut app), vec![(UVec2::new(5, 5), 20)]);
1315 - assert_eq!(carrying(&app, colonist), None);
1316 - }
1317 -
1318 - #[test]
1319 - fn colonists_found_a_stockpile_when_items_need_storing() {
1320 - let mut app = test_app(map_with_trees(&[]));
1321 - put_beds(&mut app, 1);
1322 - set_goals(&mut app, &[]);
1323 - put_wood(&mut app, UVec2::new(5, 5), 20);
1324 - spawn_colonist(&mut app, UVec2::new(2, 5));
1325 - run_ticks(&mut app, 60 * 15);
1326 - assert_eq!(
1327 - stockpile_size(&app),
1328 - (NEW_STOCKPILE_SIZE * NEW_STOCKPILE_SIZE) as usize
1329 - );
1330 - assert_eq!(stored(&mut app), 20);
1331 - }
1332 -
1333 - #[test]
1334 - fn no_stockpile_is_founded_while_there_is_nothing_to_store() {
1335 - let mut app = test_app(map_with_trees(&[]));
1336 - set_goals(&mut app, &[]);
1337 - spawn_colonist(&mut app, UVec2::new(2, 5));
1338 - run_ticks(&mut app, 60 * 5);
1339 - assert_eq!(stockpile_size(&app), 0);
1340 - }
1341 -
1342 - #[test]
1343 - fn two_colonists_never_haul_the_same_stack() {
1344 - let mut app = hauling_app((IVec2::new(15, 15), IVec2::new(16, 16)));
1345 - put_wood(&mut app, UVec2::new(5, 5), 10);
1346 - put_wood(&mut app, UVec2::new(5, 6), 10);
1347 - let a = spawn_colonist(&mut app, UVec2::new(2, 5));
1348 - let b = spawn_colonist(&mut app, UVec2::new(2, 6));
1349 - run_ticks(&mut app, 61);
1350 - let pickup = |c| match job(&app, c).map(|j| j.task) {
1351 - Some(Task::Haul { pickup, .. }) => pickup,
1352 - other => panic!("expected haul, got {other:?}"),
1353 - };
1354 - assert_ne!(pickup(a), pickup(b));
1355 - }
1356 -
1357 - // Orders interrupt whatever a colonist is doing, but whatever it is carrying stays in its
1358 - // hands; once it is idle again it finishes delivering it instead of dropping it anywhere.
1359 - #[test]
1360 - fn interrupted_haulers_deliver_their_load_later() {
1361 - let mut app = hauling_app((IVec2::new(15, 15), IVec2::new(17, 15)));
1362 - put_wood(&mut app, UVec2::new(4, 5), 20);
1363 - let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
1364 - let mut ticks = 0;
1365 - while carrying(&app, colonist).is_none() && ticks < 60 * 10 {
1366 - run_ticks(&mut app, 1);
1367 - ticks += 1;
1368 - }
1369 - app.world_mut().write_message(Order {
1370 - colonist,
1371 - tile: UVec2::new(2, 2),
1372 - });
1373 - run_ticks(&mut app, 60 * 3);
1374 - assert!(carrying(&app, colonist).is_some());
1375 - run_ticks(&mut app, 60 * 20);
1376 - assert_eq!(carrying(&app, colonist), None);
1377 - assert_eq!(stored(&mut app), 20);
1378 - }
1379 -
1380 - #[test]
1381 - fn right_clicking_loose_items_orders_a_haul() {
1382 - let mut app = hauling_app((IVec2::new(15, 15), IVec2::new(15, 15)));
1383 - set_goals(&mut app, &[(ItemKind::Wood, 1000)]);
1384 - put_wood(&mut app, UVec2::new(5, 5), 20);
1385 - let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
1386 - app.world_mut().write_message(Order {
1387 - colonist,
1388 - tile: UVec2::new(5, 5),
1389 - });
1390 - run_ticks(&mut app, 1);
1391 - assert!(matches!(
1392 - job(&app, colonist),
1393 - Some(Job {
1394 - task: Task::Haul { .. },
1395 - ordered: true
1396 - })
1397 - ));
1398 - }
1399 -
1400 - fn mining_map() -> WorldMap {
1401 - let mut map = WorldMap::filled(UVec2::new(20, 20), Terrain::Grass);
1402 - for y in 0..20 {
1403 - map.set(UVec2::new(10, y), Terrain::Stone);
1404 - }
1405 - map.set(UVec2::new(10, 5), Terrain::IronOre);
1406 - map
1407 - }
1408 -
1409 - fn items_of(app: &mut App, kind: ItemKind) -> u32 {
1410 - let world = app.world_mut();
1411 - let items: Vec<Item> = world.query::<&Item>().iter(world).copied().collect();
1412 - stock(items.iter(), kind)
1413 - }
1414 -
1415 - #[test]
1416 - fn colonists_mine_rock_the_colony_needs_and_leave_gravel() {
1417 - let mut app = test_app(mining_map());
1418 - set_goals(&mut app, &[(ItemKind::Stone, 10)]);
1419 - spawn_colonist(&mut app, UVec2::new(8, 12));
1420 - run_ticks(&mut app, 60 * 12);
1421 - assert_eq!(items_of(&mut app, ItemKind::Stone), 10);
1422 - assert_eq!(items_of(&mut app, ItemKind::Iron), 0);
1423 - let map = app.world().resource::<WorldMap>();
1424 - let gravel = map
1425 - .positions()
1426 - .filter(|p| map.get(p.as_ivec2()) == Some(Terrain::Gravel))
1427 - .count();
1428 - assert_eq!(gravel, 1);
1429 - }
1430 -
1431 - #[test]
1432 - fn colonists_walk_past_rock_they_do_not_need() {
1433 - let mut app = test_app(mining_map());
1434 - set_goals(&mut app, &[(ItemKind::Iron, 10)]);
1435 - spawn_colonist(&mut app, UVec2::new(8, 15));
1436 - run_ticks(&mut app, 60 * 15);
1437 - assert_eq!(items_of(&mut app, ItemKind::Iron), 10);
1438 - assert_eq!(items_of(&mut app, ItemKind::Stone), 0);
1439 - assert_eq!(
1440 - app.world().resource::<WorldMap>().get(IVec2::new(10, 5)),
1441 - Some(Terrain::Gravel)
1442 - );
1443 - }
1444 -
1445 - #[test]
1446 - fn right_clicking_rock_orders_mining_regardless_of_goals() {
1447 - let mut app = test_app(mining_map());
1448 - set_goals(&mut app, &[]);
1449 - let colonist = spawn_colonist(&mut app, UVec2::new(8, 12));
1450 - app.world_mut().write_message(Order {
1451 - colonist,
1452 - tile: UVec2::new(10, 12),
1453 - });
1454 - run_ticks(&mut app, 60 * 10);
1455 - assert_eq!(
1456 - app.world().resource::<WorldMap>().get(IVec2::new(10, 12)),
1457 - Some(Terrain::Gravel)
1458 - );
1459 - }
1460 -
1461 - #[test]
1462 - fn colonists_expand_a_stockpile_that_is_nearly_full() {
1463 - let mut app = hauling_app((IVec2::new(5, 5), IVec2::new(6, 5)));
1464 - let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
1465 - run_ticks(&mut app, 61);
1466 - assert!(matches!(
1467 - job(&app, colonist).map(|j| j.task),
1468 - Some(Task::ExpandStockpile { .. })
1469 - ));
1470 - run_ticks(&mut app, 60 * 5);
1471 - assert_eq!(stockpile_size(&app), 2 + EXPANSION_TILES);
1472 - }
1473 -
1474 - #[test]
1475 - fn roomy_stockpiles_are_left_alone() {
1476 - let mut app = hauling_app((IVec2::new(5, 5), IVec2::new(8, 5)));
1477 - spawn_colonist(&mut app, UVec2::new(2, 5));
1478 - run_ticks(&mut app, 60 * 5);
1479 - assert_eq!(stockpile_size(&app), 4);
1480 - }
1481 -
1482 - fn set_hour(app: &mut App, hour: u64) {
1483 - let ticks = (hour + 24 - 6) % 24 * crate::sim::TICKS_PER_HOUR;
1484 - *app.world_mut().resource_mut::<SimTick>() = SimTick(ticks);
1485 - }
1486 -
1487 - fn structures(app: &mut App) -> Vec<(UVec2, StructureKind)> {
1488 - let world = app.world_mut();
1489 - world
1490 - .query::<(&TilePos, &Structure)>()
1491 - .iter(world)
1492 - .map(|(pos, s)| (pos.0, s.kind))
1493 - .collect()
1494 - }
1495 -
1496 - fn put_campfire(app: &mut App, tile: UVec2) {
1497 - app.world_mut().spawn((
1498 - Structure {
1499 - kind: StructureKind::Campfire,
1500 - },
1501 - Fuel(StructureKind::Campfire.fuel_ticks().unwrap()),
1502 - TilePos(tile),
1503 - ));
1504 - }
1505 -
1506 - #[test]
1507 - fn colonists_light_a_campfire_near_the_stockpile_when_night_falls() {
1508 - let mut app = hauling_app((IVec2::new(10, 10), IVec2::new(12, 12)));
1509 - put_wood(&mut app, UVec2::new(10, 10), 20);
1510 - set_hour(&mut app, 18);
1511 - spawn_colonist(&mut app, UVec2::new(2, 5));
1512 - run_ticks(&mut app, 60 * 20);
1513 - let built = structures(&mut app);
1514 - let campfire = built
1515 - .iter()
1516 - .find(|(_, kind)| *kind == StructureKind::Campfire)
1517 - .expect("a campfire");
1518 - assert!(campfire.0.as_vec2().distance(Vec2::new(11.0, 11.0)) <= HOME_RADIUS);
1519 - assert_eq!(wood_on_map(&mut app), 10);
1520 - }
1521 -
1522 - #[test]
1523 - fn no_fires_are_built_during_the_day() {
1524 - let mut app = hauling_app((IVec2::new(10, 10), IVec2::new(12, 12)));
1525 - put_wood(&mut app, UVec2::new(10, 10), 20);
1526 - set_hour(&mut app, 10);
1527 - spawn_colonist(&mut app, UVec2::new(2, 5));
1528 - run_ticks(&mut app, 60 * 10);
1529 - assert!(
1530 - structures(&mut app)
1531 - .iter()
1532 - .all(|(_, kind)| kind.light().is_none())
1533 - );
1534 - }
1535 -
1536 - #[test]
1537 - fn without_wood_nothing_can_be_built() {
1538 - let mut app = hauling_app((IVec2::new(10, 10), IVec2::new(12, 12)));
1539 - set_hour(&mut app, 22);
1540 - spawn_colonist(&mut app, UVec2::new(2, 5));
1541 - run_ticks(&mut app, 60 * 10);
1542 - assert!(
1543 - structures(&mut app)
1544 - .iter()
1545 - .all(|(_, kind)| kind.light().is_none())
1546 - );
1547 - }
1548 -
1549 - #[test]
1550 - fn torches_light_stockpile_tiles_beyond_the_campfire() {
1551 - let mut app = hauling_app((IVec2::new(2, 10), IVec2::new(17, 10)));
1552 - put_wood(&mut app, UVec2::new(9, 10), 20);
1553 - put_campfire(&mut app, UVec2::new(9, 12));
1554 - set_hour(&mut app, 22);
1555 - spawn_colonist(&mut app, UVec2::new(9, 8));
1556 - run_ticks(&mut app, 60 * 15);
1557 - assert!(
1558 - structures(&mut app)
1559 - .iter()
1560 - .any(|(_, kind)| *kind == StructureKind::Torch)
1561 - );
1562 - }
1563 -
1564 - // Light is what makes fires worth building: work in darkness goes at half speed.
1565 - #[test]
1566 - fn work_is_slower_in_the_dark() {
1567 - let tree = UVec2::new(8, 5);
1568 - let finish_ticks = |daylight: f32| {
1569 - let mut app = test_app(map_with_trees(&[tree]));
1570 - let size = app.world().resource::<WorldMap>().size;
1571 - app.insert_resource(LightMap::compute(size, daylight, []));
1572 - spawn_colonist(&mut app, UVec2::new(7, 5));
1573 - let mut ticks = 0;
1574 - while app
1575 - .world()
1576 - .resource::<WorldMap>()
1577 - .plant(tree.as_ivec2())
1578 - .is_some()
1579 - {
1580 - run_ticks(&mut app, 1);
1581 - ticks += 1;
1582 - }
1583 - ticks
1584 - };
1585 - let (lit, dark) = (finish_ticks(1.0), finish_ticks(0.0));
1586 - assert!(
1587 - dark > lit + WorkKind::Chop.work() / 2 - 10,
1588 - "lit {lit}, dark {dark}"
1589 - );
1590 - }
1591 -
1592 - const NAP_TEST_REST: f32 = 0.25;
1593 -
1594 - fn set_rest(app: &mut App, colonist: Entity, rest: f32) {
1595 - app.world_mut().get_mut::<Rest>(colonist).unwrap().0 = rest;
1596 - }
1597 -
1598 - fn rest(app: &App, colonist: Entity) -> f32 {
1599 - app.world().get::<Rest>(colonist).unwrap().0
1600 - }
1601 -
1602 - fn task(app: &App, colonist: Entity) -> Option<Task> {
1603 - job(app, colonist).map(|j| j.task)
1604 - }
1605 -
1606 - #[test]
1607 - fn tired_colonists_nap_and_get_up_when_rested() {
1608 - let mut app = test_app(map_with_trees(&[]));
1609 - set_goals(&mut app, &[]);
1610 - set_hour(&mut app, 10);
1611 - let colonist = spawn_colonist(&mut app, UVec2::new(5, 5));
1612 - set_rest(&mut app, colonist, NAP_TEST_REST);
1613 - run_ticks(&mut app, 61);
1614 - assert!(matches!(task(&app, colonist), Some(Task::Sleep { .. })));
1615 - let mut ticks = 0;
1616 - while matches!(task(&app, colonist), Some(Task::Sleep { .. })) && ticks < 60 * 60 * 10 {
1617 - run_ticks(&mut app, 60);
1618 - ticks += 60;
1619 - }
1620 - assert!(!matches!(task(&app, colonist), Some(Task::Sleep { .. })));
1621 - assert!(rest(&app, colonist) > 0.95);
1622 - }
1623 -
1624 - #[test]
1625 - fn colonists_go_to_bed_at_night_and_sleep_until_morning() {
1626 - let mut app = test_app(map_with_trees(&[]));
1627 - set_goals(&mut app, &[]);
1628 - set_hour(&mut app, 22);
1629 - let colonist = spawn_colonist(&mut app, UVec2::new(5, 5));
1630 - set_rest(&mut app, colonist, 0.8);
1631 - run_ticks(&mut app, 61);
1632 - assert!(matches!(task(&app, colonist), Some(Task::Sleep { .. })));
1633 - run_ticks(&mut app, 3 * crate::sim::TICKS_PER_HOUR as u32);
1634 - assert_eq!(rest(&app, colonist), 1.0);
1635 - assert!(matches!(task(&app, colonist), Some(Task::Sleep { .. })));
1636 - }
1637 -
1638 - #[test]
1639 - fn rested_colonists_stay_up_during_the_day() {
1640 - let mut app = test_app(map_with_trees(&[]));
1641 - set_goals(&mut app, &[]);
1642 - set_hour(&mut app, 12);
1643 - let colonist = spawn_colonist(&mut app, UVec2::new(5, 5));
1644 - set_rest(&mut app, colonist, 0.8);
1645 - run_ticks(&mut app, 60 * 5);
1646 - assert!(!matches!(task(&app, colonist), Some(Task::Sleep { .. })));
1647 - }
1648 -
1649 - #[test]
1650 - fn exhausted_colonists_drop_their_work_to_sleep() {
1651 - let tree = UVec2::new(15, 5);
1652 - let mut app = test_app(map_with_trees(&[tree]));
1653 - let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
1654 - run_ticks(&mut app, 61);
1655 - assert!(matches!(task(&app, colonist), Some(Task::Work { .. })));
1656 - set_rest(&mut app, colonist, EXHAUSTED_REST / 2.0);
1657 - run_ticks(&mut app, 30);
1658 - assert!(matches!(task(&app, colonist), Some(Task::Sleep { .. })));
1659 - assert!(
1660 - app.world()
1661 - .resource::<Reservations>()
1662 - .is_free(Target::Site(tree))
1663 - );
1664 - }
1665 -
1666 - #[test]
1667 - fn exhaustion_does_not_override_orders() {
1668 - let tree = UVec2::new(15, 5);
1669 - let mut app = test_app(map_with_trees(&[tree]));
1670 - let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
1671 - app.world_mut().write_message(Order {
1672 - colonist,
1673 - tile: tree,
1674 - });
1675 - run_ticks(&mut app, 2);
1676 - set_rest(&mut app, colonist, EXHAUSTED_REST / 2.0);
1677 - run_ticks(&mut app, 30);
1678 - assert!(matches!(
1679 - job(&app, colonist),
1680 - Some(Job {
1681 - task: Task::Work { .. },
1682 - ordered: true
1683 - })
1684 - ));
1685 - }
1686 -
1687 - fn beds(app: &mut App) -> usize {
1688 - structures(app)
1689 - .iter()
1690 - .filter(|(_, kind)| *kind == StructureKind::Bed)
1691 - .count()
1692 - }
1693 -
1694 - #[test]
1695 - fn colonists_build_one_bed_each() {
1696 - let mut app = test_app(map_with_trees(&[]));
1697 - set_goals(&mut app, &[]);
1698 - let map = app.world().resource::<WorldMap>().clone();
1699 - app.world_mut().resource_mut::<Zones>().add_stockpile(
1700 - &map,
1701 - IVec2::new(2, 2),
1702 - IVec2::new(5, 5),
1703 - );
1704 - put_wood(&mut app, UVec2::new(2, 2), 50);
1705 - put_wood(&mut app, UVec2::new(3, 2), 50);
1706 - spawn_colonist(&mut app, UVec2::new(10, 10));
1707 - spawn_colonist(&mut app, UVec2::new(11, 10));
1708 - run_ticks(&mut app, 60 * 40);
1709 - assert_eq!(beds(&mut app), 2);
1710 - assert_eq!(wood_on_map(&mut app), 100 - 2 * StructureKind::Bed.cost().1);
1711 - }
1712 -
1713 - // A shelter needs a wall ring with walking space around the beds; beds hugging the stockpile
1714 - // would leave stored items where the walls must go and the room could never be planned.
1715 - #[test]
1716 - fn beds_keep_clear_of_the_stockpile() {
1717 - let mut app = test_app(map_with_trees(&[]));
1718 - set_goals(&mut app, &[]);
1719 - let map = app.world().resource::<WorldMap>().clone();
1720 - app.world_mut().resource_mut::<Zones>().add_stockpile(
1721 - &map,
1722 - IVec2::new(8, 8),
1723 - IVec2::new(11, 11),
1724 - );
1725 - put_wood(&mut app, UVec2::new(8, 8), 50);
1726 - spawn_colonist(&mut app, UVec2::new(10, 4));
1727 - run_ticks(&mut app, 60 * 20);
1728 - let built = structures(&mut app);
1729 - let bed = built
1730 - .iter()
1731 - .find(|(_, kind)| *kind == StructureKind::Bed)
1732 - .expect("a bed");
1733 - let zones = app.world().resource::<Zones>();
1734 - assert!(zones.stockpile_tiles().all(|s| {
1735 - (s.as_ivec2() - bed.0.as_ivec2()).abs().max_element() >= BED_STOCKPILE_CLEARANCE
1736 - }));
1737 - }
1738 -
1739 - #[test]
1740 - fn colonists_sleep_in_their_beds() {
1741 - let mut app = test_app(map_with_trees(&[]));
1742 - set_goals(&mut app, &[]);
1743 - put_beds(&mut app, 1);
1744 - set_hour(&mut app, 22);
1745 - let colonist = spawn_colonist(&mut app, UVec2::new(5, 5));
1746 - set_rest(&mut app, colonist, 0.5);
1747 - run_ticks(&mut app, 60 * 10);
1748 - assert_eq!(
1749 - app.world().get::<TilePos>(colonist).unwrap().0,
1750 - UVec2::new(19, 19)
1751 - );
1752 - assert!(matches!(task(&app, colonist), Some(Task::Sleep { .. })));
1753 - }
1754 -
1755 - fn put_items(app: &mut App, tile: UVec2, kind: ItemKind, count: u32) {
1756 - app.world_mut().spawn((Item { kind, count }, TilePos(tile)));
1757 - }
1758 -
1759 - #[test]
1760 - fn blueprints_are_built_with_the_right_material() {
1761 - let mut app = test_app(map_with_trees(&[]));
1762 - set_goals(&mut app, &[]);
1763 - put_beds(&mut app, 1);
1764 - put_items(&mut app, UVec2::new(2, 2), ItemKind::Stone, 10);
1765 - let wall = UVec2::new(8, 8);
1766 - app.world_mut()
1767 - .resource_mut::<Blueprints>()
1768 - .0
1769 - .insert(wall, BuildKind::Structure(StructureKind::Wall));
1770 - spawn_colonist(&mut app, UVec2::new(5, 5));
1771 - run_ticks(&mut app, 60 * 10);
1772 - assert!(app.world().resource::<WorldMap>().is_wall(wall.as_ivec2()));
1773 - assert!(app.world().resource::<Blueprints>().0.is_empty());
1774 - assert_eq!(items_of(&mut app, ItemKind::Stone), 8);
1775 - }
1776 -
1777 - #[test]
1778 - fn colonists_build_a_roofed_shelter_around_their_beds() {
1779 - let mut app = test_app(map_with_trees(&[]));
1780 - app.add_systems(Update, crate::building::plan_shelter.before(choose_jobs));
1781 - set_goals(&mut app, &[]);
1782 - for x in [10, 11] {
1783 - app.world_mut().spawn((
1784 - Structure {
1785 - kind: StructureKind::Bed,
1786 - },
1787 - TilePos(UVec2::new(x, 10)),
1788 - ));
1789 - }
1790 - put_items(&mut app, UVec2::new(2, 2), ItemKind::Stone, 50);
1791 - put_items(&mut app, UVec2::new(3, 2), ItemKind::Wood, 50);
1792 - spawn_colonist(&mut app, UVec2::new(4, 4));
1793 - spawn_colonist(&mut app, UVec2::new(5, 4));
1794 - run_ticks(&mut app, 60 * 150);
1795 - let map = app.world().resource::<WorldMap>();
1796 - assert!(map.is_roofed(IVec2::new(10, 10)) && map.is_roofed(IVec2::new(11, 10)));
1797 - let walls = map
1798 - .positions()
1799 - .filter(|p| map.is_wall(p.as_ivec2()))
1800 - .count();
1801 - assert_eq!(walls, 17);
1802 - assert!(
1803 - structures(&mut app)
1804 - .iter()
1805 - .any(|(_, kind)| *kind == StructureKind::Door)
1806 - );
1807 - }
1808 -
1809 - #[test]
1810 - fn colonists_chop_from_a_neighbouring_tile() {
1811 - let tree = UVec2::new(8, 5);
1812 - let mut app = test_app(map_with_trees(&[tree]));
1813 - let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
1814 - run_ticks(&mut app, 60 * 5);
1815 - let tile = app.world().get::<TilePos>(colonist).unwrap().0;
1816 - assert!(is_adjacent(tile.as_ivec2(), tree.as_ivec2()));
1817 - assert_eq!(
1818 - app.world().get::<SimPos>(colonist).unwrap().0,
1819 - WorldMap::tile_center(tile)
1820 - );
1821 - }
1822 -}
1041 +mod tests;

src/jobs/tests.rs +777 -0

@@ -0,0 +1,777 @@
1 +use super::*;
2 +use crate::{
3 + colonist::follow_path,
4 + items::STACK_LIMIT,
5 + sim::{SimPos, sim_pos},
6 + world::Terrain,
7 + zones::NEW_STOCKPILE_SIZE,
8 +};
9 +
10 +const TICK: Duration = Duration::from_nanos(1_000_000_000 / 60);
11 +
12 +fn test_app(map: WorldMap) -> App {
13 + let mut app = App::new();
14 + app.insert_resource(map)
15 + .insert_resource(SimRng::new(1))
16 + .init_resource::<Time>()
17 + .init_resource::<Reservations>()
18 + .init_resource::<ColonyGoals>()
19 + .init_resource::<ItemAssets>()
20 + .init_resource::<StructureAssets>()
21 + .init_resource::<Blueprints>()
22 + .init_resource::<LightMap>()
23 + .init_resource::<SimTick>()
24 + .init_resource::<Zones>()
25 + .add_message::<Order>()
26 + .add_message::<TileChanged>()
27 + .add_systems(
28 + Update,
29 + (
30 + handle_orders,
31 + choose_jobs,
32 + run_jobs,
33 + follow_path,
34 + crate::needs::update_rest,
35 + )
36 + .chain(),
37 + );
38 + app
39 +}
40 +
41 +fn spawn_colonist(app: &mut App, tile: UVec2) -> Entity {
42 + app.world_mut()
43 + .spawn((
44 + Colonist {
45 + name: "Test".into(),
46 + },
47 + TilePos(tile),
48 + sim_pos(WorldMap::tile_center(tile)),
49 + ))
50 + .id()
51 +}
52 +
53 +fn run_ticks(app: &mut App, ticks: u32) {
54 + for _ in 0..ticks {
55 + app.world_mut().resource_mut::<Time>().advance_by(TICK);
56 + app.update();
57 + }
58 +}
59 +
60 +fn map_with_trees(trees: &[UVec2]) -> WorldMap {
61 + let mut map = WorldMap::filled(UVec2::new(20, 20), Terrain::Grass);
62 + for &t in trees {
63 + map.set_plant(t, Some(Plant::Tree));
64 + }
65 + map
66 +}
67 +
68 +fn wood_on_map(app: &mut App) -> u32 {
69 + let world = app.world_mut();
70 + let items: Vec<Item> = world.query::<&Item>().iter(world).copied().collect();
71 + stock(items.iter(), ItemKind::Wood)
72 +}
73 +
74 +fn items_at(app: &mut App) -> Vec<(UVec2, u32)> {
75 + let world = app.world_mut();
76 + let mut items: Vec<(UVec2, u32)> = world
77 + .query::<(&Item, &TilePos)>()
78 + .iter(world)
79 + .map(|(item, pos)| (pos.0, item.count))
80 + .collect();
81 + items.sort_by_key(|(p, _)| (p.y, p.x));
82 + items
83 +}
84 +
85 +fn put_wood(app: &mut App, tile: UVec2, count: u32) {
86 + app.world_mut().spawn((
87 + Item {
88 + kind: ItemKind::Wood,
89 + count,
90 + },
91 + TilePos(tile),
92 + ));
93 +}
94 +
95 +fn set_goals(app: &mut App, goals: &[(ItemKind, u32)]) {
96 + *app.world_mut().resource_mut::<ColonyGoals>() = ColonyGoals(goals.iter().copied().collect());
97 +}
98 +
99 +fn stockpile_size(app: &App) -> usize {
100 + let map = app.world().resource::<WorldMap>();
101 + let zones = app.world().resource::<Zones>();
102 + map.positions().filter(|&p| zones.is_stockpile(p)).count()
103 +}
104 +
105 +fn stored(app: &mut App) -> u32 {
106 + let zones = app.world().resource::<Zones>().clone();
107 + items_at(app)
108 + .iter()
109 + .filter(|(p, _)| zones.is_stockpile(*p))
110 + .map(|(_, c)| c)
111 + .sum()
112 +}
113 +
114 +fn put_beds(app: &mut App, count: u32) {
115 + for x in 0..count {
116 + app.world_mut().spawn((
117 + Structure {
118 + kind: StructureKind::Bed,
119 + },
120 + TilePos(UVec2::new(19 - x, 19)),
121 + ));
122 + }
123 +}
124 +
125 +fn hauling_app(stockpile: (IVec2, IVec2)) -> App {
126 + let mut app = test_app(map_with_trees(&[]));
127 + put_beds(&mut app, 2);
128 + set_goals(&mut app, &[]);
129 + let map = app.world().resource::<WorldMap>().clone();
130 + app.world_mut()
131 + .resource_mut::<Zones>()
132 + .add_stockpile(&map, stockpile.0, stockpile.1);
133 + app
134 +}
135 +
136 +fn carrying(app: &App, colonist: Entity) -> Option<Item> {
137 + app.world().get::<Carrying>(colonist).unwrap().0
138 +}
139 +
140 +fn job(app: &App, colonist: Entity) -> Option<Job> {
141 + app.world().get::<CurrentJob>(colonist).unwrap().0
142 +}
143 +
144 +#[test]
145 +fn idle_colonists_cut_trees_when_the_colony_needs_wood() {
146 + let tree = UVec2::new(8, 5);
147 + let mut app = test_app(map_with_trees(&[tree]));
148 + spawn_colonist(&mut app, UVec2::new(2, 5));
149 + run_ticks(&mut app, 60 * 15);
150 + assert_eq!(
151 + app.world().resource::<WorldMap>().plant(tree.as_ivec2()),
152 + None
153 + );
154 + assert_eq!(wood_on_map(&mut app), 10);
155 + assert!(app.world().resource::<Reservations>().0.is_empty());
156 +}
157 +
158 +#[test]
159 +fn colonists_leave_trees_alone_when_there_is_enough_wood() {
160 + let tree = UVec2::new(8, 5);
161 + let mut app = test_app(map_with_trees(&[tree]));
162 + set_goals(&mut app, &[]);
163 + spawn_colonist(&mut app, UVec2::new(2, 5));
164 + run_ticks(&mut app, 60 * 15);
165 + assert_eq!(
166 + app.world().resource::<WorldMap>().plant(tree.as_ivec2()),
167 + Some(Plant::Tree)
168 + );
169 +}
170 +
171 +#[test]
172 +fn colonists_never_pick_the_same_tree() {
173 + let trees = [UVec2::new(10, 10), UVec2::new(10, 11)];
174 + let mut app = test_app(map_with_trees(&trees));
175 + let a = spawn_colonist(&mut app, UVec2::new(5, 10));
176 + let b = spawn_colonist(&mut app, UVec2::new(5, 11));
177 + run_ticks(&mut app, 61);
178 + let target = |c| match job(&app, c) {
179 + Some(Job {
180 + task: Task::Work { site, .. },
181 + ..
182 + }) => site,
183 + other => panic!("expected chop job, got {other:?}"),
184 + };
185 + assert_ne!(target(a), target(b));
186 +}
187 +
188 +// Each tree or rock being worked will add its yield; counting that as material on the way
189 +// keeps the whole colony from felling a forest when only a little more wood is needed.
190 +#[test]
191 +fn work_in_progress_counts_towards_goals() {
192 + let goals = ColonyGoals(HashMap::from([(ItemKind::Wood, 25)]));
193 + assert!(goals.wanted(ItemKind::Wood, 10, 10));
194 + assert!(!goals.wanted(ItemKind::Wood, 10, 20));
195 + assert!(!goals.wanted(ItemKind::Wood, 30, 0));
196 + assert!(!goals.wanted(ItemKind::Gold, 0, 0));
197 +}
198 +
199 +#[test]
200 +fn ordered_chop_ignores_the_wood_goal() {
201 + let tree = UVec2::new(8, 5);
202 + let mut app = test_app(map_with_trees(&[tree]));
203 + set_goals(&mut app, &[]);
204 + let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
205 + app.world_mut().write_message(Order {
206 + colonist,
207 + tile: tree,
208 + });
209 + run_ticks(&mut app, 60 * 15);
210 + assert_eq!(
211 + app.world().resource::<WorldMap>().plant(tree.as_ivec2()),
212 + None
213 + );
214 +}
215 +
216 +#[test]
217 +fn ordering_a_move_cancels_the_current_job_and_frees_its_tree() {
218 + let tree = UVec2::new(15, 5);
219 + let mut app = test_app(map_with_trees(&[tree]));
220 + let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
221 + run_ticks(&mut app, 61);
222 + assert!(matches!(
223 + job(&app, colonist),
224 + Some(Job {
225 + task: Task::Work { .. },
226 + ..
227 + })
228 + ));
229 + app.world_mut().write_message(Order {
230 + colonist,
231 + tile: UVec2::new(2, 15),
232 + });
233 + run_ticks(&mut app, 1);
234 + assert_eq!(
235 + job(&app, colonist).map(|j| j.task),
236 + Some(Task::MoveTo(UVec2::new(2, 15)))
237 + );
238 + assert!(
239 + app.world()
240 + .resource::<Reservations>()
241 + .is_free(Target::Site(tree))
242 + );
243 +}
244 +
245 +#[test]
246 +fn idle_colonists_haul_loose_items_into_the_stockpile() {
247 + let mut app = hauling_app((IVec2::new(15, 15), IVec2::new(17, 15)));
248 + put_wood(&mut app, UVec2::new(5, 5), 20);
249 + spawn_colonist(&mut app, UVec2::new(2, 5));
250 + run_ticks(&mut app, 60 * 15);
251 + assert_eq!(items_at(&mut app), vec![(UVec2::new(15, 15), 20)]);
252 +}
253 +
254 +#[test]
255 +fn large_stacks_take_several_trips_and_spill_into_more_stockpile_tiles() {
256 + let mut app = hauling_app((IVec2::new(15, 15), IVec2::new(17, 15)));
257 + put_wood(&mut app, UVec2::new(5, 5), STACK_LIMIT + 10);
258 + spawn_colonist(&mut app, UVec2::new(2, 5));
259 + run_ticks(&mut app, 60 * 40);
260 + let stacks = items_at(&mut app);
261 + assert_eq!(stored(&mut app), STACK_LIMIT + 10);
262 + assert!(stacks.len() >= 2);
263 + assert!(stacks.iter().all(|(_, count)| *count <= STACK_LIMIT));
264 +}
265 +
266 +#[test]
267 +fn items_already_in_a_stockpile_stay_put() {
268 + let mut app = hauling_app((IVec2::new(5, 5), IVec2::new(16, 16)));
269 + put_wood(&mut app, UVec2::new(5, 5), 20);
270 + let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
271 + run_ticks(&mut app, 60 * 5);
272 + assert_eq!(items_at(&mut app), vec![(UVec2::new(5, 5), 20)]);
273 + assert_eq!(carrying(&app, colonist), None);
274 +}
275 +
276 +#[test]
277 +fn colonists_found_a_stockpile_when_items_need_storing() {
278 + let mut app = test_app(map_with_trees(&[]));
279 + put_beds(&mut app, 1);
280 + set_goals(&mut app, &[]);
281 + put_wood(&mut app, UVec2::new(5, 5), 20);
282 + spawn_colonist(&mut app, UVec2::new(2, 5));
283 + run_ticks(&mut app, 60 * 15);
284 + assert_eq!(
285 + stockpile_size(&app),
286 + (NEW_STOCKPILE_SIZE * NEW_STOCKPILE_SIZE) as usize
287 + );
288 + assert_eq!(stored(&mut app), 20);
289 +}
290 +
291 +#[test]
292 +fn no_stockpile_is_founded_while_there_is_nothing_to_store() {
293 + let mut app = test_app(map_with_trees(&[]));
294 + set_goals(&mut app, &[]);
295 + spawn_colonist(&mut app, UVec2::new(2, 5));
296 + run_ticks(&mut app, 60 * 5);
297 + assert_eq!(stockpile_size(&app), 0);
298 +}
299 +
300 +#[test]
301 +fn two_colonists_never_haul_the_same_stack() {
302 + let mut app = hauling_app((IVec2::new(15, 15), IVec2::new(16, 16)));
303 + put_wood(&mut app, UVec2::new(5, 5), 10);
304 + put_wood(&mut app, UVec2::new(5, 6), 10);
305 + let a = spawn_colonist(&mut app, UVec2::new(2, 5));
306 + let b = spawn_colonist(&mut app, UVec2::new(2, 6));
307 + run_ticks(&mut app, 61);
308 + let pickup = |c| match job(&app, c).map(|j| j.task) {
309 + Some(Task::Haul { pickup, .. }) => pickup,
310 + other => panic!("expected haul, got {other:?}"),
311 + };
312 + assert_ne!(pickup(a), pickup(b));
313 +}
314 +
315 +// Orders interrupt whatever a colonist is doing, but whatever it is carrying stays in its
316 +// hands; once it is idle again it finishes delivering it instead of dropping it anywhere.
317 +#[test]
318 +fn interrupted_haulers_deliver_their_load_later() {
319 + let mut app = hauling_app((IVec2::new(15, 15), IVec2::new(17, 15)));
320 + put_wood(&mut app, UVec2::new(4, 5), 20);
321 + let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
322 + let mut ticks = 0;
323 + while carrying(&app, colonist).is_none() && ticks < 60 * 10 {
324 + run_ticks(&mut app, 1);
325 + ticks += 1;
326 + }
327 + app.world_mut().write_message(Order {
328 + colonist,
329 + tile: UVec2::new(2, 2),
330 + });
331 + run_ticks(&mut app, 60 * 3);
332 + assert!(carrying(&app, colonist).is_some());
333 + run_ticks(&mut app, 60 * 20);
334 + assert_eq!(carrying(&app, colonist), None);
335 + assert_eq!(stored(&mut app), 20);
336 +}
337 +
338 +#[test]
339 +fn right_clicking_loose_items_orders_a_haul() {
340 + let mut app = hauling_app((IVec2::new(15, 15), IVec2::new(15, 15)));
341 + set_goals(&mut app, &[(ItemKind::Wood, 1000)]);
342 + put_wood(&mut app, UVec2::new(5, 5), 20);
343 + let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
344 + app.world_mut().write_message(Order {
345 + colonist,
346 + tile: UVec2::new(5, 5),
347 + });
348 + run_ticks(&mut app, 1);
349 + assert!(matches!(
350 + job(&app, colonist),
351 + Some(Job {
352 + task: Task::Haul { .. },
353 + ordered: true
354 + })
355 + ));
356 +}
357 +
358 +fn mining_map() -> WorldMap {
359 + let mut map = WorldMap::filled(UVec2::new(20, 20), Terrain::Grass);
360 + for y in 0..20 {
361 + map.set(UVec2::new(10, y), Terrain::Stone);
362 + }
363 + map.set(UVec2::new(10, 5), Terrain::IronOre);
364 + map
365 +}
366 +
367 +fn items_of(app: &mut App, kind: ItemKind) -> u32 {
368 + let world = app.world_mut();
369 + let items: Vec<Item> = world.query::<&Item>().iter(world).copied().collect();
370 + stock(items.iter(), kind)
371 +}
372 +
373 +#[test]
374 +fn colonists_mine_rock_the_colony_needs_and_leave_gravel() {
375 + let mut app = test_app(mining_map());
376 + set_goals(&mut app, &[(ItemKind::Stone, 10)]);
377 + spawn_colonist(&mut app, UVec2::new(8, 12));
378 + run_ticks(&mut app, 60 * 12);
379 + assert_eq!(items_of(&mut app, ItemKind::Stone), 10);
380 + assert_eq!(items_of(&mut app, ItemKind::Iron), 0);
381 + let map = app.world().resource::<WorldMap>();
382 + let gravel = map
383 + .positions()
384 + .filter(|p| map.get(p.as_ivec2()) == Some(Terrain::Gravel))
385 + .count();
386 + assert_eq!(gravel, 1);
387 +}
388 +
389 +#[test]
390 +fn colonists_walk_past_rock_they_do_not_need() {
391 + let mut app = test_app(mining_map());
392 + set_goals(&mut app, &[(ItemKind::Iron, 10)]);
393 + spawn_colonist(&mut app, UVec2::new(8, 15));
394 + run_ticks(&mut app, 60 * 15);
395 + assert_eq!(items_of(&mut app, ItemKind::Iron), 10);
396 + assert_eq!(items_of(&mut app, ItemKind::Stone), 0);
397 + assert_eq!(
398 + app.world().resource::<WorldMap>().get(IVec2::new(10, 5)),
399 + Some(Terrain::Gravel)
400 + );
401 +}
402 +
403 +#[test]
404 +fn right_clicking_rock_orders_mining_regardless_of_goals() {
405 + let mut app = test_app(mining_map());
406 + set_goals(&mut app, &[]);
407 + let colonist = spawn_colonist(&mut app, UVec2::new(8, 12));
408 + app.world_mut().write_message(Order {
409 + colonist,
410 + tile: UVec2::new(10, 12),
411 + });
412 + run_ticks(&mut app, 60 * 10);
413 + assert_eq!(
414 + app.world().resource::<WorldMap>().get(IVec2::new(10, 12)),
415 + Some(Terrain::Gravel)
416 + );
417 +}
418 +
419 +#[test]
420 +fn colonists_expand_a_stockpile_that_is_nearly_full() {
421 + let mut app = hauling_app((IVec2::new(5, 5), IVec2::new(6, 5)));
422 + let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
423 + run_ticks(&mut app, 61);
424 + assert!(matches!(
425 + job(&app, colonist).map(|j| j.task),
426 + Some(Task::ExpandStockpile { .. })
427 + ));
428 + run_ticks(&mut app, 60 * 5);
429 + assert_eq!(stockpile_size(&app), 2 + EXPANSION_TILES);
430 +}
431 +
432 +#[test]
433 +fn roomy_stockpiles_are_left_alone() {
434 + let mut app = hauling_app((IVec2::new(5, 5), IVec2::new(8, 5)));
435 + spawn_colonist(&mut app, UVec2::new(2, 5));
436 + run_ticks(&mut app, 60 * 5);
437 + assert_eq!(stockpile_size(&app), 4);
438 +}
439 +
440 +fn set_hour(app: &mut App, hour: u64) {
441 + let ticks = (hour + 24 - 6) % 24 * crate::sim::TICKS_PER_HOUR;
442 + *app.world_mut().resource_mut::<SimTick>() = SimTick(ticks);
443 +}
444 +
445 +fn structures(app: &mut App) -> Vec<(UVec2, StructureKind)> {
446 + let world = app.world_mut();
447 + world
448 + .query::<(&TilePos, &Structure)>()
449 + .iter(world)
450 + .map(|(pos, s)| (pos.0, s.kind))
451 + .collect()
452 +}
453 +
454 +fn put_campfire(app: &mut App, tile: UVec2) {
455 + app.world_mut().spawn((
456 + Structure {
457 + kind: StructureKind::Campfire,
458 + },
459 + Fuel(StructureKind::Campfire.fuel_ticks().unwrap()),
460 + TilePos(tile),
461 + ));
462 +}
463 +
464 +#[test]
465 +fn colonists_light_a_campfire_near_the_stockpile_when_night_falls() {
466 + let mut app = hauling_app((IVec2::new(10, 10), IVec2::new(12, 12)));
467 + put_wood(&mut app, UVec2::new(10, 10), 20);
468 + set_hour(&mut app, 18);
469 + spawn_colonist(&mut app, UVec2::new(2, 5));
470 + run_ticks(&mut app, 60 * 20);
471 + let built = structures(&mut app);
472 + let campfire = built
473 + .iter()
474 + .find(|(_, kind)| *kind == StructureKind::Campfire)
475 + .expect("a campfire");
476 + assert!(campfire.0.as_vec2().distance(Vec2::new(11.0, 11.0)) <= HOME_RADIUS);
477 + assert_eq!(wood_on_map(&mut app), 10);
478 +}
479 +
480 +#[test]
481 +fn no_fires_are_built_during_the_day() {
482 + let mut app = hauling_app((IVec2::new(10, 10), IVec2::new(12, 12)));
483 + put_wood(&mut app, UVec2::new(10, 10), 20);
484 + set_hour(&mut app, 10);
485 + spawn_colonist(&mut app, UVec2::new(2, 5));
486 + run_ticks(&mut app, 60 * 10);
487 + assert!(
488 + structures(&mut app)
489 + .iter()
490 + .all(|(_, kind)| kind.light().is_none())
491 + );
492 +}
493 +
494 +#[test]
495 +fn without_wood_nothing_can_be_built() {
496 + let mut app = hauling_app((IVec2::new(10, 10), IVec2::new(12, 12)));
497 + set_hour(&mut app, 22);
498 + spawn_colonist(&mut app, UVec2::new(2, 5));
499 + run_ticks(&mut app, 60 * 10);
500 + assert!(
501 + structures(&mut app)
502 + .iter()
503 + .all(|(_, kind)| kind.light().is_none())
504 + );
505 +}
506 +
507 +#[test]
508 +fn torches_light_stockpile_tiles_beyond_the_campfire() {
509 + let mut app = hauling_app((IVec2::new(2, 10), IVec2::new(17, 10)));
510 + put_wood(&mut app, UVec2::new(9, 10), 20);
511 + put_campfire(&mut app, UVec2::new(9, 12));
512 + set_hour(&mut app, 22);
513 + spawn_colonist(&mut app, UVec2::new(9, 8));
514 + run_ticks(&mut app, 60 * 15);
515 + assert!(
516 + structures(&mut app)
517 + .iter()
518 + .any(|(_, kind)| *kind == StructureKind::Torch)
519 + );
520 +}
521 +
522 +// Light is what makes fires worth building: work in darkness goes at half speed.
523 +#[test]
524 +fn work_is_slower_in_the_dark() {
525 + let tree = UVec2::new(8, 5);
526 + let finish_ticks = |daylight: f32| {
527 + let mut app = test_app(map_with_trees(&[tree]));
528 + let size = app.world().resource::<WorldMap>().size;
529 + app.insert_resource(LightMap::compute(size, daylight, []));
530 + spawn_colonist(&mut app, UVec2::new(7, 5));
531 + let mut ticks = 0;
532 + while app
533 + .world()
534 + .resource::<WorldMap>()
535 + .plant(tree.as_ivec2())
536 + .is_some()
537 + {
538 + run_ticks(&mut app, 1);
539 + ticks += 1;
540 + }
541 + ticks
542 + };
543 + let (lit, dark) = (finish_ticks(1.0), finish_ticks(0.0));
544 + assert!(
545 + dark > lit + WorkKind::Chop.work() / 2 - 10,
546 + "lit {lit}, dark {dark}"
547 + );
548 +}
549 +
550 +const NAP_TEST_REST: f32 = 0.25;
551 +
552 +fn set_rest(app: &mut App, colonist: Entity, rest: f32) {
553 + app.world_mut().get_mut::<Rest>(colonist).unwrap().0 = rest;
554 +}
555 +
556 +fn rest(app: &App, colonist: Entity) -> f32 {
557 + app.world().get::<Rest>(colonist).unwrap().0
558 +}
559 +
560 +fn task(app: &App, colonist: Entity) -> Option<Task> {
561 + job(app, colonist).map(|j| j.task)
562 +}
563 +
564 +#[test]
565 +fn tired_colonists_nap_and_get_up_when_rested() {
566 + let mut app = test_app(map_with_trees(&[]));
567 + set_goals(&mut app, &[]);
568 + set_hour(&mut app, 10);
569 + let colonist = spawn_colonist(&mut app, UVec2::new(5, 5));
570 + set_rest(&mut app, colonist, NAP_TEST_REST);
571 + run_ticks(&mut app, 61);
572 + assert!(matches!(task(&app, colonist), Some(Task::Sleep { .. })));
573 + let mut ticks = 0;
574 + while matches!(task(&app, colonist), Some(Task::Sleep { .. })) && ticks < 60 * 60 * 10 {
575 + run_ticks(&mut app, 60);
576 + ticks += 60;
577 + }
578 + assert!(!matches!(task(&app, colonist), Some(Task::Sleep { .. })));
579 + assert!(rest(&app, colonist) > 0.95);
580 +}
581 +
582 +#[test]
583 +fn colonists_go_to_bed_at_night_and_sleep_until_morning() {
584 + let mut app = test_app(map_with_trees(&[]));
585 + set_goals(&mut app, &[]);
586 + set_hour(&mut app, 22);
587 + let colonist = spawn_colonist(&mut app, UVec2::new(5, 5));
588 + set_rest(&mut app, colonist, 0.8);
589 + run_ticks(&mut app, 61);
590 + assert!(matches!(task(&app, colonist), Some(Task::Sleep { .. })));
591 + run_ticks(&mut app, 3 * crate::sim::TICKS_PER_HOUR as u32);
592 + assert_eq!(rest(&app, colonist), 1.0);
593 + assert!(matches!(task(&app, colonist), Some(Task::Sleep { .. })));
594 +}
595 +
596 +#[test]
597 +fn rested_colonists_stay_up_during_the_day() {
598 + let mut app = test_app(map_with_trees(&[]));
599 + set_goals(&mut app, &[]);
600 + set_hour(&mut app, 12);
601 + let colonist = spawn_colonist(&mut app, UVec2::new(5, 5));
602 + set_rest(&mut app, colonist, 0.8);
603 + run_ticks(&mut app, 60 * 5);
604 + assert!(!matches!(task(&app, colonist), Some(Task::Sleep { .. })));
605 +}
606 +
607 +#[test]
608 +fn exhausted_colonists_drop_their_work_to_sleep() {
609 + let tree = UVec2::new(15, 5);
610 + let mut app = test_app(map_with_trees(&[tree]));
611 + let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
612 + run_ticks(&mut app, 61);
613 + assert!(matches!(task(&app, colonist), Some(Task::Work { .. })));
614 + set_rest(&mut app, colonist, EXHAUSTED_REST / 2.0);
615 + run_ticks(&mut app, 30);
616 + assert!(matches!(task(&app, colonist), Some(Task::Sleep { .. })));
617 + assert!(
618 + app.world()
619 + .resource::<Reservations>()
620 + .is_free(Target::Site(tree))
621 + );
622 +}
623 +
624 +#[test]
625 +fn exhaustion_does_not_override_orders() {
626 + let tree = UVec2::new(15, 5);
627 + let mut app = test_app(map_with_trees(&[tree]));
628 + let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
629 + app.world_mut().write_message(Order {
630 + colonist,
631 + tile: tree,
632 + });
633 + run_ticks(&mut app, 2);
634 + set_rest(&mut app, colonist, EXHAUSTED_REST / 2.0);
635 + run_ticks(&mut app, 30);
636 + assert!(matches!(
637 + job(&app, colonist),
638 + Some(Job {
639 + task: Task::Work { .. },
640 + ordered: true
641 + })
642 + ));
643 +}
644 +
645 +fn beds(app: &mut App) -> usize {
646 + structures(app)
647 + .iter()
648 + .filter(|(_, kind)| *kind == StructureKind::Bed)
649 + .count()
650 +}
651 +
652 +#[test]
653 +fn colonists_build_one_bed_each() {
654 + let mut app = test_app(map_with_trees(&[]));
655 + set_goals(&mut app, &[]);
656 + let map = app.world().resource::<WorldMap>().clone();
657 + app.world_mut()
658 + .resource_mut::<Zones>()
659 + .add_stockpile(&map, IVec2::new(2, 2), IVec2::new(5, 5));
660 + put_wood(&mut app, UVec2::new(2, 2), 50);
661 + put_wood(&mut app, UVec2::new(3, 2), 50);
662 + spawn_colonist(&mut app, UVec2::new(10, 10));
663 + spawn_colonist(&mut app, UVec2::new(11, 10));
664 + run_ticks(&mut app, 60 * 40);
665 + assert_eq!(beds(&mut app), 2);
666 + assert_eq!(wood_on_map(&mut app), 100 - 2 * StructureKind::Bed.cost().1);
667 +}
668 +
669 +// A shelter needs a wall ring with walking space around the beds; beds hugging the stockpile
670 +// would leave stored items where the walls must go and the room could never be planned.
671 +#[test]
672 +fn beds_keep_clear_of_the_stockpile() {
673 + let mut app = test_app(map_with_trees(&[]));
674 + set_goals(&mut app, &[]);
675 + let map = app.world().resource::<WorldMap>().clone();
676 + app.world_mut().resource_mut::<Zones>().add_stockpile(
677 + &map,
678 + IVec2::new(8, 8),
679 + IVec2::new(11, 11),
680 + );
681 + put_wood(&mut app, UVec2::new(8, 8), 50);
682 + spawn_colonist(&mut app, UVec2::new(10, 4));
683 + run_ticks(&mut app, 60 * 20);
684 + let built = structures(&mut app);
685 + let bed = built
686 + .iter()
687 + .find(|(_, kind)| *kind == StructureKind::Bed)
688 + .expect("a bed");
689 + let zones = app.world().resource::<Zones>();
690 + assert!(zones.stockpile_tiles().all(|s| {
691 + (s.as_ivec2() - bed.0.as_ivec2()).abs().max_element() >= BED_STOCKPILE_CLEARANCE
692 + }));
693 +}
694 +
695 +#[test]
696 +fn colonists_sleep_in_their_beds() {
697 + let mut app = test_app(map_with_trees(&[]));
698 + set_goals(&mut app, &[]);
699 + put_beds(&mut app, 1);
700 + set_hour(&mut app, 22);
701 + let colonist = spawn_colonist(&mut app, UVec2::new(5, 5));
702 + set_rest(&mut app, colonist, 0.5);
703 + run_ticks(&mut app, 60 * 10);
704 + assert_eq!(
705 + app.world().get::<TilePos>(colonist).unwrap().0,
706 + UVec2::new(19, 19)
707 + );
708 + assert!(matches!(task(&app, colonist), Some(Task::Sleep { .. })));
709 +}
710 +
711 +fn put_items(app: &mut App, tile: UVec2, kind: ItemKind, count: u32) {
712 + app.world_mut().spawn((Item { kind, count }, TilePos(tile)));
713 +}
714 +
715 +#[test]
716 +fn blueprints_are_built_with_the_right_material() {
717 + let mut app = test_app(map_with_trees(&[]));
718 + set_goals(&mut app, &[]);
719 + put_beds(&mut app, 1);
720 + put_items(&mut app, UVec2::new(2, 2), ItemKind::Stone, 10);
721 + let wall = UVec2::new(8, 8);
722 + app.world_mut()
723 + .resource_mut::<Blueprints>()
724 + .0
725 + .insert(wall, BuildKind::Structure(StructureKind::Wall));
726 + spawn_colonist(&mut app, UVec2::new(5, 5));
727 + run_ticks(&mut app, 60 * 10);
728 + assert!(app.world().resource::<WorldMap>().is_wall(wall.as_ivec2()));
729 + assert!(app.world().resource::<Blueprints>().0.is_empty());
730 + assert_eq!(items_of(&mut app, ItemKind::Stone), 8);
731 +}
732 +
733 +#[test]
734 +fn colonists_build_a_roofed_shelter_around_their_beds() {
735 + let mut app = test_app(map_with_trees(&[]));
736 + app.add_systems(Update, crate::building::plan_shelter.before(choose_jobs));
737 + set_goals(&mut app, &[]);
738 + for x in [10, 11] {
739 + app.world_mut().spawn((
740 + Structure {
741 + kind: StructureKind::Bed,
742 + },
743 + TilePos(UVec2::new(x, 10)),
744 + ));
745 + }
746 + put_items(&mut app, UVec2::new(2, 2), ItemKind::Stone, 50);
747 + put_items(&mut app, UVec2::new(3, 2), ItemKind::Wood, 50);
748 + spawn_colonist(&mut app, UVec2::new(4, 4));
749 + spawn_colonist(&mut app, UVec2::new(5, 4));
750 + run_ticks(&mut app, 60 * 150);
751 + let map = app.world().resource::<WorldMap>();
752 + assert!(map.is_roofed(IVec2::new(10, 10)) && map.is_roofed(IVec2::new(11, 10)));
753 + let walls = map
754 + .positions()
755 + .filter(|p| map.is_wall(p.as_ivec2()))
756 + .count();
757 + assert_eq!(walls, 17);
758 + assert!(
759 + structures(&mut app)
760 + .iter()
761 + .any(|(_, kind)| *kind == StructureKind::Door)
762 + );
763 +}
764 +
765 +#[test]
766 +fn colonists_chop_from_a_neighbouring_tile() {
767 + let tree = UVec2::new(8, 5);
768 + let mut app = test_app(map_with_trees(&[tree]));
769 + let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
770 + run_ticks(&mut app, 60 * 5);
771 + let tile = app.world().get::<TilePos>(colonist).unwrap().0;
772 + assert!(is_adjacent(tile.as_ivec2(), tree.as_ivec2()));
773 + assert_eq!(
774 + app.world().get::<SimPos>(colonist).unwrap().0,
775 + WorldMap::tile_center(tile)
776 + );
777 +}