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