1 #include <algorithm>
2 #include <climits>
3 #include <functional>
4 #include <list>
5 #include <map>
6 #include <memory>
7 #include <set>
8 #include <sstream>
9 #include <string>
10 #include <utility>
11 #include <vector>
12
13 #include "activity_type.h"
14 #include "avatar.h"
15 #include "calendar.h"
16 #include "cata_utility.h"
17 #include "catch/catch.hpp"
18 #include "character.h"
19 #include "game.h"
20 #include "inventory.h"
21 #include "item.h"
22 #include "item_pocket.h"
23 #include "itype.h"
24 #include "map.h"
25 #include "map_helpers.h"
26 #include "npc.h"
27 #include "optional.h"
28 #include "pimpl.h"
29 #include "player_activity.h"
30 #include "player_helpers.h"
31 #include "point.h"
32 #include "recipe.h"
33 #include "recipe_dictionary.h"
34 #include "requirements.h"
35 #include "ret_val.h"
36 #include "skill.h"
37 #include "temp_crafting_inventory.h"
38 #include "type_id.h"
39 #include "value_ptr.h"
40
41 TEST_CASE( "recipe_subset" )
42 {
43 recipe_subset subset;
44
45 REQUIRE( subset.size() == 0 );
46 GIVEN( "a recipe of rum" ) {
47 const recipe *r = &recipe_id( "brew_rum" ).obj();
48
49 WHEN( "the recipe is included" ) {
50 subset.include( r );
51
52 THEN( "it's in the subset" ) {
53 CHECK( subset.size() == 1 );
54 CHECK( subset.contains( r ) );
55 }
56 THEN( "it has its default difficulty" ) {
57 CHECK( subset.get_custom_difficulty( r ) == r->difficulty );
58 }
59 THEN( "it's in the right category" ) {
60 const auto cat_recipes( subset.in_category( "CC_FOOD" ) );
61
62 CHECK( cat_recipes.size() == 1 );
63 CHECK( std::find( cat_recipes.begin(), cat_recipes.end(), r ) != cat_recipes.end() );
64 }
65 THEN( "it uses water" ) {
66 const auto comp_recipes( subset.of_component( itype_id( "water" ) ) );
67
68 CHECK( comp_recipes.size() == 1 );
69 CHECK( comp_recipes.find( r ) != comp_recipes.end() );
70 }
71 AND_WHEN( "the subset is cleared" ) {
72 subset.clear();
73
74 THEN( "it's no longer in the subset" ) {
75 CHECK( subset.size() == 0 );
76 CHECK_FALSE( subset.contains( r ) );
77 }
78 }
79 }
80 WHEN( "the recipe is included with higher difficulty" ) {
81 subset.include( r, r->difficulty + 1 );
82
83 THEN( "it's harder to perform" ) {
84 CHECK( subset.get_custom_difficulty( r ) == r->difficulty + 1 );
85 }
86 AND_WHEN( "it's included again with default difficulty" ) {
87 subset.include( r );
88
89 THEN( "it recovers its normal difficulty" ) {
90 CHECK( subset.get_custom_difficulty( r ) == r->difficulty );
91 }
92 }
93 AND_WHEN( "it's included again with lower difficulty" ) {
94 subset.include( r, r->difficulty - 1 );
95
96 THEN( "it becomes easier to perform" ) {
97 CHECK( subset.get_custom_difficulty( r ) == r->difficulty - 1 );
98 }
99 }
100 }
101 WHEN( "the recipe is included with lower difficulty" ) {
102 subset.include( r, r->difficulty - 1 );
103
104 THEN( "it's easier to perform" ) {
105 CHECK( subset.get_custom_difficulty( r ) == r->difficulty - 1 );
106 }
107 AND_WHEN( "it's included again with default difficulty" ) {
108 subset.include( r );
109
110 THEN( "it's still easier to perform" ) {
111 CHECK( subset.get_custom_difficulty( r ) == r->difficulty - 1 );
112 }
113 }
114 AND_WHEN( "it's included again with higher difficulty" ) {
115 subset.include( r, r->difficulty + 1 );
116
117 THEN( "it's still easier to perform" ) {
118 CHECK( subset.get_custom_difficulty( r ) == r->difficulty - 1 );
119 }
120 }
121 }
122 }
123 }
124
125 TEST_CASE( "available_recipes", "[recipes]" )
126 {
127 const recipe *r = &recipe_id( "magazine_battery_light_mod" ).obj();
128 avatar dummy;
129
130 REQUIRE( dummy.get_skill_level( r->skill_used ) == 0 );
131 REQUIRE_FALSE( dummy.knows_recipe( r ) );
132 REQUIRE( r->skill_used );
133
134 GIVEN( "a recipe that can be automatically learned" ) {
135 WHEN( "the player has lower skill" ) {
136 for( const std::pair<const skill_id, int> &skl : r->required_skills ) {
137 dummy.set_skill_level( skl.first, skl.second - 1 );
138 }
139
140 THEN( "he can't craft it" ) {
141 CHECK_FALSE( dummy.knows_recipe( r ) );
142 }
143 }
144 WHEN( "the player has just the skill that's required" ) {
145 dummy.set_skill_level( r->skill_used, r->difficulty );
146 for( const std::pair<const skill_id, int> &skl : r->required_skills ) {
147 dummy.set_skill_level( skl.first, skl.second );
148 }
149
150 THEN( "he can craft it now!" ) {
151 CHECK( dummy.knows_recipe( r ) );
152
153 AND_WHEN( "his skill rusts" ) {
154 dummy.set_skill_level( r->skill_used, 0 );
155 for( const std::pair<const skill_id, int> &skl : r->required_skills ) {
156 dummy.set_skill_level( skl.first, 0 );
157 }
158
159 THEN( "he still remembers how to craft it" ) {
160 CHECK( dummy.knows_recipe( r ) );
161 }
162 }
163 }
164 }
165 }
166
167 GIVEN( "an appropriate book" ) {
168 dummy.worn.push_back( item( "backpack" ) );
169 item &craftbook = dummy.i_add( item( "manual_electronics" ) );
170 REQUIRE( craftbook.is_book() );
171 REQUIRE_FALSE( craftbook.type->book->recipes.empty() );
172 REQUIRE_FALSE( dummy.knows_recipe( r ) );
173
174 WHEN( "the player read it and has an appropriate skill" ) {
175 dummy.do_read( craftbook );
176 dummy.set_skill_level( r->skill_used, 2 );
177 // Secondary skills are just set to be what the autolearn requires
178 // but the primary is not
179 for( const std::pair<const skill_id, int> &skl : r->required_skills ) {
180 dummy.set_skill_level( skl.first, skl.second );
181 }
182
183 AND_WHEN( "he searches for the recipe in the book" ) {
184 THEN( "he finds it!" ) {
185 // update the crafting inventory cache
186 dummy.moves++;
187 CHECK( dummy.get_recipes_from_books( dummy.crafting_inventory() ).contains( r ) );
188 }
189 THEN( "it's easier in the book" ) {
190 // update the crafting inventory cache
191 dummy.moves++;
192 CHECK( dummy.get_recipes_from_books( dummy.crafting_inventory() ).get_custom_difficulty( r ) == 2 );
193 }
194 THEN( "he still hasn't the recipe memorized" ) {
195 CHECK_FALSE( dummy.knows_recipe( r ) );
196 }
197 }
198 AND_WHEN( "he gets rid of the book" ) {
199 dummy.i_rem( &craftbook );
200
201 THEN( "he can't brew the recipe anymore" ) {
202 // update the crafting inventory cache
203 dummy.moves++;
204 CHECK_FALSE( dummy.get_recipes_from_books( dummy.crafting_inventory() ).contains( r ) );
205 }
206 }
207 }
208 }
209
210 GIVEN( "an eink pc with a sushi recipe" ) {
211 const recipe *r2 = &recipe_id( "sushi_rice" ).obj();
212 dummy.worn.push_back( item( "backpack" ) );
213 item &eink = dummy.i_add( item( "eink_tablet_pc" ) );
214 eink.set_var( "EIPC_RECIPES", ",sushi_rice," );
215 REQUIRE_FALSE( dummy.knows_recipe( r2 ) );
216
217 WHEN( "the player holds it and has an appropriate skill" ) {
218 dummy.set_skill_level( r2->skill_used, 2 );
219
220 AND_WHEN( "he searches for the recipe in the tablet" ) {
221 THEN( "he finds it!" ) {
222 // update the crafting inventory cache
223 dummy.moves++;
224 CHECK( dummy.get_recipes_from_books( dummy.crafting_inventory() ).contains( r2 ) );
225 }
226 THEN( "he still hasn't the recipe memorized" ) {
227 CHECK_FALSE( dummy.knows_recipe( r2 ) );
228 }
229 }
230 AND_WHEN( "he gets rid of the tablet" ) {
231 dummy.i_rem( &eink );
232
233 THEN( "he can't make the recipe anymore" ) {
234 // update the crafting inventory cache
235 dummy.moves++;
236 CHECK_FALSE( dummy.get_recipes_from_books( dummy.crafting_inventory() ).contains( r2 ) );
237 }
238 }
239 }
240 }
241 }
242
243 // This crashes subsequent testcases for some reason.
244 TEST_CASE( "crafting_with_a_companion", "[.]" )
245 {
246 const recipe *r = &recipe_id( "brew_mead" ).obj();
247 avatar dummy;
248
249 REQUIRE( dummy.get_skill_level( r->skill_used ) == 0 );
250 REQUIRE_FALSE( dummy.knows_recipe( r ) );
251 REQUIRE( r->skill_used );
252
253 GIVEN( "a companion who can help with crafting" ) {
254 standard_npc who( "helper" );
255
256 who.set_attitude( NPCATT_FOLLOW );
257 who.spawn_at_sm( tripoint_zero );
258
259 g->load_npcs();
260
261 CHECK( !dummy.in_vehicle );
262 dummy.setpos( who.pos() );
263 const auto helpers( dummy.get_crafting_helpers() );
264
265 REQUIRE( std::find( helpers.begin(), helpers.end(), &who ) != helpers.end() );
266 // update the crafting inventory cache
267 dummy.moves++;
268 REQUIRE_FALSE( dummy.get_available_recipes( dummy.crafting_inventory(), &helpers ).contains( r ) );
269 REQUIRE_FALSE( who.knows_recipe( r ) );
270
271 WHEN( "you have the required skill" ) {
272 dummy.set_skill_level( r->skill_used, r->difficulty );
273
274 AND_WHEN( "he knows the recipe" ) {
275 who.learn_recipe( r );
276
277 THEN( "he helps you" ) {
278 // update the crafting inventory cache
279 dummy.moves++;
280 CHECK( dummy.get_available_recipes( dummy.crafting_inventory(), &helpers ).contains( r ) );
281 }
282 }
283 AND_WHEN( "he has the cookbook in his inventory" ) {
284 item &cookbook = who.i_add( item( "brewing_cookbook" ) );
285
286 REQUIRE( cookbook.is_book() );
287 REQUIRE_FALSE( cookbook.type->book->recipes.empty() );
288
289 THEN( "he shows it to you" ) {
290 // update the crafting inventory cache
291 dummy.moves++;
292 CHECK( dummy.get_available_recipes( dummy.crafting_inventory(), &helpers ).contains( r ) );
293 }
294 }
295 }
296 }
297 }
298
give_tools(const std::vector<item> & tools)299 static void give_tools( const std::vector<item> &tools )
300 {
301 Character &player_character = get_player_character();
302 player_character.worn.clear();
303 player_character.calc_encumbrance();
304 player_character.inv->clear();
305 player_character.remove_weapon();
306 const item backpack( "debug_backpack" );
307 player_character.worn.push_back( backpack );
308
309 for( const item &gear : tools ) {
310 player_character.i_add( gear );
311 }
312 }
313
grant_skills_to_character(Character & you,const recipe & r)314 static void grant_skills_to_character( Character &you, const recipe &r )
315 {
316 // Ensure adequate skill for all "required" skills
317 for( const std::pair<const skill_id, int> &skl : r.required_skills ) {
318 you.set_skill_level( skl.first, skl.second );
319 }
320 // and just in case "used" skill difficulty is higher, set that too
321 you.set_skill_level( r.skill_used, std::max( r.difficulty,
322 you.get_skill_level( r.skill_used ) ) );
323 }
324
prep_craft(const recipe_id & rid,const std::vector<item> & tools,bool expect_craftable)325 static void prep_craft( const recipe_id &rid, const std::vector<item> &tools,
326 bool expect_craftable )
327 {
328 clear_avatar();
329 clear_map();
330
331 const tripoint test_origin( 60, 60, 0 );
332 Character &player_character = get_player_character();
333 player_character.toggle_trait( trait_id( "DEBUG_CNF" ) );
334 player_character.setpos( test_origin );
335 const recipe &r = rid.obj();
336 grant_skills_to_character( player_character, r );
337
338 give_tools( tools );
339 player_character.moves--;
340 const inventory &crafting_inv = player_character.crafting_inventory();
341
342 bool can_craft_with_crafting_inv = r.deduped_requirements().can_make_with_inventory(
343 crafting_inv, r.get_component_filter() );
344 REQUIRE( can_craft_with_crafting_inv == expect_craftable );
345 bool can_craft_with_temp_inv = r.deduped_requirements().can_make_with_inventory(
346 temp_crafting_inventory( crafting_inv ), r.get_component_filter() );
347 REQUIRE( can_craft_with_temp_inv == expect_craftable );
348 }
349
350 static time_point midnight = calendar::turn_zero + 0_hours;
351 static time_point midday = calendar::turn_zero + 12_hours;
352
set_time(const time_point & time)353 static void set_time( const time_point &time )
354 {
355 calendar::turn = time;
356 g->reset_light_level();
357 int z = get_player_character().posz();
358 map &here = get_map();
359 here.update_visibility_cache( z );
360 here.invalidate_map_cache( z );
361 here.build_map_cache( z );
362 }
363
364 // This tries to actually run the whole craft activity, which is more thorough,
365 // but slow
actually_test_craft(const recipe_id & rid,int interrupt_after_turns,int skill_level=-1)366 static int actually_test_craft( const recipe_id &rid, int interrupt_after_turns,
367 int skill_level = -1 )
368 {
369 set_time( midday ); // Ensure light for crafting
370 avatar &player_character = get_avatar();
371 const recipe &rec = rid.obj();
372 REQUIRE( player_character.morale_crafting_speed_multiplier( rec ) == 1.0 );
373 REQUIRE( player_character.lighting_craft_speed_multiplier( rec ) == 1.0 );
374 REQUIRE( !player_character.activity );
375
376 // This really shouldn't be needed, but for some reason the tests fail for mingw builds without it
377 player_character.learn_recipe( &rec );
378 REQUIRE( player_character.has_recipe( &rec, player_character.crafting_inventory(),
379 player_character.get_crafting_helpers() ) != -1 );
380 player_character.remove_weapon();
381 REQUIRE( !player_character.is_armed() );
382 player_character.make_craft( rid, 1 );
383 REQUIRE( player_character.activity );
384 REQUIRE( player_character.activity.id() == activity_id( "ACT_CRAFT" ) );
385 int turns = 0;
386 while( player_character.activity.id() == activity_id( "ACT_CRAFT" ) ) {
387 if( turns >= interrupt_after_turns ||
388 ( skill_level >= 0 && player_character.get_skill_level( rec.skill_used ) > skill_level ) ) {
389 set_time( midnight ); // Kill light to interrupt crafting
390 }
391 ++turns;
392 player_character.moves = 100;
393 player_character.activity.do_turn( player_character );
394 if( turns % 60 == 0 ) {
395 player_character.update_mental_focus();
396 }
397 }
398 return turns;
399 }
400
401 TEST_CASE( "UPS shows as a crafting component", "[crafting][ups]" )
402 {
403 avatar dummy;
404 clear_character( dummy );
405 dummy.worn.push_back( item( "backpack" ) );
406 item &ups = dummy.i_add( item( "UPS_off", calendar::turn_zero, 500 ) );
407 REQUIRE( dummy.has_item( ups ) );
408 REQUIRE( ups.charges == 500 );
409 REQUIRE( dummy.charges_of( itype_id( "UPS_off" ) ) == 500 );
410 REQUIRE( dummy.charges_of( itype_id( "UPS" ) ) == 500 );
411 }
412
413 TEST_CASE( "tools use charge to craft", "[crafting][charge]" )
414 {
415 std::vector<item> tools;
416
417 GIVEN( "recipe and required tools/materials" ) {
418 recipe_id carver( "carver_off" );
419 // Uses fabrication skill
420 // Requires electronics 3
421 // Difficulty 4
422 // Learned from advanced_electronics or textbook_electronics
423
424 // Tools needed:
425 tools.emplace_back( "screwdriver" );
426 tools.emplace_back( "mold_plastic" );
427
428 // Materials needed
429 tools.insert( tools.end(), 10, item( "solder_wire" ) );
430 tools.insert( tools.end(), 6, item( "plastic_chunk" ) );
431 tools.insert( tools.end(), 2, item( "blade" ) );
432 tools.insert( tools.end(), 5, item( "cable" ) );
433 tools.emplace_back( "motor_tiny" );
434 tools.emplace_back( "power_supply" );
435 tools.emplace_back( "scrap" );
436
437 // Charges needed to craft:
438 // - 10 charges of soldering iron
439 // - 10 charges of surface heat
440
441 WHEN( "each tool has enough charges" ) {
442 item hotplate = tool_with_ammo( "hotplate", 20 );
443 REQUIRE( hotplate.ammo_remaining() == 20 );
444 tools.push_back( hotplate );
445 item soldering = tool_with_ammo( "soldering_iron", 20 );
446 REQUIRE( soldering.ammo_remaining() == 20 );
447 tools.push_back( soldering );
448
449 THEN( "crafting succeeds, and uses charges from each tool" ) {
450 prep_craft( recipe_id( "carver_off" ), tools, true );
451 int turns = actually_test_craft( recipe_id( "carver_off" ), INT_MAX );
452 CAPTURE( turns );
453 CHECK( get_remaining_charges( "hotplate" ) == 10 );
454 CHECK( get_remaining_charges( "soldering_iron" ) == 10 );
455 }
456 }
457
458 WHEN( "multiple tools have enough combined charges" ) {
459 tools.insert( tools.end(), 2, tool_with_ammo( "hotplate", 5 ) );
460 tools.insert( tools.end(), 2, tool_with_ammo( "soldering_iron", 5 ) );
461
462 THEN( "crafting succeeds, and uses charges from multiple tools" ) {
463 prep_craft( recipe_id( "carver_off" ), tools, true );
464 actually_test_craft( recipe_id( "carver_off" ), INT_MAX );
465 CHECK( get_remaining_charges( "hotplate" ) == 0 );
466 CHECK( get_remaining_charges( "soldering_iron" ) == 0 );
467 }
468 }
469
470 WHEN( "UPS-modded tools have enough charges" ) {
471 item hotplate( "hotplate" );
472 hotplate.put_in( item( "battery_ups" ), item_pocket::pocket_type::MOD );
473 tools.push_back( hotplate );
474 item soldering_iron( "soldering_iron" );
475 soldering_iron.put_in( item( "battery_ups" ), item_pocket::pocket_type::MOD );
476 tools.push_back( soldering_iron );
477 item UPS( "UPS_off" );
478 item UPS_mag( UPS.magazine_default() );
479 UPS_mag.ammo_set( UPS_mag.ammo_default(), 500 );
480 UPS.put_in( UPS_mag, item_pocket::pocket_type::MAGAZINE_WELL );
481 tools.emplace_back( UPS );
482
483 THEN( "crafting succeeds, and uses charges from the UPS" ) {
484 prep_craft( recipe_id( "carver_off" ), tools, true );
485 actually_test_craft( recipe_id( "carver_off" ), INT_MAX );
486 CHECK( get_remaining_charges( "hotplate" ) == 0 );
487 CHECK( get_remaining_charges( "soldering_iron" ) == 0 );
488 CHECK( get_remaining_charges( "UPS_off" ) == 480 );
489 }
490 }
491
492 WHEN( "UPS-modded tools do not have enough charges" ) {
493 item hotplate( "hotplate" );
494 hotplate.put_in( item( "battery_ups" ), item_pocket::pocket_type::MOD );
495 tools.push_back( hotplate );
496 item soldering_iron( "soldering_iron" );
497 soldering_iron.put_in( item( "battery_ups" ), item_pocket::pocket_type::MOD );
498 tools.push_back( soldering_iron );
499 tools.emplace_back( "UPS_off", calendar::turn_zero, 10 );
500
501 THEN( "crafting fails, and no charges are used" ) {
502 prep_craft( recipe_id( "carver_off" ), tools, false );
503 CHECK( get_remaining_charges( "UPS_off" ) == 10 );
504 }
505 }
506 }
507 }
508
509 TEST_CASE( "tool_use", "[crafting][tool]" )
510 {
511 SECTION( "clean_water" ) {
512 std::vector<item> tools;
513 tools.push_back( tool_with_ammo( "hotplate", 20 ) );
514 item plastic_bottle( "bottle_plastic" );
515 plastic_bottle.put_in(
516 item( "water", calendar::turn_zero, 2 ), item_pocket::pocket_type::CONTAINER );
517 tools.push_back( plastic_bottle );
518 tools.emplace_back( "pot" );
519
520 // Can't actually test crafting here since crafting a liquid currently causes a ui prompt
521 prep_craft( recipe_id( "water_clean" ), tools, true );
522 }
523 SECTION( "clean_water_in_loaded_mess_kit" ) {
524 std::vector<item> tools;
525 tools.push_back( tool_with_ammo( "hotplate", 20 ) );
526 item plastic_bottle( "bottle_plastic" );
527 plastic_bottle.put_in(
528 item( "water", calendar::turn_zero, 2 ), item_pocket::pocket_type::CONTAINER );
529 tools.push_back( plastic_bottle );
530 tools.push_back( tool_with_ammo( "mess_kit", 20 ) );
531
532 // Can't actually test crafting here since crafting a liquid currently causes a ui prompt
533 prep_craft( recipe_id( "water_clean" ), tools, true );
534 }
535 SECTION( "clean_water_in_loaded_survivor_mess_kit" ) {
536 std::vector<item> tools;
537 tools.push_back( tool_with_ammo( "hotplate", 20 ) );
538 item plastic_bottle( "bottle_plastic" );
539 plastic_bottle.put_in(
540 item( "water", calendar::turn_zero, 2 ), item_pocket::pocket_type::CONTAINER );
541 tools.push_back( plastic_bottle );
542 tools.push_back( tool_with_ammo( "survivor_mess_kit", 20 ) );
543
544 // Can't actually test crafting here since crafting a liquid currently causes a ui prompt
545 prep_craft( recipe_id( "water_clean" ), tools, true );
546 }
547 SECTION( "clean_water_in_occupied_cooking_vessel" ) {
548 std::vector<item> tools;
549 tools.push_back( tool_with_ammo( "hotplate", 20 ) );
550 item plastic_bottle( "bottle_plastic" );
551 plastic_bottle.put_in(
552 item( "water", calendar::turn_zero, 2 ), item_pocket::pocket_type::CONTAINER );
553 tools.push_back( plastic_bottle );
554 item jar( "jar_glass_sealed" );
555 // If it's not watertight the water will spill.
556 REQUIRE( jar.is_watertight_container() );
557 jar.put_in( item( "water", calendar::turn_zero, 2 ), item_pocket::pocket_type::CONTAINER );
558 tools.push_back( jar );
559
560 prep_craft( recipe_id( "water_clean" ), tools, false );
561 }
562 }
563
564 // Resume the first in progress craft found in the player's inventory
resume_craft()565 static int resume_craft()
566 {
567 avatar &player_character = get_avatar();
568 std::vector<item *> crafts = player_character.items_with( []( const item & itm ) {
569 return itm.is_craft();
570 } );
571 REQUIRE( crafts.size() == 1 );
572 item *craft = crafts.front();
573 set_time( midday ); // Ensure light for crafting
574 REQUIRE( player_character.crafting_speed_multiplier( *craft, cata::nullopt ) == 1.0 );
575 REQUIRE( !player_character.activity );
576 player_character.use( player_character.get_item_position( craft ) );
577 REQUIRE( player_character.activity );
578 REQUIRE( player_character.activity.id() == activity_id( "ACT_CRAFT" ) );
579 int turns = 0;
580 while( player_character.activity.id() == activity_id( "ACT_CRAFT" ) ) {
581 ++turns;
582 player_character.moves = 100;
583 player_character.activity.do_turn( player_character );
584 if( turns % 60 == 0 ) {
585 player_character.update_mental_focus();
586 }
587 }
588 return turns;
589 }
590
verify_inventory(const std::vector<std::string> & has,const std::vector<std::string> & hasnt)591 static void verify_inventory( const std::vector<std::string> &has,
592 const std::vector<std::string> &hasnt )
593 {
594 std::ostringstream os;
595 os << "Inventory:\n";
596 Character &player_character = get_player_character();
597 for( const item *i : player_character.inv_dump() ) {
598 os << " " << i->typeId().str() << " (" << i->charges << ")\n";
599 }
600 os << "Wielded:\n" << player_character.weapon.tname() << "\n";
601 INFO( os.str() );
602 for( const std::string &i : has ) {
603 INFO( "expecting " << i );
604 const bool has_item =
605 player_has_item_of_type( i ) || player_character.weapon.type->get_id() == itype_id( i );
606 REQUIRE( has_item );
607 }
608 for( const std::string &i : hasnt ) {
609 INFO( "not expecting " << i );
610 const bool hasnt_item =
611 !player_has_item_of_type( i ) && !( player_character.weapon.type->get_id() == itype_id( i ) );
612 REQUIRE( hasnt_item );
613 }
614 }
615
616 TEST_CASE( "total crafting time with or without interruption", "[crafting][time][resume]" )
617 {
618 GIVEN( "a recipe and all the required tools and materials to craft it" ) {
619 recipe_id test_recipe( "razor_shaving" );
620 int expected_time_taken = test_recipe->batch_time( get_player_character(), 1, 1, 0 );
621 int expected_turns_taken = divide_round_up( expected_time_taken, 100 );
622
623 std::vector<item> tools;
624 tools.emplace_back( "pockknife" );
625
626 // Will interrupt after 2 turns, so craft needs to take at least that long
627 REQUIRE( expected_turns_taken > 2 );
628 int actual_turns_taken;
629
630 WHEN( "crafting begins, and continues until the craft is completed" ) {
631 tools.emplace_back( "razor_blade", calendar::turn_zero, 1 );
632 tools.emplace_back( "plastic_chunk", calendar::turn_zero, 1 );
633 prep_craft( test_recipe, tools, true );
634 actual_turns_taken = actually_test_craft( test_recipe, INT_MAX );
635
636 THEN( "it should take the expected number of turns" ) {
637 CHECK( actual_turns_taken == expected_turns_taken );
638
639 AND_THEN( "the finished item should be in the inventory" ) {
640 verify_inventory( { "razor_shaving" }, { "razor_blade" } );
641 }
642 }
643 }
644
645 WHEN( "crafting begins, but is interrupted after 2 turns" ) {
646 tools.emplace_back( "razor_blade", calendar::turn_zero, 1 );
647 tools.emplace_back( "plastic_chunk", calendar::turn_zero, 1 );
648 prep_craft( test_recipe, tools, true );
649 actual_turns_taken = actually_test_craft( test_recipe, 2 );
650 REQUIRE( actual_turns_taken == 3 );
651
652 THEN( "the in-progress craft should be in the inventory" ) {
653 verify_inventory( { "craft" }, { "razor_shaving" } );
654
655 AND_WHEN( "crafting resumes until the craft is finished" ) {
656 actual_turns_taken = resume_craft();
657
658 THEN( "it should take the remaining number of turns" ) {
659 CHECK( actual_turns_taken == expected_turns_taken - 2 );
660
661 AND_THEN( "the finished item should be in the inventory" ) {
662 verify_inventory( { "razor_shaving" }, { "craft" } );
663 }
664 }
665 }
666 }
667 }
668 }
669 }
670
671 static std::map<quality_id, itype_id> quality_to_tool = {{
672 { quality_id( "CUT" ), itype_id( "pockknife" ) }, { quality_id( "SEW" ), itype_id( "needle_bone" ) }, { quality_id( "LEATHER_AWL" ), itype_id( "awl_bone" ) }, { quality_id( "ANVIL" ), itype_id( "anvil" ) }, { quality_id( "HAMMER" ), itype_id( "hammer" ) }, { quality_id( "SAW_M" ), itype_id( "hacksaw" ) }, { quality_id( "CHISEL" ), itype_id( "chisel" ) }
673 }
674 };
675
grant_proficiencies_to_character(Character & you,const recipe & r,bool grant_optional_proficiencies)676 static void grant_proficiencies_to_character( Character &you, const recipe &r,
677 bool grant_optional_proficiencies )
678 {
679 if( grant_optional_proficiencies ) {
680 for( const proficiency_id &prof : r.assist_proficiencies() ) {
681 you.add_proficiency( prof, true );
682 }
683 } else {
684 REQUIRE( you.known_proficiencies().empty() );
685 }
686 for( const proficiency_id &prof : r.required_proficiencies() ) {
687 you.add_proficiency( prof, true );
688 }
689 }
690
test_skill_progression(const recipe_id & test_recipe,int expected_turns_taken,int morale_level,bool grant_optional_proficiencies)691 static void test_skill_progression( const recipe_id &test_recipe, int expected_turns_taken,
692 int morale_level, bool grant_optional_proficiencies )
693 {
694 Character &you = get_player_character();
695 int actual_turns_taken = 0;
696 const recipe &r = *test_recipe;
697 const skill_id skill_used = r.skill_used;
698 // Do we need to check required skills too?
699 const int starting_skill_level = r.difficulty;
700 std::vector<item> tools;
701 const requirement_data &req = r.simple_requirements();
702 for( const std::vector<tool_comp> &tool_list : req.get_tools() ) {
703 for( const tool_comp &tool : tool_list ) {
704 tools.push_back( tool_with_ammo( tool.type.str(), tool.count ) );
705 break;
706 }
707 }
708 for( const std::vector<quality_requirement> &qualities : req.get_qualities() ) {
709 for( const quality_requirement &quality : qualities ) {
710 const auto &tool_id = quality_to_tool.find( quality.type );
711 CAPTURE( quality.type.str() );
712 REQUIRE( tool_id != quality_to_tool.end() );
713 tools.emplace_back( tool_id->second );
714 break;
715 }
716 }
717 for( const std::vector<item_comp> &components : req.get_components() ) {
718 for( const item_comp &component : components ) {
719 for( int i = 0; i < component.count * 2; ++i ) {
720 tools.emplace_back( component.type );
721 }
722 break;
723 }
724 }
725
726 prep_craft( test_recipe, tools, true );
727 grant_proficiencies_to_character( you, r, grant_optional_proficiencies );
728 you.set_focus( 100 );
729 if( morale_level != 0 ) {
730 you.add_morale( morale_type( "morale_food_good" ), morale_level );
731 REQUIRE( you.get_morale_level() == morale_level );
732 }
733 SkillLevel &level = you.get_skill_level_object( skill_used );
734 int previous_exercise = level.exercise( true );
735 do {
736 actual_turns_taken += actually_test_craft( test_recipe, INT_MAX, starting_skill_level );
737 if( you.get_skill_level( skill_used ) == starting_skill_level ) {
738 int new_exercise = level.exercise( true );
739 REQUIRE( previous_exercise < new_exercise );
740 previous_exercise = new_exercise;
741 }
742 give_tools( tools );
743 } while( you.get_skill_level( skill_used ) == starting_skill_level );
744 CAPTURE( test_recipe.str() );
745 CAPTURE( expected_turns_taken );
746 CAPTURE( grant_optional_proficiencies );
747 CHECK( you.get_skill_level( skill_used ) == starting_skill_level + 1 );
748 CHECK( actual_turns_taken == expected_turns_taken );
749 }
750
751 TEST_CASE( "crafting_skill_gain", "[skill],[crafting],[slow]" )
752 {
753 SECTION( "lvl 0 -> 1" ) {
754 GIVEN( "nominal morale" ) {
755 test_skill_progression( recipe_id( "blanket" ), 175, 0, false );
756 test_skill_progression( recipe_id( "blanket" ), 175, 0, true );
757 }
758 GIVEN( "high morale" ) {
759 test_skill_progression( recipe_id( "blanket" ), 173, 50, false );
760 test_skill_progression( recipe_id( "blanket" ), 173, 50, true );
761 }
762 GIVEN( "very high morale" ) {
763 test_skill_progression( recipe_id( "blanket" ), 173, 100, false );
764 test_skill_progression( recipe_id( "blanket" ), 173, 100, true );
765 }
766 }
767 SECTION( "lvl 1 -> 2" ) {
768 GIVEN( "nominal morale" ) {
769 test_skill_progression( recipe_id( "2byarm_guard" ), 2140, 0, false );
770 test_skill_progression( recipe_id( "2byarm_guard" ), 2140, 0, true );
771 }
772 GIVEN( "high morale" ) {
773 test_skill_progression( recipe_id( "2byarm_guard" ), 1842, 50, false );
774 test_skill_progression( recipe_id( "2byarm_guard" ), 1842, 50, true );
775 }
776 GIVEN( "very high morale" ) {
777 test_skill_progression( recipe_id( "2byarm_guard" ), 1737, 100, false );
778 test_skill_progression( recipe_id( "2byarm_guard" ), 1737, 100, true );
779 }
780 }
781 SECTION( "lvl 2 -> lvl 3" ) {
782 GIVEN( "nominal morale" ) {
783 test_skill_progression( recipe_id( "vambrace_larmor" ), 12127, 0, false );
784 test_skill_progression( recipe_id( "vambrace_larmor" ), 6291, 0, true );
785 }
786 GIVEN( "high morale" ) {
787 test_skill_progression( recipe_id( "vambrace_larmor" ), 9919, 50, false );
788 test_skill_progression( recipe_id( "vambrace_larmor" ), 5230, 50, true );
789 }
790 GIVEN( "very high morale" ) {
791 test_skill_progression( recipe_id( "vambrace_larmor" ), 9160, 100, false );
792 test_skill_progression( recipe_id( "vambrace_larmor" ), 4836, 100, true );
793 }
794 }
795 SECTION( "lvl 3 -> lvl 4" ) {
796 GIVEN( "nominal morale" ) {
797 test_skill_progression( recipe_id( "armguard_larmor" ), 22711, 0, false );
798 test_skill_progression( recipe_id( "armguard_larmor" ), 12138, 0, true );
799 }
800 GIVEN( "high morale" ) {
801 test_skill_progression( recipe_id( "armguard_larmor" ), 18436, 50, false );
802 test_skill_progression( recipe_id( "armguard_larmor" ), 10003, 50, true );
803 }
804 GIVEN( "very high morale" ) {
805 test_skill_progression( recipe_id( "armguard_larmor" ), 16951, 100, false );
806 test_skill_progression( recipe_id( "armguard_larmor" ), 9203, 100, true );
807 }
808 }
809 SECTION( "lvl 4 -> 5" ) {
810 GIVEN( "nominal morale" ) {
811 test_skill_progression( recipe_id( "armguard_metal" ), 37537, 0, false );
812 test_skill_progression( recipe_id( "armguard_metal" ), 19638, 0, true );
813 }
814 GIVEN( "high morale" ) {
815 test_skill_progression( recipe_id( "armguard_metal" ), 29872, 50, false );
816 test_skill_progression( recipe_id( "armguard_metal" ), 16125, 50, true );
817 }
818 GIVEN( "very high morale" ) {
819 test_skill_progression( recipe_id( "armguard_metal" ), 27472, 100, false );
820 test_skill_progression( recipe_id( "armguard_metal" ), 14805, 100, true );
821 }
822 }
823 SECTION( "lvl 5 -> 6" ) {
824 GIVEN( "nominal morale" ) {
825 test_skill_progression( recipe_id( "armguard_chitin" ), 100755, 0, false );
826 test_skill_progression( recipe_id( "armguard_chitin" ), 28817, 0, true );
827 }
828 GIVEN( "high morale" ) {
829 test_skill_progression( recipe_id( "armguard_chitin" ), 79969, 50, false );
830 test_skill_progression( recipe_id( "armguard_chitin" ), 23613, 50, true );
831 }
832 GIVEN( "very high morale" ) {
833 test_skill_progression( recipe_id( "armguard_chitin" ), 73091, 100, false );
834 test_skill_progression( recipe_id( "armguard_chitin" ), 21651, 100, true );
835 }
836 }
837 SECTION( "lvl 6 -> 7" ) {
838 GIVEN( "nominal morale" ) {
839 test_skill_progression( recipe_id( "armguard_acidchitin" ), 137741, 0, false );
840 test_skill_progression( recipe_id( "armguard_acidchitin" ), 39651, 0, true );
841 }
842 GIVEN( "high morale" ) {
843 test_skill_progression( recipe_id( "armguard_acidchitin" ), 109241, 50, false );
844 test_skill_progression( recipe_id( "armguard_acidchitin" ), 32470, 50, true );
845 }
846 GIVEN( "very high morale" ) {
847 test_skill_progression( recipe_id( "armguard_acidchitin" ), 99810, 100, false );
848 test_skill_progression( recipe_id( "armguard_acidchitin" ), 29755, 100, true );
849 }
850 }
851 SECTION( "lvl 7 -> 8" ) {
852 GIVEN( "nominal morale" ) {
853 test_skill_progression( recipe_id( "armguard_lightplate" ), 227023, 0, false );
854 test_skill_progression( recipe_id( "armguard_lightplate" ), 52138, 0, true );
855 }
856 GIVEN( "high morale" ) {
857 test_skill_progression( recipe_id( "armguard_lightplate" ), 178077, 50, false );
858 test_skill_progression( recipe_id( "armguard_lightplate" ), 42656, 50, true );
859 }
860 GIVEN( "very high morale" ) {
861 test_skill_progression( recipe_id( "armguard_lightplate" ), 162603, 100, false );
862 test_skill_progression( recipe_id( "armguard_lightplate" ), 39078, 100, true );
863 }
864 }
865 SECTION( "lvl 8 -> 9" ) {
866 GIVEN( "nominal morale" ) {
867 test_skill_progression( recipe_id( "helmet_scavenger" ), 143457, 0, false );
868 test_skill_progression( recipe_id( "helmet_scavenger" ), 66243, 0, true );
869 }
870 GIVEN( "high morale" ) {
871 test_skill_progression( recipe_id( "helmet_scavenger" ), 114137, 50, false );
872 test_skill_progression( recipe_id( "helmet_scavenger" ), 54170, 50, true );
873 }
874 GIVEN( "very high morale" ) {
875 test_skill_progression( recipe_id( "helmet_scavenger" ), 105128, 100, false );
876 test_skill_progression( recipe_id( "helmet_scavenger" ), 49609, 100, true );
877 }
878 }
879 SECTION( "lvl 9 -> 10" ) {
880 GIVEN( "nominal morale" ) {
881 test_skill_progression( recipe_id( "helmet_kabuto" ), 280299, 0, false );
882 test_skill_progression( recipe_id( "helmet_kabuto" ), 82489, 0, true );
883 }
884 GIVEN( "high morale" ) {
885 test_skill_progression( recipe_id( "helmet_kabuto" ), 221512, 50, false );
886 test_skill_progression( recipe_id( "helmet_kabuto" ), 67364, 50, true );
887 }
888 GIVEN( "very high morale" ) {
889 test_skill_progression( recipe_id( "helmet_kabuto" ), 202846, 100, false );
890 test_skill_progression( recipe_id( "helmet_kabuto" ), 61584, 100, true );
891 }
892 }
893 SECTION( "long craft with proficiency delays" ) {
894 GIVEN( "nominal morale" ) {
895 test_skill_progression( recipe_id( "longbow" ), 71187, 0, false );
896 test_skill_progression( recipe_id( "longbow" ), 28804, 0, true );
897 }
898 GIVEN( "high morale" ) {
899 test_skill_progression( recipe_id( "longbow" ), 56945, 50, false );
900 test_skill_progression( recipe_id( "longbow" ), 23608, 50, true );
901 }
902 GIVEN( "very high morale" ) {
903 test_skill_progression( recipe_id( "longbow" ), 52222, 100, false );
904 test_skill_progression( recipe_id( "longbow" ), 21651, 100, true );
905 }
906 }
907 SECTION( "extremely short craft" ) {
908 GIVEN( "nominal morale" ) {
909 test_skill_progression( recipe_id( "fishing_hook_basic" ), 174, 0, false );
910 test_skill_progression( recipe_id( "fishing_hook_basic" ), 174, 0, true );
911 }
912 GIVEN( "high morale" ) {
913 test_skill_progression( recipe_id( "fishing_hook_basic" ), 172, 50, false );
914 test_skill_progression( recipe_id( "fishing_hook_basic" ), 172, 50, true );
915 }
916 GIVEN( "very high morale" ) {
917 test_skill_progression( recipe_id( "fishing_hook_basic" ), 172, 100, false );
918 test_skill_progression( recipe_id( "fishing_hook_basic" ), 172, 100, true );
919 }
920 }
921 }
922
923 TEST_CASE( "check-tool_qualities" )
924 {
925 CHECK( tool_with_ammo( "mess_kit", 20 ).has_quality( quality_id( "BOIL" ), 2, 1 ) );
926 CHECK( tool_with_ammo( "survivor_mess_kit", 20 ).has_quality( quality_id( "BOIL" ), 2, 1 ) );
927 CHECK( tool_with_ammo( "survivor_mess_kit", 20 ).get_quality( quality_id( "BOIL" ) ) > 0 );
928 }
929
930 TEST_CASE( "book_proficiency_mitigation", "[crafting][proficiency]" )
931 {
932 GIVEN( "a recipe with required proficiencies" ) {
933 clear_avatar();
934 clear_map();
935 const recipe &test_recipe = recipe_id( "leather_belt" ).obj();
936
937 grant_skills_to_character( get_player_character(), test_recipe );
938 int unmitigated_time_taken = test_recipe.batch_time( get_player_character(), 1, 1, 0 );
939
940 WHEN( "player has a book mitigating lack of proficiency" ) {
941 std::vector<item> books;
942 books.emplace_back( "manual_tailor" );
943 give_tools( books );
944 get_player_character().invalidate_crafting_inventory();
945 int mitigated_time_taken = test_recipe.batch_time( get_player_character(), 1, 1, 0 );
946 THEN( "it takes less time to craft the recipe" ) {
947 CHECK( mitigated_time_taken < unmitigated_time_taken );
948 }
949 AND_WHEN( "player acquires missing proficiencies" ) {
950 grant_proficiencies_to_character( get_player_character(), test_recipe, true );
951 int proficient_time_taken = test_recipe.batch_time( get_player_character(), 1, 1, 0 );
952 THEN( "it takes even less time to craft the recipe" ) {
953 CHECK( proficient_time_taken < mitigated_time_taken );
954 }
955 }
956 }
957 }
958 }
959