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