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