src/jobs/tests.rs 55.6 K · 1664 lines · raw · history

1 use std::collections::VecDeque;
2
3 use super::*;
4 use crate::{
5 animals::{Animal, AnimalKind},
6 colonist::follow_path,
7 economy::{COLONY_RADIUS, Stock},
8 food::Hunger,
9 items::{STACK_LIMIT, stock},
10 sim::{SimPos, sim_pos},
11 world::Terrain,
12 zones::NEW_STOCKPILE_SIZE,
13 };
14
15 const TICK: Duration = Duration::from_nanos(1_000_000_000 / 60);
16
17 fn test_app(map: WorldMap) -> App {
18 let mut app = App::new();
19 app.insert_resource(map)
20 .insert_resource(SimRng::new(1))
21 .init_resource::<Time>()
22 .init_resource::<Reservations>()
23 .init_resource::<crate::family::Homes>()
24 .init_resource::<crate::pathfinding::ColonyReach>()
25 .init_resource::<crate::rooms::Rooms>()
26 .add_message::<crate::mood::Ate>()
27 .init_resource::<crate::designations::Designations>()
28 .init_resource::<ColonyGoals>()
29 .init_resource::<ItemAssets>()
30 .init_resource::<StructureAssets>()
31 .init_resource::<Blueprints>()
32 .init_resource::<LightMap>()
33 .init_resource::<SimTick>()
34 .init_resource::<Zones>()
35 .add_message::<Order>()
36 .add_message::<TileChanged>()
37 .add_systems(
38 Update,
39 (
40 handle_orders,
41 choose_jobs,
42 run_jobs,
43 follow_path,
44 crate::needs::update_rest,
45 )
46 .chain(),
47 );
48 app
49 }
50
51 fn spawn_colonist(app: &mut App, tile: UVec2) -> Entity {
52 app.world_mut()
53 .spawn((
54 Colonist {
55 name: "Test".into(),
56 },
57 TilePos(tile),
58 sim_pos(WorldMap::tile_center(tile)),
59 ))
60 .id()
61 }
62
63 fn run_ticks(app: &mut App, ticks: u32) {
64 for _ in 0..ticks {
65 app.world_mut().resource_mut::<Time>().advance_by(TICK);
66 app.update();
67 }
68 }
69
70 fn map_with_trees(trees: &[UVec2]) -> WorldMap {
71 let mut map = WorldMap::filled(UVec2::new(20, 20), Terrain::Grass);
72 for &t in trees {
73 map.set_plant(t, Some(Plant::Tree));
74 }
75 map
76 }
77
78 fn wood_on_map(app: &mut App) -> u32 {
79 let world = app.world_mut();
80 let items: Vec<Item> = world.query::<&Item>().iter(world).copied().collect();
81 stock(items.iter(), ItemKind::Wood)
82 }
83
84 fn items_at(app: &mut App) -> Vec<(UVec2, u32)> {
85 let world = app.world_mut();
86 let mut items: Vec<(UVec2, u32)> = world
87 .query::<(&Item, &TilePos)>()
88 .iter(world)
89 .map(|(item, pos)| (pos.0, item.count))
90 .collect();
91 items.sort_by_key(|(p, _)| (p.y, p.x));
92 items
93 }
94
95 fn put_wood(app: &mut App, tile: UVec2, count: u32) {
96 app.world_mut()
97 .spawn((Item::new(ItemKind::Wood, count), TilePos(tile)));
98 }
99
100 fn set_goals(app: &mut App, goals: &[(ItemKind, u32)]) {
101 *app.world_mut().resource_mut::<ColonyGoals>() = ColonyGoals::fixed(goals);
102 }
103
104 fn stockpile_size(app: &App) -> usize {
105 let map = app.world().resource::<WorldMap>();
106 let zones = app.world().resource::<Zones>();
107 map.positions().filter(|&p| zones.is_stockpile(p)).count()
108 }
109
110 fn stored(app: &mut App) -> u32 {
111 let zones = app.world().resource::<Zones>().clone();
112 items_at(app)
113 .iter()
114 .filter(|(p, _)| zones.is_stockpile(*p))
115 .map(|(_, c)| c)
116 .sum()
117 }
118
119 fn put_beds(app: &mut App, count: u32) {
120 for x in 0..count {
121 app.world_mut().spawn((
122 Structure {
123 kind: StructureKind::Bed,
124 },
125 TilePos(UVec2::new(19 - x, 19)),
126 ));
127 }
128 }
129
130 fn hauling_app(stockpile: (IVec2, IVec2)) -> App {
131 let mut app = test_app(map_with_trees(&[]));
132 put_beds(&mut app, 2);
133 set_goals(&mut app, &[]);
134 let map = app.world().resource::<WorldMap>().clone();
135 app.world_mut()
136 .resource_mut::<Zones>()
137 .add_stockpile(&map, stockpile.0, stockpile.1);
138 app
139 }
140
141 fn carrying(app: &App, colonist: Entity) -> Option<Item> {
142 app.world().get::<Carrying>(colonist).unwrap().0
143 }
144
145 fn job(app: &App, colonist: Entity) -> Option<Job> {
146 app.world().get::<CurrentJob>(colonist).unwrap().0
147 }
148
149 #[test]
150 fn idle_colonists_cut_trees_when_the_colony_needs_wood() {
151 let tree = UVec2::new(8, 5);
152 let mut app = test_app(map_with_trees(&[tree]));
153 spawn_colonist(&mut app, UVec2::new(2, 5));
154 run_ticks(&mut app, 60 * 15);
155 assert_eq!(
156 app.world().resource::<WorldMap>().plant(tree.as_ivec2()),
157 None
158 );
159 assert_eq!(wood_on_map(&mut app), 10);
160 assert!(app.world().resource::<Reservations>().0.is_empty());
161 }
162
163 #[test]
164 fn colonists_leave_trees_alone_when_there_is_enough_wood() {
165 let tree = UVec2::new(8, 5);
166 let mut app = test_app(map_with_trees(&[tree]));
167 set_goals(&mut app, &[]);
168 spawn_colonist(&mut app, UVec2::new(2, 5));
169 run_ticks(&mut app, 60 * 15);
170 assert_eq!(
171 app.world().resource::<WorldMap>().plant(tree.as_ivec2()),
172 Some(Plant::Tree)
173 );
174 }
175
176 #[test]
177 fn colonists_never_pick_the_same_tree() {
178 let trees = [UVec2::new(10, 10), UVec2::new(10, 11)];
179 let mut app = test_app(map_with_trees(&trees));
180 let a = spawn_colonist(&mut app, UVec2::new(5, 10));
181 let b = spawn_colonist(&mut app, UVec2::new(5, 11));
182 run_ticks(&mut app, 61);
183 let target = |c| match job(&app, c) {
184 Some(Job {
185 task: Task::Work { site, .. },
186 ..
187 }) => site,
188 other => panic!("expected chop job, got {other:?}"),
189 };
190 assert_ne!(target(a), target(b));
191 }
192
193 #[test]
194 fn ordered_chop_ignores_the_wood_goal() {
195 let tree = UVec2::new(8, 5);
196 let mut app = test_app(map_with_trees(&[tree]));
197 set_goals(&mut app, &[]);
198 let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
199 app.world_mut().write_message(Order {
200 colonist,
201 tile: tree,
202 });
203 run_ticks(&mut app, 60 * 15);
204 assert_eq!(
205 app.world().resource::<WorldMap>().plant(tree.as_ivec2()),
206 None
207 );
208 }
209
210 #[test]
211 fn ordering_a_move_cancels_the_current_job_and_frees_its_tree() {
212 let tree = UVec2::new(15, 5);
213 let mut app = test_app(map_with_trees(&[tree]));
214 let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
215 run_ticks(&mut app, 61);
216 assert!(matches!(
217 job(&app, colonist),
218 Some(Job {
219 task: Task::Work { .. },
220 ..
221 })
222 ));
223 app.world_mut().write_message(Order {
224 colonist,
225 tile: UVec2::new(2, 15),
226 });
227 run_ticks(&mut app, 1);
228 assert_eq!(
229 job(&app, colonist).map(|j| j.task),
230 Some(Task::MoveTo(UVec2::new(2, 15)))
231 );
232 assert!(
233 app.world()
234 .resource::<Reservations>()
235 .is_free(Target::Site(tree))
236 );
237 }
238
239 #[test]
240 fn idle_colonists_haul_loose_items_into_the_stockpile() {
241 let mut app = hauling_app((IVec2::new(15, 15), IVec2::new(17, 15)));
242 put_wood(&mut app, UVec2::new(5, 5), 20);
243 spawn_colonist(&mut app, UVec2::new(2, 5));
244 run_ticks(&mut app, 60 * 15);
245 assert_eq!(items_at(&mut app), vec![(UVec2::new(15, 15), 20)]);
246 }
247
248 #[test]
249 fn large_stacks_take_several_trips_and_spill_into_more_stockpile_tiles() {
250 let mut app = hauling_app((IVec2::new(15, 15), IVec2::new(17, 15)));
251 put_wood(&mut app, UVec2::new(5, 5), STACK_LIMIT + 10);
252 spawn_colonist(&mut app, UVec2::new(2, 5));
253 run_ticks(&mut app, 60 * 40);
254 let stacks = items_at(&mut app);
255 assert_eq!(stored(&mut app), STACK_LIMIT + 10);
256 assert!(stacks.len() >= 2);
257 assert!(stacks.iter().all(|(_, count)| *count <= STACK_LIMIT));
258 }
259
260 #[test]
261 fn items_already_in_a_stockpile_stay_put() {
262 let mut app = hauling_app((IVec2::new(5, 5), IVec2::new(16, 16)));
263 put_wood(&mut app, UVec2::new(5, 5), 20);
264 let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
265 run_ticks(&mut app, 60 * 5);
266 assert_eq!(items_at(&mut app), vec![(UVec2::new(5, 5), 20)]);
267 assert_eq!(carrying(&app, colonist), None);
268 }
269
270 #[test]
271 fn colonists_found_a_stockpile_when_items_need_storing() {
272 let mut app = test_app(map_with_trees(&[]));
273 put_beds(&mut app, 1);
274 set_goals(&mut app, &[]);
275 put_wood(&mut app, UVec2::new(5, 5), 20);
276 spawn_colonist(&mut app, UVec2::new(2, 5));
277 run_ticks(&mut app, 60 * 15);
278 assert_eq!(
279 stockpile_size(&app),
280 (NEW_STOCKPILE_SIZE * NEW_STOCKPILE_SIZE) as usize
281 );
282 assert_eq!(stored(&mut app), 20);
283 }
284
285 #[test]
286 fn no_stockpile_is_founded_while_there_is_nothing_to_store() {
287 let mut app = test_app(map_with_trees(&[]));
288 set_goals(&mut app, &[]);
289 spawn_colonist(&mut app, UVec2::new(2, 5));
290 run_ticks(&mut app, 60 * 5);
291 assert_eq!(stockpile_size(&app), 0);
292 }
293
294 #[test]
295 fn two_colonists_never_haul_the_same_stack() {
296 let mut app = hauling_app((IVec2::new(15, 15), IVec2::new(16, 16)));
297 put_wood(&mut app, UVec2::new(5, 5), 10);
298 put_wood(&mut app, UVec2::new(5, 6), 10);
299 let a = spawn_colonist(&mut app, UVec2::new(2, 5));
300 let b = spawn_colonist(&mut app, UVec2::new(2, 6));
301 run_ticks(&mut app, 61);
302 let pickup = |c| match job(&app, c).map(|j| j.task) {
303 Some(Task::Haul { pickup, .. }) => pickup,
304 other => panic!("expected haul, got {other:?}"),
305 };
306 assert_ne!(pickup(a), pickup(b));
307 }
308
309 // Orders interrupt whatever a colonist is doing, but whatever it is carrying stays in its
310 // hands; once it is idle again it finishes delivering it instead of dropping it anywhere.
311 #[test]
312 fn interrupted_haulers_deliver_their_load_later() {
313 let mut app = hauling_app((IVec2::new(15, 15), IVec2::new(17, 15)));
314 put_wood(&mut app, UVec2::new(4, 5), 20);
315 let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
316 let mut ticks = 0;
317 while carrying(&app, colonist).is_none() && ticks < 60 * 10 {
318 run_ticks(&mut app, 1);
319 ticks += 1;
320 }
321 app.world_mut().write_message(Order {
322 colonist,
323 tile: UVec2::new(2, 2),
324 });
325 run_ticks(&mut app, 60 * 3);
326 assert!(carrying(&app, colonist).is_some());
327 run_ticks(&mut app, 60 * 20);
328 assert_eq!(carrying(&app, colonist), None);
329 assert_eq!(stored(&mut app), 20);
330 }
331
332 #[test]
333 fn right_clicking_loose_items_orders_a_haul() {
334 let mut app = hauling_app((IVec2::new(15, 15), IVec2::new(15, 15)));
335 set_goals(&mut app, &[(ItemKind::Wood, 1000)]);
336 put_wood(&mut app, UVec2::new(5, 5), 20);
337 let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
338 app.world_mut().write_message(Order {
339 colonist,
340 tile: UVec2::new(5, 5),
341 });
342 run_ticks(&mut app, 1);
343 assert!(matches!(
344 job(&app, colonist),
345 Some(Job {
346 task: Task::Haul { .. },
347 ordered: true
348 })
349 ));
350 }
351
352 fn mining_map() -> WorldMap {
353 let mut map = WorldMap::filled(UVec2::new(20, 20), Terrain::Grass);
354 for y in 0..20 {
355 map.set(UVec2::new(10, y), Terrain::Stone);
356 }
357 map.set(UVec2::new(10, 5), Terrain::IronOre);
358 map
359 }
360
361 fn items_of(app: &mut App, kind: ItemKind) -> u32 {
362 let world = app.world_mut();
363 let items: Vec<Item> = world.query::<&Item>().iter(world).copied().collect();
364 stock(items.iter(), kind)
365 }
366
367 #[test]
368 fn colonists_mine_rock_the_colony_needs_and_leave_gravel() {
369 let mut app = test_app(mining_map());
370 set_goals(&mut app, &[(ItemKind::Stone, 10)]);
371 spawn_colonist(&mut app, UVec2::new(8, 12));
372 run_ticks(&mut app, 60 * 12);
373 assert_eq!(items_of(&mut app, ItemKind::Stone), 10);
374 assert_eq!(items_of(&mut app, ItemKind::Iron), 0);
375 let map = app.world().resource::<WorldMap>();
376 let gravel = map
377 .positions()
378 .filter(|p| map.get(p.as_ivec2()) == Some(Terrain::Gravel))
379 .count();
380 assert_eq!(gravel, 1);
381 }
382
383 #[test]
384 fn colonists_walk_past_rock_they_do_not_need() {
385 let mut app = test_app(mining_map());
386 set_goals(&mut app, &[(ItemKind::Iron, 10)]);
387 spawn_colonist(&mut app, UVec2::new(8, 15));
388 run_ticks(&mut app, 60 * 15);
389 assert_eq!(items_of(&mut app, ItemKind::Iron), 10);
390 assert_eq!(items_of(&mut app, ItemKind::Stone), 0);
391 assert_eq!(
392 app.world().resource::<WorldMap>().get(IVec2::new(10, 5)),
393 Some(Terrain::Gravel)
394 );
395 }
396
397 #[test]
398 fn right_clicking_rock_orders_mining_regardless_of_goals() {
399 let mut app = test_app(mining_map());
400 set_goals(&mut app, &[]);
401 let colonist = spawn_colonist(&mut app, UVec2::new(8, 12));
402 app.world_mut().write_message(Order {
403 colonist,
404 tile: UVec2::new(10, 12),
405 });
406 run_ticks(&mut app, 60 * 10);
407 assert_eq!(
408 app.world().resource::<WorldMap>().get(IVec2::new(10, 12)),
409 Some(Terrain::Gravel)
410 );
411 }
412
413 #[test]
414 fn colonists_expand_a_stockpile_that_is_nearly_full() {
415 let mut app = hauling_app((IVec2::new(5, 5), IVec2::new(6, 5)));
416 let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
417 run_ticks(&mut app, 61);
418 assert!(matches!(
419 job(&app, colonist).map(|j| j.task),
420 Some(Task::ExpandStockpile { .. })
421 ));
422 run_ticks(&mut app, 60 * 5);
423 assert_eq!(stockpile_size(&app), 2 + EXPANSION_TILES);
424 }
425
426 #[test]
427 fn roomy_stockpiles_are_left_alone() {
428 let mut app = hauling_app((IVec2::new(5, 5), IVec2::new(8, 5)));
429 spawn_colonist(&mut app, UVec2::new(2, 5));
430 run_ticks(&mut app, 60 * 5);
431 assert_eq!(stockpile_size(&app), 4);
432 }
433
434 fn set_hour(app: &mut App, hour: u64) {
435 let ticks = (hour + 24 - 6) % 24 * crate::sim::TICKS_PER_HOUR;
436 *app.world_mut().resource_mut::<SimTick>() = SimTick(ticks);
437 }
438
439 fn structures(app: &mut App) -> Vec<(UVec2, StructureKind)> {
440 let world = app.world_mut();
441 world
442 .query::<(&TilePos, &Structure)>()
443 .iter(world)
444 .map(|(pos, s)| (pos.0, s.kind))
445 .collect()
446 }
447
448 fn put_campfire(app: &mut App, tile: UVec2) {
449 app.world_mut().spawn((
450 Structure {
451 kind: StructureKind::Campfire,
452 },
453 Fuel(StructureKind::Campfire.fuel_ticks().unwrap()),
454 TilePos(tile),
455 ));
456 }
457
458 #[test]
459 fn colonists_light_a_campfire_near_the_stockpile_when_night_falls() {
460 let mut app = hauling_app((IVec2::new(10, 10), IVec2::new(12, 12)));
461 put_wood(&mut app, UVec2::new(10, 10), 20);
462 set_hour(&mut app, 18);
463 spawn_colonist(&mut app, UVec2::new(2, 5));
464 run_ticks(&mut app, 60 * 20);
465 let built = structures(&mut app);
466 let campfire = built
467 .iter()
468 .find(|(_, kind)| *kind == StructureKind::Campfire)
469 .expect("a campfire");
470 assert!(campfire.0.as_vec2().distance(Vec2::new(11.0, 11.0)) <= HOME_RADIUS);
471 assert_eq!(wood_on_map(&mut app), 10);
472 }
473
474 #[test]
475 fn no_fires_are_built_during_the_day() {
476 let mut app = hauling_app((IVec2::new(10, 10), IVec2::new(12, 12)));
477 put_wood(&mut app, UVec2::new(10, 10), 20);
478 set_hour(&mut app, 10);
479 spawn_colonist(&mut app, UVec2::new(2, 5));
480 run_ticks(&mut app, 60 * 10);
481 assert!(
482 structures(&mut app)
483 .iter()
484 .all(|(_, kind)| kind.light().is_none())
485 );
486 }
487
488 #[test]
489 fn without_wood_nothing_can_be_built() {
490 let mut app = hauling_app((IVec2::new(10, 10), IVec2::new(12, 12)));
491 set_hour(&mut app, 22);
492 spawn_colonist(&mut app, UVec2::new(2, 5));
493 run_ticks(&mut app, 60 * 10);
494 assert!(
495 structures(&mut app)
496 .iter()
497 .all(|(_, kind)| kind.light().is_none())
498 );
499 }
500
501 #[test]
502 fn torches_light_stockpile_tiles_beyond_the_campfire() {
503 let mut app = hauling_app((IVec2::new(2, 10), IVec2::new(17, 10)));
504 put_wood(&mut app, UVec2::new(9, 10), 20);
505 put_campfire(&mut app, UVec2::new(9, 12));
506 set_hour(&mut app, 22);
507 spawn_colonist(&mut app, UVec2::new(9, 8));
508 run_ticks(&mut app, 60 * 15);
509 assert!(
510 structures(&mut app)
511 .iter()
512 .any(|(_, kind)| *kind == StructureKind::Torch)
513 );
514 }
515
516 // Light is what makes fires worth building: work in darkness goes at half speed.
517 #[test]
518 fn work_is_slower_in_the_dark() {
519 let tree = UVec2::new(8, 5);
520 let finish_ticks = |daylight: f32| {
521 let mut app = test_app(map_with_trees(&[tree]));
522 let map = app.world().resource::<WorldMap>().clone();
523 app.insert_resource(LightMap::compute(&map, daylight, []));
524 spawn_colonist(&mut app, UVec2::new(7, 5));
525 let mut ticks = 0;
526 while app
527 .world()
528 .resource::<WorldMap>()
529 .plant(tree.as_ivec2())
530 .is_some()
531 {
532 run_ticks(&mut app, 1);
533 ticks += 1;
534 }
535 ticks
536 };
537 let (lit, dark) = (finish_ticks(1.0), finish_ticks(0.0));
538 assert!(
539 dark > lit + WorkKind::Chop.work() / 2 - 10,
540 "lit {lit}, dark {dark}"
541 );
542 }
543
544 const NAP_TEST_REST: f32 = 0.25;
545
546 fn set_rest(app: &mut App, colonist: Entity, rest: f32) {
547 app.world_mut().get_mut::<Rest>(colonist).unwrap().0 = rest;
548 }
549
550 fn rest(app: &App, colonist: Entity) -> f32 {
551 app.world().get::<Rest>(colonist).unwrap().0
552 }
553
554 fn task(app: &App, colonist: Entity) -> Option<Task> {
555 job(app, colonist).map(|j| j.task)
556 }
557
558 #[test]
559 fn tired_colonists_nap_and_get_up_when_rested() {
560 let mut app = test_app(map_with_trees(&[]));
561 set_goals(&mut app, &[]);
562 set_hour(&mut app, 10);
563 let colonist = spawn_colonist(&mut app, UVec2::new(5, 5));
564 set_rest(&mut app, colonist, NAP_TEST_REST);
565 run_ticks(&mut app, 61);
566 assert!(matches!(task(&app, colonist), Some(Task::Sleep { .. })));
567 let mut ticks = 0;
568 while matches!(task(&app, colonist), Some(Task::Sleep { .. })) && ticks < 60 * 60 * 10 {
569 run_ticks(&mut app, 60);
570 ticks += 60;
571 }
572 assert!(!matches!(task(&app, colonist), Some(Task::Sleep { .. })));
573 assert!(rest(&app, colonist) > 0.95);
574 }
575
576 #[test]
577 fn colonists_go_to_bed_at_night_and_sleep_until_morning() {
578 let mut app = test_app(map_with_trees(&[]));
579 set_goals(&mut app, &[]);
580 set_hour(&mut app, 22);
581 let colonist = spawn_colonist(&mut app, UVec2::new(5, 5));
582 set_rest(&mut app, colonist, 0.8);
583 run_ticks(&mut app, 61);
584 assert!(matches!(task(&app, colonist), Some(Task::Sleep { .. })));
585 run_ticks(&mut app, 3 * crate::sim::TICKS_PER_HOUR as u32);
586 assert_eq!(rest(&app, colonist), 1.0);
587 assert!(matches!(task(&app, colonist), Some(Task::Sleep { .. })));
588 }
589
590 #[test]
591 fn rested_colonists_stay_up_during_the_day() {
592 let mut app = test_app(map_with_trees(&[]));
593 set_goals(&mut app, &[]);
594 set_hour(&mut app, 12);
595 let colonist = spawn_colonist(&mut app, UVec2::new(5, 5));
596 set_rest(&mut app, colonist, 0.8);
597 run_ticks(&mut app, 60 * 5);
598 assert!(!matches!(task(&app, colonist), Some(Task::Sleep { .. })));
599 }
600
601 #[test]
602 fn exhausted_colonists_drop_their_work_to_sleep() {
603 let tree = UVec2::new(15, 5);
604 let mut app = test_app(map_with_trees(&[tree]));
605 let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
606 run_ticks(&mut app, 61);
607 assert!(matches!(task(&app, colonist), Some(Task::Work { .. })));
608 set_rest(&mut app, colonist, EXHAUSTED_REST / 2.0);
609 run_ticks(&mut app, 30);
610 assert!(matches!(task(&app, colonist), Some(Task::Sleep { .. })));
611 assert!(
612 app.world()
613 .resource::<Reservations>()
614 .is_free(Target::Site(tree))
615 );
616 }
617
618 #[test]
619 fn exhaustion_does_not_override_orders() {
620 let tree = UVec2::new(15, 5);
621 let mut app = test_app(map_with_trees(&[tree]));
622 let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
623 app.world_mut().write_message(Order {
624 colonist,
625 tile: tree,
626 });
627 run_ticks(&mut app, 2);
628 set_rest(&mut app, colonist, EXHAUSTED_REST / 2.0);
629 run_ticks(&mut app, 30);
630 assert!(matches!(
631 job(&app, colonist),
632 Some(Job {
633 task: Task::Work { .. },
634 ordered: true
635 })
636 ));
637 }
638
639 fn beds(app: &mut App) -> usize {
640 structures(app)
641 .iter()
642 .filter(|(_, kind)| *kind == StructureKind::Bed)
643 .count()
644 }
645
646 #[test]
647 fn colonists_build_one_bed_each() {
648 let mut app = test_app(map_with_trees(&[]));
649 set_goals(&mut app, &[]);
650 let map = app.world().resource::<WorldMap>().clone();
651 app.world_mut()
652 .resource_mut::<Zones>()
653 .add_stockpile(&map, IVec2::new(2, 2), IVec2::new(5, 5));
654 put_wood(&mut app, UVec2::new(2, 2), 50);
655 put_wood(&mut app, UVec2::new(3, 2), 50);
656 spawn_colonist(&mut app, UVec2::new(10, 10));
657 spawn_colonist(&mut app, UVec2::new(11, 10));
658 run_ticks(&mut app, 60 * 40);
659 assert_eq!(beds(&mut app), 2);
660 assert_eq!(wood_on_map(&mut app), 100 - 2 * StructureKind::Bed.cost().1);
661 }
662
663 // A shelter needs a wall ring with walking space around the beds; beds hugging the stockpile
664 // would leave stored items where the walls must go and the room could never be planned.
665 #[test]
666 fn beds_keep_clear_of_the_stockpile() {
667 let mut app = test_app(map_with_trees(&[]));
668 set_goals(&mut app, &[]);
669 let map = app.world().resource::<WorldMap>().clone();
670 app.world_mut().resource_mut::<Zones>().add_stockpile(
671 &map,
672 IVec2::new(8, 8),
673 IVec2::new(11, 11),
674 );
675 put_wood(&mut app, UVec2::new(8, 8), 50);
676 spawn_colonist(&mut app, UVec2::new(10, 4));
677 run_ticks(&mut app, 60 * 20);
678 let built = structures(&mut app);
679 let bed = built
680 .iter()
681 .find(|(_, kind)| *kind == StructureKind::Bed)
682 .expect("a bed");
683 let zones = app.world().resource::<Zones>();
684 assert!(zones.stockpile_tiles().all(|s| {
685 (s.as_ivec2() - bed.0.as_ivec2()).abs().max_element() >= BED_STOCKPILE_CLEARANCE
686 }));
687 }
688
689 #[test]
690 fn colonists_sleep_in_their_beds() {
691 let mut app = test_app(map_with_trees(&[]));
692 set_goals(&mut app, &[]);
693 put_beds(&mut app, 1);
694 set_hour(&mut app, 22);
695 let colonist = spawn_colonist(&mut app, UVec2::new(5, 5));
696 set_rest(&mut app, colonist, 0.5);
697 run_ticks(&mut app, 60 * 10);
698 assert_eq!(
699 app.world().get::<TilePos>(colonist).unwrap().0,
700 UVec2::new(19, 19)
701 );
702 assert!(matches!(task(&app, colonist), Some(Task::Sleep { .. })));
703 }
704
705 fn put_items(app: &mut App, tile: UVec2, kind: ItemKind, count: u32) {
706 app.world_mut()
707 .spawn((Item::new(kind, count), TilePos(tile)));
708 }
709
710 #[test]
711 fn blueprints_are_built_with_the_right_material() {
712 let mut app = test_app(map_with_trees(&[]));
713 set_goals(&mut app, &[]);
714 put_beds(&mut app, 1);
715 put_items(&mut app, UVec2::new(2, 2), ItemKind::Stone, 10);
716 let wall = UVec2::new(8, 8);
717 app.world_mut()
718 .resource_mut::<Blueprints>()
719 .0
720 .insert(wall, BuildKind::Structure(StructureKind::Wall));
721 spawn_colonist(&mut app, UVec2::new(5, 5));
722 run_ticks(&mut app, 60 * 10);
723 assert!(app.world().resource::<WorldMap>().is_wall(wall.as_ivec2()));
724 assert!(app.world().resource::<Blueprints>().0.is_empty());
725 assert_eq!(items_of(&mut app, ItemKind::Stone), 8);
726 }
727
728 #[test]
729 fn colonists_build_a_roofed_shelter_around_their_beds() {
730 let mut app = test_app(map_with_trees(&[]));
731 app.add_systems(Update, crate::building::plan_shelter.before(choose_jobs));
732 set_goals(&mut app, &[]);
733 for x in [10, 11] {
734 app.world_mut().spawn((
735 Structure {
736 kind: StructureKind::Bed,
737 },
738 TilePos(UVec2::new(x, 10)),
739 ));
740 }
741 put_items(&mut app, UVec2::new(2, 2), ItemKind::Stone, 50);
742 put_items(&mut app, UVec2::new(3, 2), ItemKind::Wood, 50);
743 spawn_colonist(&mut app, UVec2::new(4, 4));
744 spawn_colonist(&mut app, UVec2::new(5, 4));
745 run_ticks(&mut app, 60 * 150);
746 let map = app.world().resource::<WorldMap>();
747 assert!(map.is_roofed(IVec2::new(10, 10)) && map.is_roofed(IVec2::new(11, 10)));
748 let walls = map
749 .positions()
750 .filter(|p| map.is_wall(p.as_ivec2()))
751 .count();
752 assert_eq!(walls, 17);
753 assert!(
754 structures(&mut app)
755 .iter()
756 .any(|(_, kind)| *kind == StructureKind::Door)
757 );
758 }
759
760 #[test]
761 fn colonists_chop_from_a_neighbouring_tile() {
762 let tree = UVec2::new(8, 5);
763 let mut app = test_app(map_with_trees(&[tree]));
764 let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
765 run_ticks(&mut app, 60 * 5);
766 let tile = app.world().get::<TilePos>(colonist).unwrap().0;
767 assert!(is_adjacent(tile.as_ivec2(), tree.as_ivec2()));
768 assert_eq!(
769 app.world().get::<SimPos>(colonist).unwrap().0,
770 WorldMap::tile_center(tile)
771 );
772 }
773
774 fn set_fed(app: &mut App, colonist: Entity, fed: f32) {
775 app.world_mut().get_mut::<Hunger>(colonist).unwrap().0 = fed;
776 }
777
778 fn fed(app: &App, colonist: Entity) -> f32 {
779 app.world().get::<Hunger>(colonist).unwrap().0
780 }
781
782 #[test]
783 fn hungry_colonists_eat_until_full() {
784 let mut app = test_app(map_with_trees(&[]));
785 set_goals(&mut app, &[]);
786 put_items(&mut app, UVec2::new(8, 5), ItemKind::Berries, 20);
787 let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
788 set_fed(&mut app, colonist, 0.3);
789 run_ticks(&mut app, 60 * 6);
790 assert!(fed(&app, colonist) > 0.95);
791 assert_eq!(items_of(&mut app, ItemKind::Berries), 13);
792 }
793
794 #[test]
795 fn colonists_are_not_peckish_when_fed() {
796 let mut app = test_app(map_with_trees(&[]));
797 set_goals(&mut app, &[]);
798 put_items(&mut app, UVec2::new(8, 5), ItemKind::Berries, 20);
799 let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
800 set_fed(&mut app, colonist, 0.8);
801 run_ticks(&mut app, 60 * 6);
802 assert_eq!(items_of(&mut app, ItemKind::Berries), 20);
803 }
804
805 #[test]
806 fn starving_colonists_drop_their_work_to_eat() {
807 let tree = UVec2::new(15, 5);
808 let mut app = test_app(map_with_trees(&[tree]));
809 put_items(&mut app, UVec2::new(2, 8), ItemKind::Meat, 20);
810 let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
811 run_ticks(&mut app, 61);
812 assert!(task(&app, colonist).is_some_and(|t| !matches!(t, Task::Eat { .. })));
813 set_fed(&mut app, colonist, STARVING / 2.0);
814 run_ticks(&mut app, 30);
815 assert!(matches!(task(&app, colonist), Some(Task::Eat { .. })));
816 }
817
818 // Starvation only interrupts work when there is food to fetch. Without that check a starving
819 // colonist dropped their job every tick, searched the whole map for food and picked the same
820 // job again, never finishing anything and stalling the simulation.
821 #[test]
822 fn starving_colonists_without_food_keep_working() {
823 let tree = UVec2::new(6, 5);
824 let mut app = test_app(map_with_trees(&[tree]));
825 set_goals(&mut app, &[(ItemKind::Wood, 50)]);
826 let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
827 set_fed(&mut app, colonist, STARVING / 2.0);
828 run_ticks(&mut app, 60 + 60 * 4 + WorkKind::Chop.work());
829 assert!(items_of(&mut app, ItemKind::Wood) > 0);
830 }
831
832 #[test]
833 fn colonists_forage_ripe_berries_when_the_colony_wants_them() {
834 let mut map = map_with_trees(&[]);
835 let ripe = UVec2::new(8, 5);
836 let unripe = UVec2::new(4, 5);
837 for (bush, growth) in [(ripe, 1.0), (unripe, 0.5)] {
838 map.set_plant(bush, Some(Plant::BerryBush));
839 map.set_growth(bush, growth);
840 }
841 let mut app = test_app(map);
842 set_goals(&mut app, &[(ItemKind::Berries, 30)]);
843 spawn_colonist(&mut app, UVec2::new(2, 5));
844 run_ticks(&mut app, 60 * 10);
845 assert_eq!(items_of(&mut app, ItemKind::Berries), 8);
846 let map = app.world().resource::<WorldMap>();
847 assert_eq!(map.plant(ripe.as_ivec2()), Some(Plant::BerryBush));
848 assert_eq!(map.growth(ripe.as_ivec2()), 0.0);
849 assert_eq!(map.growth(unripe.as_ivec2()), 0.5);
850 }
851
852 fn put_animal(app: &mut App, kind: AnimalKind, tile: UVec2) -> Entity {
853 app.world_mut()
854 .spawn((
855 Animal { kind },
856 TilePos(tile),
857 sim_pos(WorldMap::tile_center(tile)),
858 ))
859 .id()
860 }
861
862 #[test]
863 fn colonists_hunt_when_the_colony_wants_meat() {
864 let mut app = test_app(map_with_trees(&[]));
865 app.add_systems(Update, crate::animals::wander.before(choose_jobs));
866 set_goals(&mut app, &[(ItemKind::Meat, 20)]);
867 let deer = put_animal(&mut app, AnimalKind::Deer, UVec2::new(15, 15));
868 spawn_colonist(&mut app, UVec2::new(2, 2));
869 run_ticks(&mut app, 60 * 15 + BUTCHER_WORK);
870 assert!(app.world().get_entity(deer).is_err());
871 assert_eq!(items_of(&mut app, ItemKind::Meat), AnimalKind::Deer.meat());
872 }
873
874 #[test]
875 fn animals_are_left_alone_when_there_is_enough_meat() {
876 let mut app = test_app(map_with_trees(&[]));
877 set_goals(&mut app, &[]);
878 let rabbit = put_animal(&mut app, AnimalKind::Rabbit, UVec2::new(10, 10));
879 spawn_colonist(&mut app, UVec2::new(2, 2));
880 run_ticks(&mut app, 60 * 10);
881 assert!(app.world().get_entity(rabbit).is_ok());
882 }
883
884 #[test]
885 fn right_clicking_an_animal_orders_a_hunt() {
886 let mut app = test_app(map_with_trees(&[]));
887 set_goals(&mut app, &[]);
888 let rabbit = put_animal(&mut app, AnimalKind::Rabbit, UVec2::new(10, 10));
889 let colonist = spawn_colonist(&mut app, UVec2::new(2, 2));
890 app.world_mut().write_message(Order {
891 colonist,
892 tile: UVec2::new(10, 10),
893 });
894 run_ticks(&mut app, 60 * 10);
895 assert!(app.world().get_entity(rabbit).is_err());
896 assert_eq!(
897 items_of(&mut app, ItemKind::Meat),
898 AnimalKind::Rabbit.meat()
899 );
900 }
901
902 fn farm_app(tiles: &[UVec2]) -> App {
903 let mut map = map_with_trees(&[]);
904 for &t in tiles {
905 map.set(t, Terrain::Soil);
906 }
907 let mut app = test_app(map);
908 set_goals(&mut app, &[]);
909 put_beds(&mut app, 1);
910 let map = app.world().resource::<WorldMap>().clone();
911 app.world_mut()
912 .resource_mut::<Zones>()
913 .add_farm_tiles(&map, tiles.iter().copied());
914 app
915 }
916
917 #[test]
918 fn colonists_sow_empty_farm_tiles() {
919 let farm = [UVec2::new(8, 8), UVec2::new(9, 8)];
920 let mut app = farm_app(&farm);
921 spawn_colonist(&mut app, UVec2::new(2, 2));
922 run_ticks(&mut app, 60 * 10);
923 let map = app.world().resource::<WorldMap>();
924 assert!(
925 farm.iter()
926 .all(|t| map.plant(t.as_ivec2()) == Some(Plant::Crop))
927 );
928 }
929
930 // Sowing used to be the last resort, so a colony with any material or construction demand
931 // never planted its farm and starved once wild food ran out.
932 #[test]
933 fn hungry_colonies_sow_before_gathering_materials() {
934 let farm = [UVec2::new(8, 8)];
935 let mut app = farm_app(&farm);
936 app.world_mut()
937 .resource_mut::<WorldMap>()
938 .set_plant(UVec2::new(3, 2), Some(Plant::Tree));
939 set_goals(
940 &mut app,
941 &[(ItemKind::Vegetables, 30), (ItemKind::Wood, 30)],
942 );
943 let colonist = spawn_colonist(&mut app, UVec2::new(2, 2));
944 run_ticks(&mut app, 61);
945 assert!(matches!(
946 task(&app, colonist),
947 Some(Task::Work {
948 kind: WorkKind::Sow,
949 ..
950 })
951 ));
952 }
953
954 #[test]
955 fn hungry_colonies_harvest_before_gathering_materials() {
956 let farm = [UVec2::new(8, 8)];
957 let mut app = farm_app(&farm);
958 {
959 let mut map = app.world_mut().resource_mut::<WorldMap>();
960 map.set_plant(UVec2::new(3, 2), Some(Plant::Tree));
961 map.set_plant(farm[0], Some(Plant::Crop));
962 map.set_growth(farm[0], 1.0);
963 }
964 set_goals(
965 &mut app,
966 &[(ItemKind::Vegetables, 30), (ItemKind::Wood, 30)],
967 );
968 let colonist = spawn_colonist(&mut app, UVec2::new(2, 2));
969 run_ticks(&mut app, 61);
970 assert!(matches!(
971 task(&app, colonist),
972 Some(Task::Work {
973 kind: WorkKind::Harvest,
974 ..
975 })
976 ));
977 }
978
979 #[test]
980 fn ripe_crops_are_harvested_into_vegetables_and_resown() {
981 let farm = [UVec2::new(8, 8)];
982 let mut app = farm_app(&farm);
983 set_goals(&mut app, &[(ItemKind::Vegetables, 30)]);
984 {
985 let mut map = app.world_mut().resource_mut::<WorldMap>();
986 map.set_plant(farm[0], Some(Plant::Crop));
987 map.set_growth(farm[0], 1.0);
988 }
989 spawn_colonist(&mut app, UVec2::new(2, 2));
990 run_ticks(&mut app, 60 * 15);
991 assert_eq!(items_of(&mut app, ItemKind::Vegetables), 10);
992 let map = app.world().resource::<WorldMap>();
993 assert_eq!(map.plant(farm[0].as_ivec2()), Some(Plant::Crop));
994 assert!(map.growth(farm[0].as_ivec2()) < 0.1);
995 }
996
997 // Food the colony cannot reach must not count towards its stock: a stray stack far from home
998 // once satisfied the raw food target, so nobody harvested ripe crops while everyone starved.
999 #[test]
1000 fn stock_out_of_the_colonys_reach_does_not_stop_harvesting() {
1001 let farm = UVec2::new(8, 8);
1002 let mut map = WorldMap::filled(UVec2::splat(150), Terrain::Grass);
1003 map.set(farm, Terrain::Soil);
1004 map.set_plant(farm, Some(Plant::Crop));
1005 map.set_growth(farm, 1.0);
1006 let mut app = test_app(map.clone());
1007 set_goals(&mut app, &[(ItemKind::Vegetables, 10)]);
1008 put_beds(&mut app, 1);
1009 let mut zones = app.world_mut().resource_mut::<Zones>();
1010 zones.add_farm_tiles(&map, [farm]);
1011 zones.add_tiles(&map, [UVec2::new(4, 4)]);
1012 let far = UVec2::new(4, 4) + UVec2::splat(COLONY_RADIUS as u32 + 1);
1013 put_items(&mut app, far, ItemKind::Meat, 20);
1014 spawn_colonist(&mut app, UVec2::new(2, 2));
1015 run_ticks(&mut app, 60 * 15);
1016 assert!(items_of(&mut app, ItemKind::Vegetables) > 0);
1017 }
1018
1019 #[test]
1020 fn colonist_searches_cover_the_colony_radius() {
1021 let map = WorldMap::filled(UVec2::splat(200), Terrain::Grass);
1022 let home = UVec2::splat(100);
1023 let corner = home + UVec2::splat(COLONY_RADIUS as u32);
1024 assert_eq!(
1025 nearest_tile(&map, home, SEARCH_TILES, |p| p == corner),
1026 Some(corner)
1027 );
1028 }
1029
1030 #[test]
1031 fn colonists_cook_raw_food_into_meals_at_a_campfire() {
1032 let mut app = test_app(map_with_trees(&[]));
1033 set_goals(&mut app, &[(ItemKind::Meal, 2)]);
1034 put_beds(&mut app, 1);
1035 put_campfire(&mut app, UVec2::new(10, 10));
1036 put_items(&mut app, UVec2::new(5, 5), ItemKind::Meat, 12);
1037 spawn_colonist(&mut app, UVec2::new(2, 2));
1038 run_ticks(&mut app, 60 * 20);
1039 assert_eq!(items_of(&mut app, ItemKind::Meal), 2);
1040 assert_eq!(items_of(&mut app, ItemKind::Meat), 0);
1041 }
1042
1043 #[test]
1044 fn colonists_light_a_fire_to_cook_on() {
1045 let mut app = test_app(map_with_trees(&[]));
1046 set_goals(&mut app, &[(ItemKind::Meal, 1)]);
1047 put_beds(&mut app, 1);
1048 put_items(&mut app, UVec2::new(5, 5), ItemKind::Berries, 6);
1049 put_items(&mut app, UVec2::new(6, 5), ItemKind::Wood, 10);
1050 spawn_colonist(&mut app, UVec2::new(2, 2));
1051 run_ticks(&mut app, 60 * 25);
1052 assert!(
1053 structures(&mut app)
1054 .iter()
1055 .any(|(_, kind)| *kind == StructureKind::Campfire)
1056 );
1057 assert_eq!(items_of(&mut app, ItemKind::Meal), 1);
1058 }
1059
1060 #[test]
1061 fn no_cooking_without_demand_for_meals() {
1062 let mut app = test_app(map_with_trees(&[]));
1063 set_goals(&mut app, &[]);
1064 put_beds(&mut app, 1);
1065 put_campfire(&mut app, UVec2::new(10, 10));
1066 put_items(&mut app, UVec2::new(5, 5), ItemKind::Meat, 12);
1067 spawn_colonist(&mut app, UVec2::new(2, 2));
1068 run_ticks(&mut app, 60 * 10);
1069 assert_eq!(items_of(&mut app, ItemKind::Meal), 0);
1070 }
1071
1072 // Chopping and hauling leave material in many small piles; a builder gathers from several
1073 // piles instead of waiting for one big enough to cover the whole cost.
1074 #[test]
1075 fn builders_gather_material_from_several_piles() {
1076 let mut app = test_app(map_with_trees(&[]));
1077 set_goals(&mut app, &[]);
1078 put_wood(&mut app, UVec2::new(4, 4), 10);
1079 put_wood(&mut app, UVec2::new(15, 4), 10);
1080 spawn_colonist(&mut app, UVec2::new(2, 2));
1081 run_ticks(&mut app, 60 * 30);
1082 assert_eq!(beds(&mut app), 1);
1083 assert_eq!(wood_on_map(&mut app), 20 - StructureKind::Bed.cost().1);
1084 }
1085
1086 #[test]
1087 fn colonists_pave_planned_paths_with_stone() {
1088 let mut app = test_app(map_with_trees(&[]));
1089 set_goals(&mut app, &[]);
1090 put_beds(&mut app, 1);
1091 let tile = UVec2::new(8, 8);
1092 app.world_mut()
1093 .resource_mut::<WorldMap>()
1094 .set(tile, Terrain::Path);
1095 app.world_mut()
1096 .resource_mut::<Blueprints>()
1097 .0
1098 .insert(tile, BuildKind::Pave);
1099 put_items(&mut app, UVec2::new(2, 2), ItemKind::Stone, 5);
1100 spawn_colonist(&mut app, UVec2::new(4, 4));
1101 run_ticks(&mut app, 60 * 10);
1102 assert_eq!(
1103 app.world().resource::<WorldMap>().get(tile.as_ivec2()),
1104 Some(Terrain::Paved)
1105 );
1106 assert_eq!(items_of(&mut app, ItemKind::Stone), 4);
1107 }
1108
1109 // The wall-builder must not trap a colonist who is just stepping onto the tile: that colonist's
1110 // TilePos is still the previous tile, so only checking TilePos would let the wall close on them.
1111 #[test]
1112 fn walls_wait_for_colonists_stepping_onto_the_tile() {
1113 let mut app = test_app(map_with_trees(&[]));
1114 set_goals(&mut app, &[]);
1115 put_beds(&mut app, 2);
1116 put_items(&mut app, UVec2::new(2, 2), ItemKind::Stone, 10);
1117 let wall = UVec2::new(8, 8);
1118 app.world_mut()
1119 .resource_mut::<Blueprints>()
1120 .0
1121 .insert(wall, BuildKind::Structure(StructureKind::Wall));
1122 let colonists = [
1123 spawn_colonist(&mut app, UVec2::new(4, 4)),
1124 spawn_colonist(&mut app, UVec2::new(8, 10)),
1125 ];
1126 let mut sent = false;
1127 for _ in 0..60 * 20 {
1128 let nearly_built = |c| {
1129 matches!(
1130 task(&app, c),
1131 Some(Task::Build { progress, kind, .. }) if progress + 4 >= kind.work()
1132 )
1133 };
1134 if !sent && let Some(&builder) = colonists.iter().find(|&&c| nearly_built(c)) {
1135 let walker = *colonists.iter().find(|&&c| c != builder).unwrap();
1136 let from = app.world().get::<TilePos>(walker).unwrap().0;
1137 let route: VecDeque<UVec2> = find_path(app.world().resource::<WorldMap>(), from, wall)
1138 .unwrap()
1139 .into();
1140 app.world_mut().get_mut::<Path>(walker).unwrap().0 = route;
1141 sent = true;
1142 }
1143 run_ticks(&mut app, 1);
1144 let map = app.world().resource::<WorldMap>();
1145 for &c in &colonists {
1146 let tile = app.world().get::<TilePos>(c).unwrap().0;
1147 assert!(!map.is_wall(tile.as_ivec2()), "colonist trapped in a wall");
1148 }
1149 }
1150 assert!(sent);
1151 assert!(app.world().resource::<WorldMap>().is_wall(wall.as_ivec2()));
1152 }
1153
1154 #[test]
1155 fn children_play_instead_of_working() {
1156 let mut app = test_app(map_with_trees(&[UVec2::new(4, 2)]));
1157 set_goals(&mut app, &[(ItemKind::Wood, 50)]);
1158 let child = spawn_colonist(&mut app, UVec2::new(2, 2));
1159 app.world_mut()
1160 .entity_mut(child)
1161 .insert(crate::family::Child { born: 0 });
1162 run_ticks(&mut app, 60 * 10);
1163 assert!(!matches!(task(&app, child), Some(Task::Work { .. })));
1164 assert_eq!(items_of(&mut app, ItemKind::Wood), 0);
1165 }
1166
1167 #[test]
1168 fn colonists_refuel_a_dying_campfire_instead_of_building_a_new_one() {
1169 let mut app = test_app(map_with_trees(&[]));
1170 set_goals(&mut app, &[]);
1171 put_beds(&mut app, 1);
1172 put_campfire(&mut app, UVec2::new(10, 10));
1173 let full = StructureKind::Campfire.fuel_ticks().unwrap();
1174 {
1175 let world = app.world_mut();
1176 let mut fuel = world.query::<&mut Fuel>();
1177 fuel.single_mut(world).unwrap().0 = full / 10;
1178 }
1179 put_items(&mut app, UVec2::new(5, 5), ItemKind::Wood, 20);
1180 spawn_colonist(&mut app, UVec2::new(2, 2));
1181 run_ticks(&mut app, 60 * 15);
1182 let world = app.world_mut();
1183 let fuel: Vec<u32> = world.query::<&Fuel>().iter(world).map(|f| f.0).collect();
1184 assert_eq!(fuel.len(), 1);
1185 assert!(fuel[0] > full / 2, "{fuel:?}");
1186 }
1187
1188 #[test]
1189 fn designated_trees_are_cut_even_when_wood_is_not_wanted() {
1190 let marked = UVec2::new(12, 5);
1191 let mut app = test_app(map_with_trees(&[UVec2::new(4, 5), marked]));
1192 set_goals(&mut app, &[]);
1193 app.world_mut()
1194 .resource_mut::<crate::designations::Designations>()
1195 .0
1196 .insert(marked);
1197 spawn_colonist(&mut app, UVec2::new(2, 5));
1198 run_ticks(&mut app, 61 + 60 * 3 + WorkKind::Chop.work());
1199 let map = app.world().resource::<WorldMap>();
1200 assert!(map.plant(marked.as_ivec2()).is_none());
1201 assert_eq!(map.plant(IVec2::new(4, 5)), Some(Plant::Tree));
1202 }
1203
1204 #[test]
1205 fn colonists_deconstruct_marked_structures_and_recover_some_material() {
1206 let mut app = test_app(map_with_trees(&[]));
1207 set_goals(&mut app, &[]);
1208 let wall = UVec2::new(8, 5);
1209 app.world_mut().spawn((
1210 Structure {
1211 kind: StructureKind::Wall,
1212 },
1213 TilePos(wall),
1214 ));
1215 app.world_mut()
1216 .resource_mut::<WorldMap>()
1217 .set_wall(wall, true);
1218 app.world_mut()
1219 .resource_mut::<Blueprints>()
1220 .0
1221 .insert(wall, BuildKind::Deconstruct);
1222 spawn_colonist(&mut app, UVec2::new(2, 5));
1223 run_ticks(&mut app, 60 * 10);
1224 assert!(!app.world().resource::<WorldMap>().is_wall(wall.as_ivec2()));
1225 let world = app.world_mut();
1226 assert_eq!(world.query::<&Structure>().iter(world).count(), 0);
1227 assert!(app.world().resource::<Blueprints>().0.is_empty());
1228 assert_eq!(items_of(&mut app, ItemKind::Stone), 1);
1229 }
1230
1231 #[test]
1232 fn carcasses_are_butchered_into_meat() {
1233 let mut app = test_app(map_with_trees(&[]));
1234 set_goals(&mut app, &[]);
1235 put_items(&mut app, UVec2::new(8, 5), ItemKind::Carcass, 8);
1236 spawn_colonist(&mut app, UVec2::new(2, 5));
1237 run_ticks(&mut app, 61 + 60 * 3 + BUTCHER_WORK);
1238 assert_eq!(items_of(&mut app, ItemKind::Carcass), 0);
1239 assert_eq!(items_of(&mut app, ItemKind::Meat), 8);
1240 }
1241
1242 #[test]
1243 fn hunters_shoot_from_a_distance() {
1244 let mut app = test_app(map_with_trees(&[]));
1245 app.add_systems(Update, crate::animals::wander.before(choose_jobs));
1246 set_goals(&mut app, &[(ItemKind::Meat, 20)]);
1247 let deer = put_animal(&mut app, AnimalKind::Deer, UVec2::new(15, 15));
1248 let hunter = spawn_colonist(&mut app, UVec2::new(2, 2));
1249 let mut shots_from = Vec::new();
1250 let mut last_progress = 0;
1251 for _ in 0..60 * 15 {
1252 run_ticks(&mut app, 1);
1253 let Some(prey) = app.world().get::<TilePos>(deer) else {
1254 break;
1255 };
1256 let at = app.world().get::<TilePos>(hunter).unwrap();
1257 if let Some(Task::Hunt { progress, .. }) = task(&app, hunter) {
1258 if progress > last_progress {
1259 shots_from.push(prey.0.as_vec2().distance(at.0.as_vec2()));
1260 }
1261 last_progress = progress;
1262 }
1263 }
1264 assert!(app.world().get_entity(deer).is_err());
1265 assert!(shots_from.iter().any(|&d| d > 2.0), "{shots_from:?}");
1266 assert!(
1267 shots_from.iter().all(|&d| d <= HUNT_RANGE + 1.5),
1268 "{shots_from:?}"
1269 );
1270 }
1271
1272 #[test]
1273 fn colonists_tame_wild_animals_when_food_is_plentiful() {
1274 let mut app = test_app(map_with_trees(&[]));
1275 app.add_systems(Update, crate::animals::wander.before(choose_jobs));
1276 set_goals(&mut app, &[]);
1277 put_beds(&mut app, 1);
1278 let rabbit = put_animal(&mut app, AnimalKind::Rabbit, UVec2::new(12, 12));
1279 spawn_colonist(&mut app, UVec2::new(2, 2));
1280 run_ticks(&mut app, 60 * 6 + AnimalKind::Rabbit.tame_work());
1281 assert!(app.world().get::<crate::animals::Tame>(rabbit).is_some());
1282 }
1283
1284 // Tame herds are a renewable meat supply, so hunters take spare tame animals (beyond a breeding
1285 // pair) before chasing wild ones.
1286 #[test]
1287 fn hunters_slaughter_spare_tame_animals_first() {
1288 let wild = (
1289 Entity::from_raw_u32(1).unwrap(),
1290 UVec2::new(5, 5),
1291 AnimalKind::Deer,
1292 false,
1293 );
1294 let tame = |i: u32, x: u32| {
1295 (
1296 Entity::from_raw_u32(10 + i).unwrap(),
1297 UVec2::new(x, 15),
1298 AnimalKind::Deer,
1299 true,
1300 )
1301 };
1302 let map = map_with_trees(&[]);
1303 let reservations = Reservations::default();
1304 let pick = |animals: &[Wildlife]| match find_hunt(&map, &reservations, animals, UVec2::ONE) {
1305 Some(Task::Hunt { animal, .. }) => animal,
1306 _ => panic!("no hunt"),
1307 };
1308 assert_eq!(pick(&[wild, tame(0, 10), tame(1, 11)]), wild.0);
1309 let herd = [wild, tame(0, 10), tame(1, 11), tame(2, 12)];
1310 assert_eq!(pick(&herd), tame(0, 10).0);
1311 }
1312
1313 #[test]
1314 fn colonists_skip_work_types_they_are_not_assigned() {
1315 let mut app = test_app(map_with_trees(&[UVec2::new(4, 2)]));
1316 set_goals(&mut app, &[(ItemKind::Wood, 50)]);
1317 let colonist = spawn_colonist(&mut app, UVec2::new(2, 2));
1318 app.world_mut()
1319 .get_mut::<WorkPriorities>(colonist)
1320 .unwrap()
1321 .toggle(WorkType::Gather);
1322 run_ticks(&mut app, 60 * 10);
1323 assert_eq!(items_of(&mut app, ItemKind::Wood), 0);
1324 app.world_mut()
1325 .get_mut::<WorkPriorities>(colonist)
1326 .unwrap()
1327 .toggle(WorkType::Gather);
1328 run_ticks(&mut app, 60 * 15);
1329 assert!(items_of(&mut app, ItemKind::Wood) > 0);
1330 }
1331
1332 #[test]
1333 fn hauled_items_go_to_the_highest_priority_stockpile_that_accepts_them() {
1334 let mut app = test_app(map_with_trees(&[]));
1335 set_goals(&mut app, &[]);
1336 put_beds(&mut app, 1);
1337 let map = app.world().resource::<WorldMap>().clone();
1338 {
1339 let mut zones = app.world_mut().resource_mut::<Zones>();
1340 zones.add_stockpile(&map, IVec2::new(3, 3), IVec2::new(4, 4));
1341 zones.add_stockpile(&map, IVec2::new(15, 15), IVec2::new(16, 16));
1342 zones.add_stockpile(&map, IVec2::new(15, 3), IVec2::new(16, 4));
1343 zones.update_region_rule(UVec2::new(15, 15), |rule| {
1344 rule.priority = crate::zones::StoragePriority::High
1345 });
1346 zones.update_region_rule(UVec2::new(3, 3), |rule| rule.toggle(Stock::Stone));
1347 }
1348 put_items(&mut app, UVec2::new(3, 3), ItemKind::Stone, 5);
1349 put_items(&mut app, UVec2::new(6, 6), ItemKind::Wood, 5);
1350 spawn_colonist(&mut app, UVec2::new(5, 5));
1351 run_ticks(&mut app, 60 * 30);
1352 let world = app.world_mut();
1353 let placed: Vec<(ItemKind, UVec2)> = world
1354 .query::<(&Item, &TilePos)>()
1355 .iter(world)
1356 .map(|(i, p)| (i.kind, p.0))
1357 .collect();
1358 let in_high = |t: UVec2| t.x >= 15 && t.y >= 15;
1359 assert!(placed.iter().all(|(_, t)| in_high(*t)), "{placed:?}");
1360 }
1361
1362 #[test]
1363 fn stored_items_move_up_to_a_higher_priority_stockpile() {
1364 let mut app = test_app(map_with_trees(&[]));
1365 set_goals(&mut app, &[]);
1366 put_beds(&mut app, 1);
1367 let map = app.world().resource::<WorldMap>().clone();
1368 {
1369 let mut zones = app.world_mut().resource_mut::<Zones>();
1370 zones.add_stockpile(&map, IVec2::new(3, 3), IVec2::new(4, 4));
1371 zones.add_stockpile(&map, IVec2::new(14, 14), IVec2::new(15, 15));
1372 }
1373 put_items(&mut app, UVec2::new(3, 3), ItemKind::Wood, 5);
1374 spawn_colonist(&mut app, UVec2::new(5, 5));
1375 run_ticks(&mut app, 60 * 5);
1376 let wood_at = |app: &mut App| {
1377 let world = app.world_mut();
1378 world
1379 .query::<(&Item, &TilePos)>()
1380 .iter(world)
1381 .map(|(_, p)| p.0)
1382 .collect::<Vec<_>>()
1383 };
1384 assert_eq!(wood_at(&mut app), vec![UVec2::new(3, 3)]);
1385 app.world_mut()
1386 .resource_mut::<Zones>()
1387 .update_region_rule(UVec2::new(14, 14), |rule| {
1388 rule.priority = crate::zones::StoragePriority::High
1389 });
1390 run_ticks(&mut app, 60 * 20);
1391 assert!(wood_at(&mut app).iter().all(|t| t.x >= 14 && t.y >= 14));
1392 }
1393
1394 // Cold colonists stop to warm up by a fire before taking new work; they would otherwise lose
1395 // mood, rest and food to the cold for as long as winter lasts.
1396 #[test]
1397 fn cold_colonists_warm_up_by_a_fire() {
1398 let mut app = test_app(WorldMap::filled(UVec2::splat(30), Terrain::Grass));
1399 let winter = 9 * 24 * crate::sim::TICKS_PER_HOUR;
1400 app.insert_resource(SimTick(winter));
1401 app.world_mut().spawn((
1402 Structure {
1403 kind: StructureKind::Campfire,
1404 },
1405 TilePos(UVec2::new(20, 20)),
1406 ));
1407 let colonist = spawn_colonist(&mut app, UVec2::new(2, 2));
1408 app.world_mut()
1409 .entity_mut(colonist)
1410 .insert(crate::climate::Warmth(-5.0));
1411 for _ in 0..300 {
1412 run_ticks(&mut app, 1);
1413 if job(&app, colonist).is_some() {
1414 break;
1415 }
1416 }
1417 let Some(Job {
1418 task: Task::MoveTo(spot),
1419 ..
1420 }) = job(&app, colonist)
1421 else {
1422 panic!("expected a move to the fire, got {:?}", job(&app, colonist));
1423 };
1424 assert!(spot.as_vec2().distance(Vec2::splat(20.0)) <= 3.0);
1425 }
1426
1427 // Tools are made from iron at a workbench when the colony wants more, and colonists without
1428 // tools pick one up; working with tools is half again as fast.
1429 #[test]
1430 fn colonists_make_tools_at_a_workbench_and_equip_them() {
1431 let mut app = test_app(WorldMap::filled(UVec2::splat(30), Terrain::Grass));
1432 set_goals(&mut app, &[(ItemKind::Tools, 1)]);
1433 app.world_mut().spawn((
1434 Structure {
1435 kind: StructureKind::Workbench,
1436 },
1437 TilePos(UVec2::new(15, 15)),
1438 ));
1439 app.world_mut().spawn((
1440 Item::new(ItemKind::Iron, TOOL_IRON),
1441 TilePos(UVec2::new(5, 5)),
1442 ));
1443 let colonist = spawn_colonist(&mut app, UVec2::new(2, 2));
1444 run_ticks(&mut app, 3000);
1445 assert_eq!(items_of(&mut app, ItemKind::Iron), 0);
1446 assert!(app.world().get::<Tool>(colonist).is_some());
1447 assert_eq!(tool_rate(2, true), 3);
1448 assert_eq!(tool_rate(1, true), 2);
1449 }
1450
1451 #[test]
1452 fn a_workbench_is_built_when_tools_are_wanted() {
1453 let mut app = test_app(WorldMap::filled(UVec2::splat(30), Terrain::Grass));
1454 set_goals(&mut app, &[(ItemKind::Tools, 1)]);
1455 put_wood(&mut app, UVec2::new(5, 5), 50);
1456 put_beds(&mut app, 1);
1457 app.world_mut().spawn((
1458 Item::new(ItemKind::Iron, TOOL_IRON),
1459 TilePos(UVec2::new(6, 5)),
1460 ));
1461 spawn_colonist(&mut app, UVec2::new(2, 2));
1462 run_ticks(&mut app, 4000);
1463 let world = app.world_mut();
1464 let benches = world
1465 .query::<&Structure>()
1466 .iter(world)
1467 .filter(|s| s.kind == StructureKind::Workbench)
1468 .count();
1469 assert_eq!(benches, 1);
1470 }
1471
1472 // A colonist's skill sets how fast they work, and working practises it.
1473 #[test]
1474 fn skilled_colonists_work_faster_and_practice_improves_them() {
1475 let ticks_to_chop = |level: u32| {
1476 let tree = UVec2::new(8, 5);
1477 let mut app = test_app(map_with_trees(&[tree]));
1478 let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
1479 app.world_mut()
1480 .entity_mut(colonist)
1481 .insert(crate::skills::Skills::uniform(level));
1482 let mut ticks = 0;
1483 while wood_on_map(&mut app) == 0 && ticks < 60 * 60 {
1484 run_ticks(&mut app, 10);
1485 ticks += 10;
1486 }
1487 let skills = app.world().get::<crate::skills::Skills>(colonist).unwrap();
1488 assert_ne!(skills, &crate::skills::Skills::uniform(level));
1489 ticks
1490 };
1491 assert!(ticks_to_chop(15) * 3 / 2 < ticks_to_chop(1));
1492 }
1493
1494 // Colonists leave work they are poor at to others: it comes after everything else they could do.
1495 #[test]
1496 fn colonists_try_work_they_are_poor_at_last() {
1497 let first_job = |construction: u32| {
1498 let mut app = test_app(map_with_trees(&[UVec2::new(8, 5)]));
1499 set_goals(&mut app, &[(ItemKind::Wood, 100)]);
1500 put_wood(&mut app, UVec2::new(4, 4), 20);
1501 put_beds(&mut app, 1);
1502 app.world_mut()
1503 .resource_mut::<Blueprints>()
1504 .0
1505 .insert(UVec2::new(3, 8), BuildKind::Floor);
1506 let colonist = spawn_colonist(&mut app, UVec2::new(2, 5));
1507 let mut skills = crate::skills::Skills::uniform(5);
1508 let level = crate::skills::xp_for(construction) - crate::skills::xp_for(5);
1509 skills.practice(crate::skills::Skill::Construction, level);
1510 app.world_mut().entity_mut(colonist).insert(skills);
1511 for _ in 0..300 {
1512 run_ticks(&mut app, 1);
1513 if let Some(job) = job(&app, colonist)
1514 && !matches!(job.task, Task::ExpandStockpile { .. } | Task::Haul { .. })
1515 {
1516 return job.task;
1517 }
1518 }
1519 panic!("no job");
1520 };
1521 assert!(matches!(first_job(8), Task::Build { .. }));
1522 assert!(matches!(
1523 first_job(1),
1524 Task::Work {
1525 kind: WorkKind::Chop,
1526 ..
1527 }
1528 ));
1529 }
1530
1531 // A wolf near the colony is everyone's business: healthy adults drop their work to go after it,
1532 // while children (and the badly hurt) run the other way.
1533 #[test]
1534 fn adults_fight_wolves_and_children_flee() {
1535 let mut app = test_app(WorldMap::filled(UVec2::splat(40), Terrain::Grass));
1536 let wolf = app
1537 .world_mut()
1538 .spawn((
1539 Animal {
1540 kind: AnimalKind::Wolf,
1541 },
1542 TilePos(UVec2::new(20, 20)),
1543 ))
1544 .id();
1545 let adult = spawn_colonist(&mut app, UVec2::new(15, 20));
1546 let child = spawn_colonist(&mut app, UVec2::new(22, 20));
1547 app.world_mut()
1548 .entity_mut(child)
1549 .insert(crate::family::Child { born: 0 });
1550 run_ticks(&mut app, 1);
1551 assert!(matches!(
1552 job(&app, adult).map(|j| j.task),
1553 Some(Task::Hunt { animal, .. }) if animal == wolf
1554 ));
1555 let route = &app.world().get::<Path>(child).unwrap().0;
1556 assert!(route.back().is_some_and(|end| end.x > 26));
1557 }
1558
1559 // Families cook at the hearth in their own home (another family's hearth is off limits, even if
1560 // it is closer), and the meals they make stay in the home instead of being hauled away.
1561 #[test]
1562 fn families_cook_at_their_own_hearth_and_keep_the_meals() {
1563 let mut app = test_app(WorldMap::filled(UVec2::splat(30), Terrain::Grass));
1564 set_goals(&mut app, &[(ItemKind::Meal, 5)]);
1565 let own = crate::family::Home::at(UVec2::new(15, 3));
1566 let other = crate::family::Home::at(UVec2::new(3, 3));
1567 app.world_mut()
1568 .resource_mut::<crate::family::Homes>()
1569 .by_family
1570 .extend([(1, own), (2, other)]);
1571 for home in [own, other] {
1572 app.world_mut().spawn((
1573 Structure {
1574 kind: StructureKind::Hearth,
1575 },
1576 TilePos(home.hearth_tile()),
1577 ));
1578 }
1579 app.world_mut()
1580 .spawn((Item::new(ItemKind::Berries, 20), TilePos(UVec2::new(9, 9))));
1581 let cook = spawn_colonist(&mut app, UVec2::new(9, 6));
1582 app.world_mut()
1583 .entity_mut(cook)
1584 .insert(crate::family::Family(1));
1585 let mut first = None;
1586 for _ in 0..600 {
1587 run_ticks(&mut app, 1);
1588 if let Some(job) = job(&app, cook)
1589 && !matches!(job.task, Task::ExpandStockpile { .. } | Task::Haul { .. })
1590 {
1591 first = Some(job.task);
1592 break;
1593 }
1594 }
1595 assert!(
1596 matches!(first, Some(Task::Craft { station, .. }) if station == own.hearth_tile()),
1597 "{first:?}"
1598 );
1599 let meal_home = UVec2::new(16, 4);
1600 app.world_mut()
1601 .spawn((Item::new(ItemKind::Meal, 1), TilePos(meal_home)));
1602 let snapshot: Vec<(Item, TilePos)> = vec![(Item::new(ItemKind::Meal, 1), TilePos(meal_home))];
1603 let world = app.world();
1604 let haul = find_haul(
1605 world.resource::<WorldMap>(),
1606 world.resource::<Zones>(),
1607 world.resource::<Reservations>(),
1608 &crate::pathfinding::ColonyReach::default(),
1609 world.resource::<crate::family::Homes>(),
1610 &snapshot,
1611 UVec2::new(9, 6),
1612 );
1613 assert_eq!(haul, None);
1614 }
1615
1616 // Wounded colonists rest in bed while a doctor fetches a herb and tends them; tending speeds up
1617 // healing for a day, more with a herb and a skilled doctor.
1618 #[test]
1619 fn doctors_tend_the_wounded_with_herbs() {
1620 let mut app = test_app(WorldMap::filled(UVec2::splat(30), Terrain::Grass));
1621 put_beds(&mut app, 2);
1622 app.world_mut()
1623 .spawn((Item::new(ItemKind::Herbs, 3), TilePos(UVec2::new(8, 8))));
1624 let patient = spawn_colonist(&mut app, UVec2::new(15, 15));
1625 app.world_mut()
1626 .entity_mut(patient)
1627 .insert(crate::threats::Health(0.3));
1628 let doctor = spawn_colonist(&mut app, UVec2::new(5, 5));
1629 run_ticks(&mut app, 60 * 30);
1630 let tended = app.world().get::<crate::threats::Tended>(patient);
1631 assert!(tended.is_some_and(|t| t.quality >= 1.0), "{tended:?}");
1632 assert_eq!(items_of(&mut app, ItemKind::Herbs), 2);
1633 assert!(app.world().get::<crate::threats::Tended>(doctor).is_none());
1634 assert!(matches!(
1635 job(&app, patient).map(|j| j.task),
1636 Some(Task::Sleep { .. })
1637 ));
1638 }
1639
1640 // Bridges over deep water are built from the shore outwards: each finished span is the footing
1641 // for the next, until the far bank can be reached.
1642 #[test]
1643 fn colonists_bridge_deep_water_from_the_shore() {
1644 let mut map = WorldMap::filled(UVec2::new(30, 20), Terrain::Grass);
1645 for y in 0..20 {
1646 for x in 12..16 {
1647 map.set(UVec2::new(x, y), Terrain::DeepWater);
1648 }
1649 }
1650 let mut app = test_app(map);
1651 put_beds(&mut app, 1);
1652 put_items(&mut app, UVec2::new(5, 5), ItemKind::Wood, 20);
1653 let mut blueprints = app.world_mut().resource_mut::<Blueprints>();
1654 for x in 12..16 {
1655 blueprints.0.insert(UVec2::new(x, 8), BuildKind::Bridge);
1656 }
1657 spawn_colonist(&mut app, UVec2::new(6, 8));
1658 run_ticks(&mut app, 60 * 60);
1659 let map = app.world().resource::<WorldMap>();
1660 for x in 12..16 {
1661 assert_eq!(map.get(IVec2::new(x, 8)), Some(Terrain::Bridge));
1662 }
1663 assert!(find_path(map, UVec2::new(6, 8), UVec2::new(20, 8)).is_some());
1664 }