src/jobs/tests.rs 57.5 K · 1711 lines · raw · history

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