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