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