src/jobs/tests.rs 30.9 K · 981 lines · raw · history

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