1 #include "recipe.h"
2 
3 #include <algorithm>
4 #include <cmath>
5 #include <limits>
6 #include <memory>
7 #include <numeric>
8 #include <sstream>
9 
10 #include "assign.h"
11 #include "cached_options.h"
12 #include "calendar.h"
13 #include "cata_utility.h"
14 #include "character.h"
15 #include "color.h"
16 #include "debug.h"
17 #include "enum_traits.h"
18 #include "flag.h"
19 #include "game_constants.h"
20 #include "generic_factory.h"
21 #include "inventory.h"
22 #include "item.h"
23 #include "itype.h"
24 #include "json.h"
25 #include "mapgen_functions.h"
26 #include "npc.h"
27 #include "optional.h"
28 #include "output.h"
29 #include "proficiency.h"
30 #include "skill.h"
31 #include "string_formatter.h"
32 #include "string_id_utils.h"
33 #include "translations.h"
34 #include "type_id.h"
35 #include "uistate.h"
36 #include "units.h"
37 #include "value_ptr.h"
38 
39 static const itype_id itype_hotplate( "hotplate" );
40 static const itype_id itype_atomic_coffeepot( "atomic_coffeepot" );
41 
recipe()42 recipe::recipe() : skill_used( skill_id::NULL_ID() ) {}
43 
batch_duration(const Character & guy,int batch,float multiplier,size_t assistants) const44 time_duration recipe::batch_duration( const Character &guy, int batch, float multiplier,
45                                       size_t assistants ) const
46 {
47     return time_duration::from_turns( batch_time( guy, batch, multiplier, assistants ) / 100 );
48 }
49 
helpers_have_proficiencies(const Character & guy,const proficiency_id & prof)50 static bool helpers_have_proficiencies( const Character &guy, const proficiency_id &prof )
51 {
52     std::vector<npc *> helpers = guy.get_crafting_helpers();
53     for( npc *helper : helpers ) {
54         if( helper->has_proficiency( prof ) ) {
55             return true;
56         }
57     }
58     return false;
59 }
60 
time_to_craft(const Character & guy,recipe_time_flag flags) const61 time_duration recipe::time_to_craft( const Character &guy, recipe_time_flag flags ) const
62 {
63     return time_duration::from_moves( time_to_craft_moves( guy, flags ) );
64 }
65 
time_to_craft_moves(const Character & guy,recipe_time_flag flags) const66 int64_t recipe::time_to_craft_moves( const Character &guy, recipe_time_flag flags ) const
67 {
68     if( flags == recipe_time_flag::ignore_proficiencies ) {
69         return time;
70     }
71     return time * proficiency_time_maluses( guy );
72 }
73 
batch_time(const Character & guy,int batch,float multiplier,size_t assistants) const74 int64_t recipe::batch_time( const Character &guy, int batch, float multiplier,
75                             size_t assistants ) const
76 {
77     // 1.0f is full speed
78     // 0.33f is 1/3 speed
79     if( multiplier == 0.0f ) {
80         // If an item isn't craftable in the dark, show the time to complete as if you could craft it
81         multiplier = 1.0f;
82     }
83 
84     const double local_time = static_cast<double>( time_to_craft_moves( guy ) ) / multiplier;
85 
86     // if recipe does not benefit from batching and we have no assistants, don't do unnecessary additional calculations
87     if( batch_rscale == 0.0 && assistants == 0 ) {
88         return static_cast<int64_t>( local_time ) * batch;
89     }
90 
91     double total_time = 0.0;
92     // if recipe does not benefit from batching but we do have assistants, skip calculating the batching scale factor
93     if( batch_rscale == 0.0f ) {
94         total_time = local_time * batch;
95     } else {
96         // recipe benefits from batching, so batching scale factor needs to be calculated
97         // At batch_rsize, incremental time increase is 99.5% of batch_rscale
98         const double scale = batch_rsize / 6.0f;
99         for( int x = 0; x < batch; x++ ) {
100             // scaled logistic function output
101             const double logf = ( 2.0 / ( 1.0 + std::exp( -( x / scale ) ) ) ) - 1.0;
102             total_time += local_time * ( 1.0 - ( batch_rscale * logf ) );
103         }
104     }
105 
106     //Assistants can decrease the time for production but never less than that of one unit
107     if( assistants == 1 ) {
108         total_time = total_time * .75;
109     } else if( assistants >= 2 ) {
110         total_time = total_time * .60;
111     }
112     if( total_time < local_time ) {
113         total_time = local_time;
114     }
115 
116     return static_cast<int64_t>( total_time );
117 }
118 
has_flag(const std::string & flag_name) const119 bool recipe::has_flag( const std::string &flag_name ) const
120 {
121     return flags.count( flag_name );
122 }
123 
load(const JsonObject & jo,const std::string & src)124 void recipe::load( const JsonObject &jo, const std::string &src )
125 {
126     bool strict = src == "dda";
127 
128     abstract = jo.has_string( "abstract" );
129 
130     const std::string type = jo.get_string( "type" );
131 
132     if( abstract ) {
133         ident_ = recipe_id( jo.get_string( "abstract" ) );
134     } else {
135         jo.read( "result", result_, true );
136         ident_ = recipe_id( result_.str() );
137     }
138 
139     if( type == "recipe" && jo.has_string( "id_suffix" ) ) {
140         if( abstract ) {
141             jo.throw_error( "abstract recipe cannot specify id_suffix", "id_suffix" );
142         }
143         ident_ = recipe_id( ident_.str() + "_" + jo.get_string( "id_suffix" ) );
144     }
145 
146     if( jo.has_bool( "obsolete" ) ) {
147         assign( jo, "obsolete", obsolete );
148     }
149 
150     // If it's an obsolete recipe, we don't need any more data, skip loading
151     if( obsolete ) {
152         return;
153     }
154 
155     if( jo.has_int( "time" ) ) {
156         // so we can specify moves that is not a multiple of 100
157         time = jo.get_int( "time" );
158     } else if( jo.has_string( "time" ) ) {
159         time = to_moves<int>( read_from_json_string<time_duration>( *jo.get_raw( "time" ),
160                               time_duration::units ) );
161     }
162     assign( jo, "difficulty", difficulty, strict, 0, MAX_SKILL );
163     assign( jo, "flags", flags );
164 
165     // automatically set contained if we specify as container
166     assign( jo, "contained", contained, strict );
167     contained |= assign( jo, "container", container, strict );
168     assign( jo, "sealed", sealed, strict );
169 
170     if( jo.has_array( "batch_time_factors" ) ) {
171         JsonArray batch = jo.get_array( "batch_time_factors" );
172         batch_rscale = batch.get_int( 0 ) / 100.0;
173         batch_rsize  = batch.get_int( 1 );
174     }
175 
176     assign( jo, "charges", charges );
177     assign( jo, "result_mult", result_mult );
178 
179     assign( jo, "skill_used", skill_used, strict );
180 
181     if( jo.has_member( "skills_required" ) ) {
182         JsonArray sk = jo.get_array( "skills_required" );
183         required_skills.clear();
184 
185         if( sk.empty() ) {
186             // clear all requirements
187 
188         } else if( sk.has_array( 0 ) ) {
189             // multiple requirements
190             for( JsonArray arr : sk ) {
191                 required_skills[skill_id( arr.get_string( 0 ) )] = arr.get_int( 1 );
192             }
193 
194         } else {
195             // single requirement
196             required_skills[skill_id( sk.get_string( 0 ) )] = sk.get_int( 1 );
197         }
198     }
199 
200     jo.read( "proficiencies", proficiencies );
201 
202     // simplified autolearn sets requirements equal to required skills at finalization
203     if( jo.has_bool( "autolearn" ) ) {
204         assign( jo, "autolearn", autolearn );
205 
206     } else if( jo.has_array( "autolearn" ) ) {
207         autolearn = true;
208         for( JsonArray arr : jo.get_array( "autolearn" ) ) {
209             autolearn_requirements[skill_id( arr.get_string( 0 ) )] = arr.get_int( 1 );
210         }
211     }
212 
213     // Mandatory: This recipe's exertion level
214     // TODO: Make this mandatory, no default or 'fake' exception
215     std::string exert = jo.get_string( "activity_level", "MODERATE_EXERCISE" );
216     // For making scripting that needs to be broken up over multiple PRs easier
217     if( exert == "fake" ) {
218         exert = "MODERATE_EXERCISE";
219     }
220     const auto it = activity_levels_map.find( exert );
221     if( it == activity_levels_map.end() ) {
222         jo.throw_error( string_format( "Invalid activity level %s", exert ), "activity_level" );
223     }
224     exertion = it->second;
225 
226     // Never let the player have a debug or NPC recipe
227     if( jo.has_bool( "never_learn" ) ) {
228         assign( jo, "never_learn", never_learn );
229     }
230 
231     if( jo.has_member( "decomp_learn" ) ) {
232         learn_by_disassembly.clear();
233 
234         if( jo.has_int( "decomp_learn" ) ) {
235             if( !skill_used ) {
236                 jo.throw_error( "decomp_learn specified with no skill_used" );
237             }
238             assign( jo, "decomp_learn", learn_by_disassembly[skill_used] );
239 
240         } else if( jo.has_array( "decomp_learn" ) ) {
241             for( JsonArray arr : jo.get_array( "decomp_learn" ) ) {
242                 learn_by_disassembly[skill_id( arr.get_string( 0 ) )] = arr.get_int( 1 );
243             }
244         }
245     }
246 
247     if( jo.has_member( "book_learn" ) ) {
248         booksets.clear();
249         if( jo.has_array( "book_learn" ) ) {
250             for( JsonArray arr : jo.get_array( "book_learn" ) ) {
251                 booksets.emplace( itype_id( arr.get_string( 0 ) ), book_recipe_data{ arr.size() > 1 ? arr.get_int( 1 ) : -1 } );
252             }
253         } else {
254             mandatory( jo, false, "book_learn", booksets );
255         }
256     }
257 
258     if( jo.has_member( "delete_flags" ) ) {
259         flags_to_delete = jo.get_tags<flag_id>( "delete_flags" );
260     }
261 
262     // recipes not specifying any external requirements inherit from their parent recipe (if any)
263     if( jo.has_string( "using" ) ) {
264         reqs_external = { { requirement_id( jo.get_string( "using" ) ), 1 } };
265 
266     } else if( jo.has_array( "using" ) ) {
267         reqs_external.clear();
268         for( JsonArray cur : jo.get_array( "using" ) ) {
269             reqs_external.emplace_back( requirement_id( cur.get_string( 0 ) ), cur.get_int( 1 ) );
270         }
271     }
272 
273     // inline requirements are always replaced (cannot be inherited)
274     reqs_internal.clear();
275 
276     // These cannot be inherited
277     bp_autocalc = false;
278     check_blueprint_needs = false;
279     blueprint_reqs.reset();
280 
281     if( type == "recipe" ) {
282 
283         assign( jo, "category", category, strict );
284         assign( jo, "subcategory", subcategory, strict );
285         assign( jo, "description", description, strict );
286         assign( jo, "reversible", reversible, strict );
287 
288         if( jo.has_member( "byproducts" ) ) {
289             if( this->reversible ) {
290                 jo.throw_error( "Recipe cannot be reversible and have byproducts" );
291             }
292             byproducts.clear();
293             for( JsonArray arr : jo.get_array( "byproducts" ) ) {
294                 itype_id byproduct( arr.get_string( 0 ) );
295                 byproducts[ byproduct ] += arr.size() == 2 ? arr.get_int( 1 ) : 1;
296             }
297         }
298         assign( jo, "construction_blueprint", blueprint );
299         if( !blueprint.empty() ) {
300             assign( jo, "blueprint_name", bp_name );
301             bp_resources.clear();
302             for( const std::string resource : jo.get_array( "blueprint_resources" ) ) {
303                 bp_resources.emplace_back( resource );
304             }
305             for( JsonObject provide : jo.get_array( "blueprint_provides" ) ) {
306                 bp_provides.emplace_back( std::make_pair( provide.get_string( "id" ),
307                                           provide.get_int( "amount", 1 ) ) );
308             }
309             // all blueprints provide themselves with needing it written in JSON
310             bp_provides.emplace_back( std::make_pair( result_.str(), 1 ) );
311             for( JsonObject require : jo.get_array( "blueprint_requires" ) ) {
312                 bp_requires.emplace_back( std::make_pair( require.get_string( "id" ),
313                                           require.get_int( "amount", 1 ) ) );
314             }
315             // all blueprints exclude themselves with needing it written in JSON
316             bp_excludes.emplace_back( std::make_pair( result_.str(), 1 ) );
317             for( JsonObject exclude : jo.get_array( "blueprint_excludes" ) ) {
318                 bp_excludes.emplace_back( std::make_pair( exclude.get_string( "id" ),
319                                           exclude.get_int( "amount", 1 ) ) );
320             }
321             check_blueprint_needs = jo.get_bool( "check_blueprint_needs", true );
322             if( jo.has_member( "blueprint_needs" ) ) {
323                 blueprint_reqs = cata::make_value<build_reqs>();
324                 const JsonObject jneeds = jo.get_object( "blueprint_needs" );
325                 if( jneeds.has_member( "time" ) ) {
326                     if( jneeds.has_int( "time" ) ) {
327                         // so we can specify moves that is not a multiple of 100
328                         blueprint_reqs->time = jneeds.get_int( "time" );
329                     } else {
330                         blueprint_reqs->time =
331                             to_moves<int>( read_from_json_string<time_duration>(
332                                                *jneeds.get_raw( "time" ), time_duration::units ) );
333                     }
334                 }
335                 if( jneeds.has_member( "skills" ) ) {
336                     std::vector<std::pair<skill_id, int>> blueprint_skills;
337                     jneeds.read( "skills", blueprint_skills );
338                     blueprint_reqs->skills = { blueprint_skills.begin(), blueprint_skills.end() };
339                 }
340                 if( jneeds.has_member( "inline" ) ) {
341                     const requirement_id req_id( "inline_blueprint_" + type + "_" + ident_.str() );
342                     requirement_data::load_requirement( jneeds.get_object( "inline" ), req_id );
343                     blueprint_reqs->reqs.emplace( req_id, 1 );
344                 }
345             } else if( check_blueprint_needs ) {
346                 bp_autocalc = true;
347             }
348         }
349     } else if( type == "uncraft" ) {
350         reversible = true;
351     } else {
352         jo.throw_error( "unknown recipe type", "type" );
353     }
354 
355     const requirement_id req_id( "inline_" + type + "_" + ident_.str() );
356     requirement_data::load_requirement( jo, req_id );
357     reqs_internal.emplace_back( req_id, 1 );
358 }
359 
finalize()360 void recipe::finalize()
361 {
362     if( bp_autocalc ) {
363         blueprint_reqs =
364             cata::make_value<build_reqs>( get_build_reqs_for_furn_ter_ids(
365                                               get_changed_ids_from_update( blueprint ) ) );
366     } else if( test_mode && check_blueprint_needs ) {
367         check_blueprint_requirements();
368     }
369 
370     incorporate_build_reqs();
371     blueprint_reqs.reset();
372 
373     // concatenate both external and inline requirements
374     add_requirements( reqs_external );
375     add_requirements( reqs_internal );
376 
377     reqs_external.clear();
378     reqs_internal.clear();
379 
380     deduped_requirements_ = deduped_requirement_data( requirements_, ident() );
381 
382     if( contained && container.is_null() ) {
383         container = item::find_type( result_ )->default_container.value_or( "null" );
384     }
385 
386     std::set<proficiency_id> required;
387     std::set<proficiency_id> used;
388     for( recipe_proficiency &rpof : proficiencies ) {
389         if( !rpof.id.is_valid() ) {
390             debugmsg( "proficiency %s does not exist in recipe %s", rpof.id.str(), ident_.str() );
391         }
392 
393         if( rpof.required && rpof.time_multiplier != 0.0f ) {
394             debugmsg( "proficiencies in recipes cannot be both required and provide a malus in %s",
395                       rpof.id.str(), ident_.str() );
396         }
397         if( required.count( rpof.id ) || used.count( rpof.id ) ) {
398             debugmsg( "proficiency %s listed twice recipe %s", rpof.id.str(),
399                       ident_.str() );
400         }
401 
402         if( rpof.time_multiplier < 1.0f && rpof.id->default_time_multiplier() < 1.0f ) {
403             debugmsg( "proficiency %s provides a time bonus for not being known in recipe %s.  Time multiplier: %s Default multiplier: %s",
404                       rpof.id.str(), ident_.str(), rpof.time_multiplier, rpof.id->default_time_multiplier() );
405         }
406 
407         if( rpof.time_multiplier == 0.0f ) {
408             rpof.time_multiplier = rpof.id->default_time_multiplier();
409         }
410 
411         if( rpof.fail_multiplier == 0.0f ) {
412             rpof.fail_multiplier = rpof.id->default_fail_multiplier();
413         }
414 
415         if( rpof.fail_multiplier < 1.0f && rpof.id->default_fail_multiplier() < 1.0f ) {
416             debugmsg( "proficiency %s provides a fail bonus for not being known in recipe %s  Fail multiplier: %s Default multiplier: %s",
417                       rpof.id.str(), ident_.str(), rpof.fail_multiplier, rpof.id->default_fail_multiplier() );
418         }
419 
420         // Now that we've done the error checking, log that a proficiency with this id is used
421         if( rpof.required ) {
422             required.insert( rpof.id );
423         } else {
424             used.insert( rpof.id );
425         }
426     }
427 
428     if( autolearn && autolearn_requirements.empty() ) {
429         autolearn_requirements = required_skills;
430         if( skill_used ) {
431             autolearn_requirements[ skill_used ] = difficulty;
432         }
433     }
434 }
435 
add_requirements(const std::vector<std::pair<requirement_id,int>> & reqs)436 void recipe::add_requirements( const std::vector<std::pair<requirement_id, int>> &reqs )
437 {
438     requirements_ = std::accumulate( reqs.begin(), reqs.end(), requirements_ );
439 }
440 
get_consistency_error() const441 std::string recipe::get_consistency_error() const
442 {
443     if( category == "CC_BUILDING" ) {
444         if( is_blueprint() || oter_str_id( result_.c_str() ).is_valid() ) {
445             return std::string();
446         }
447         return "defines invalid result";
448     }
449 
450     if( !item::type_is_defined( result_ ) ) {
451         return "defines invalid result";
452     }
453 
454     if( charges && !item::count_by_charges( result_ ) ) {
455         return "specifies charges but result is not counted by charges";
456     }
457 
458     const auto is_invalid_bp = []( const std::pair<itype_id, int> &elem ) {
459         return !item::type_is_defined( elem.first );
460     };
461 
462     if( std::any_of( byproducts.begin(), byproducts.end(), is_invalid_bp ) ) {
463         return "defines invalid byproducts";
464     }
465 
466     if( !contained && !container.is_null() ) {
467         return "defines container but not contained";
468     }
469 
470     if( !item::type_is_defined( container ) ) {
471         return "specifies unknown container";
472     }
473 
474     const auto is_invalid_skill = []( const std::pair<skill_id, int> &elem ) {
475         return !elem.first.is_valid();
476     };
477 
478     if( ( skill_used && !skill_used.is_valid() ) ||
479         std::any_of( required_skills.begin(), required_skills.end(), is_invalid_skill ) ) {
480         return "uses invalid skill";
481     }
482 
483     const auto is_invalid_book = []( const std::pair<itype_id, book_recipe_data> &elem ) {
484         return !item::find_type( elem.first )->book;
485     };
486 
487     if( std::any_of( booksets.begin(), booksets.end(), is_invalid_book ) ) {
488         return "defines invalid book";
489     }
490 
491     return std::string();
492 }
493 
create_result() const494 item recipe::create_result() const
495 {
496     item newit( result_, calendar::turn, item::default_charges_tag{} );
497 
498     if( newit.has_flag( flag_VARSIZE ) ) {
499         newit.set_flag( flag_FIT );
500     }
501 
502     if( charges ) {
503         newit.charges = *charges;
504     }
505 
506     if( !newit.craft_has_charges() ) {
507         newit.charges = 0;
508     } else if( result_mult != 1 ) {
509         // TODO: Make it work for charge-less items (update makes amount)
510         newit.charges *= result_mult;
511     }
512 
513     if( contained ) {
514         if( newit.count_by_charges() ) {
515             newit = newit.in_container( container, newit.charges, sealed );
516         } else {
517             newit = newit.in_container( container, item::INFINITE_CHARGES, sealed );
518         }
519     }
520 
521     return newit;
522 }
523 
create_results(int batch) const524 std::vector<item> recipe::create_results( int batch ) const
525 {
526     std::vector<item> items;
527 
528     const bool by_charges = item::count_by_charges( result_ );
529     if( contained || !by_charges ) {
530         // by_charges items get their charges multiplied in create_result
531         const int num_results = by_charges ? batch : batch * result_mult;
532         for( int i = 0; i < num_results; i++ ) {
533             item newit = create_result();
534             items.push_back( newit );
535         }
536     } else {
537         item newit = create_result();
538         newit.charges *= batch;
539         items.push_back( newit );
540     }
541 
542     return items;
543 }
544 
create_byproducts(int batch) const545 std::vector<item> recipe::create_byproducts( int batch ) const
546 {
547     std::vector<item> bps;
548     for( const auto &e : byproducts ) {
549         item obj( e.first, calendar::turn, item::default_charges_tag{} );
550         if( obj.has_flag( flag_VARSIZE ) ) {
551             obj.set_flag( flag_FIT );
552         }
553 
554         if( obj.count_by_charges() ) {
555             obj.charges *= e.second * batch;
556             bps.push_back( obj );
557 
558         } else {
559             if( !obj.craft_has_charges() ) {
560                 obj.charges = 0;
561             }
562             for( int i = 0; i < e.second * batch; ++i ) {
563                 bps.push_back( obj );
564             }
565         }
566     }
567     return bps;
568 }
569 
has_byproducts() const570 bool recipe::has_byproducts() const
571 {
572     return !byproducts.empty();
573 }
574 
required_proficiencies_string(const Character * c) const575 std::string recipe::required_proficiencies_string( const Character *c ) const
576 {
577     std::vector<proficiency_id> required_profs;
578 
579     for( const recipe_proficiency &rec : proficiencies ) {
580         if( rec.required ) {
581             required_profs.push_back( rec.id );
582         }
583     }
584     std::string required = enumerate_as_string( required_profs.begin(),
585     required_profs.end(), [&]( const proficiency_id & id ) {
586         nc_color color;
587         if( c != nullptr && c->has_proficiency( id ) ) {
588             color = c_green;
589         } else if( c != nullptr && helpers_have_proficiencies( *c, id ) ) {
590             color = c_yellow;
591         } else {
592             color = c_red;
593         }
594         return colorize( id->name(), color );
595     } );
596 
597     return required;
598 }
599 
600 struct prof_penalty {
601     proficiency_id id;
602     float time_mult;
603     float failure_mult;
604     bool mitigated = false;
605 };
606 
profstring(const prof_penalty & prof,std::string & color,const std::string & name_color="cyan")607 static std::string profstring( const prof_penalty &prof,
608                                std::string &color,
609                                const std::string &name_color = "cyan" )
610 {
611     std::string mitigated_str;
612     if( prof.mitigated ) {
613         mitigated_str = _( " (Mitigated)" );
614     }
615 
616     if( prof.time_mult == 1.0f ) {
617         return string_format( _( "<color_%s>%s</color> (<color_%s>%gx\u00a0failure</color>%s)" ),
618                               name_color, prof.id->name(), color, prof.failure_mult, mitigated_str );
619     } else if( prof.failure_mult == 1.0f ) {
620         return string_format( _( "<color_%s>%s</color> (<color_%s>%gx\u00a0time</color>%s)" ),
621                               name_color, prof.id->name(), color, prof.time_mult, mitigated_str );
622     }
623 
624     return string_format(
625                _( "<color_%s>%s</color> (<color_%s>%gx\u00a0time, %gx\u00a0failure</color>%s)" ),
626                name_color, prof.id->name(), color, prof.time_mult, prof.failure_mult, mitigated_str );
627 }
628 
used_proficiencies_string(const Character * c) const629 std::string recipe::used_proficiencies_string( const Character *c ) const
630 {
631     if( c == nullptr ) {
632         return { };
633     }
634     std::vector<prof_penalty> used_profs;
635 
636     for( const recipe_proficiency &rec : proficiencies ) {
637         if( !rec.required ) {
638             if( c->has_proficiency( rec.id ) || helpers_have_proficiencies( *c, rec.id ) )  {
639                 used_profs.push_back( { rec.id, rec.time_multiplier, rec.fail_multiplier } );
640             }
641         }
642     }
643 
644     std::string color = "light_gray";
645     std::string used = enumerate_as_string( used_profs.begin(),
646     used_profs.end(), [&]( const prof_penalty & prof ) {
647         return profstring( prof, color );
648     } );
649 
650     return used;
651 }
652 
missing_proficiencies_string(const Character * c) const653 std::string recipe::missing_proficiencies_string( const Character *c ) const
654 {
655     if( c == nullptr ) {
656         return { };
657     }
658     std::vector<prof_penalty> missing_profs;
659 
660     const book_proficiency_bonuses book_bonuses =
661         c->crafting_inventory().get_book_proficiency_bonuses();
662     for( const recipe_proficiency &rec : proficiencies ) {
663         if( !rec.required ) {
664             if( !( c->has_proficiency( rec.id ) || helpers_have_proficiencies( *c, rec.id ) ) ) {
665                 prof_penalty pen = { rec.id, rec.time_multiplier, rec.fail_multiplier };
666                 if( book_bonuses.time_factor( pen.id ) != 0.0f || book_bonuses.fail_factor( pen.id ) != 0.0f ) {
667                     pen.time_mult = 1.0f + ( pen.time_mult - 1.0f ) * ( 1.0f - book_bonuses.time_factor( pen.id ) );
668                     pen.failure_mult = 1.0f + ( pen.failure_mult - 1.0f ) * ( 1.0f - book_bonuses.fail_factor(
669                                            pen.id ) );
670                     pen.mitigated = true;
671                 }
672                 missing_profs.push_back( pen );
673             }
674         }
675     }
676 
677     std::string color = "yellow";
678     std::string missing = enumerate_as_string( missing_profs.begin(),
679     missing_profs.end(), [&]( const prof_penalty & prof ) {
680         return profstring( prof, color, c->has_prof_prereqs( prof.id ) ? "cyan" : "red" );
681     } );
682 
683     return missing;
684 }
685 
recipe_proficiencies_string() const686 std::string recipe::recipe_proficiencies_string() const
687 {
688     std::vector<proficiency_id> profs;
689 
690     for( const recipe_proficiency &rec : proficiencies ) {
691         profs.push_back( rec.id );
692     }
693     std::string list = enumerate_as_string( profs.begin(),
694     profs.end(), [&]( const proficiency_id & id ) {
695         return id->name();
696     } );
697 
698     return list;
699 }
700 
required_proficiencies() const701 std::set<proficiency_id> recipe::required_proficiencies() const
702 {
703     std::set<proficiency_id> ret;
704     for( const recipe_proficiency &rec : proficiencies ) {
705         if( rec.required ) {
706             ret.insert( rec.id );
707         }
708     }
709     return ret;
710 }
711 
character_has_required_proficiencies(const Character & c) const712 bool recipe::character_has_required_proficiencies( const Character &c ) const
713 {
714     for( const proficiency_id &id : required_proficiencies() ) {
715         if( !c.has_proficiency( id ) && !helpers_have_proficiencies( c, id ) ) {
716             return false;
717         }
718     }
719     return true;
720 }
721 
assist_proficiencies() const722 std::set<proficiency_id> recipe::assist_proficiencies() const
723 {
724     std::set<proficiency_id> ret;
725     for( const recipe_proficiency &rec : proficiencies ) {
726         if( !rec.required ) {
727             ret.insert( rec.id );
728         }
729     }
730     return ret;
731 }
732 
proficiency_time_maluses(const Character & guy) const733 float recipe::proficiency_time_maluses( const Character &guy ) const
734 {
735     float total_malus = 1.0f;
736     for( const recipe_proficiency &prof : proficiencies ) {
737         if( !guy.has_proficiency( prof.id ) &&
738             !helpers_have_proficiencies( guy, prof.id ) && prof.time_multiplier > 1.0f ) {
739             float malus = 1.0f + ( prof.time_multiplier - 1.0f ) *
740                           ( 1.0f - guy.crafting_inventory().get_book_proficiency_bonuses().time_factor( prof.id ) );
741             total_malus *= malus;
742         }
743     }
744     return total_malus;
745 }
746 
proficiency_failure_maluses(const Character & guy) const747 float recipe::proficiency_failure_maluses( const Character &guy ) const
748 {
749     float total_malus = 1.0f;
750     for( const recipe_proficiency &prof : proficiencies ) {
751         if( !guy.has_proficiency( prof.id ) &&
752             !helpers_have_proficiencies( guy, prof.id ) && prof.fail_multiplier > 1.0f ) {
753             float malus = 1.0f + ( prof.fail_multiplier - 1.0f ) *
754                           ( 1.0f - guy.crafting_inventory().get_book_proficiency_bonuses().fail_factor( prof.id ) );
755             total_malus *= malus;
756         }
757     }
758     return total_malus;
759 }
760 
exertion_level() const761 float recipe::exertion_level() const
762 {
763     return exertion;
764 }
765 
766 // Format a std::pair<skill_id, int> for the crafting menu.
767 // skill colored green (or yellow if beyond characters skill)
768 // optionally with the skill level (player / difficulty)
769 template<typename Iter>
required_skills_as_string(Iter first,Iter last,const Character * c,const bool print_skill_level)770 std::string required_skills_as_string( Iter first, Iter last, const Character *c,
771                                        const bool print_skill_level )
772 {
773     if( first == last ) {
774         return _( "<color_cyan>none</color>" );
775     }
776 
777     return enumerate_as_string( first, last,
778     [&]( const std::pair<skill_id, int> &skill ) {
779         const int player_skill = c ? c->get_skill_level( skill.first ) : 0;
780         std::string difficulty_color = skill.second > player_skill ? "yellow" : "green";
781         std::string skill_level_string = print_skill_level ? "" : ( std::to_string( player_skill ) + "/" );
782         skill_level_string += std::to_string( skill.second );
783         return string_format( "<color_cyan>%s</color> <color_%s>(%s)</color>",
784                               skill.first.obj().name(), difficulty_color, skill_level_string );
785     } );
786 }
787 
788 // Format a std::pair<skill_id, int> for the basecamp bulletin board.
789 // skill colored white with difficulty in parenthesis.
790 template<typename Iter>
required_skills_as_string(Iter first,Iter last)791 std::string required_skills_as_string( Iter first, Iter last )
792 {
793     if( first == last ) {
794         return _( "<color_cyan>none</color>" );
795     }
796 
797     return enumerate_as_string( first, last,
798     [&]( const std::pair<skill_id, int> &skill ) {
799         return string_format( "<color_white>%s (%d)</color>", skill.first.obj().name(),
800                               skill.second );
801     } );
802 }
803 
primary_skill_string(const Character * c,bool print_skill_level) const804 std::string recipe::primary_skill_string( const Character *c, bool print_skill_level ) const
805 {
806     std::vector< std::pair<skill_id, int> > skillList;
807 
808     if( !skill_used.is_null() ) {
809         skillList.push_back( std::pair<skill_id, int>( skill_used, difficulty ) );
810     }
811 
812     return required_skills_as_string( skillList.begin(), skillList.end(), c, print_skill_level );
813 }
814 
required_skills_string(const Character * c,bool include_primary_skill,bool print_skill_level) const815 std::string recipe::required_skills_string( const Character *c, bool include_primary_skill,
816         bool print_skill_level ) const
817 {
818     std::vector<std::pair<skill_id, int>> skillList = sorted_lex( required_skills );
819 
820     // There is primary skill used and it should be included: add it to the beginning
821     if( !skill_used.is_null() && include_primary_skill ) {
822         skillList.insert( skillList.begin(), std::pair<skill_id, int>( skill_used, difficulty ) );
823     }
824     return required_skills_as_string( skillList.begin(), skillList.end(), c, print_skill_level );
825 }
826 
required_all_skills_string() const827 std::string recipe::required_all_skills_string() const
828 {
829     std::vector<std::pair<skill_id, int>> skillList = sorted_lex( required_skills );
830     // There is primary skill used, add it to the front
831     if( !skill_used.is_null() ) {
832         skillList.insert( skillList.begin(), std::pair<skill_id, int>( skill_used, difficulty ) );
833     }
834     return required_skills_as_string( skillList.begin(), skillList.end() );
835 }
836 
batch_savings_string() const837 std::string recipe::batch_savings_string() const
838 {
839     return ( batch_rsize != 0 ) ?
840            string_format( _( "%d%% at >%d units" ), static_cast<int>( batch_rscale * 100 ), batch_rsize )
841            : _( "none" );
842 }
843 
result_name() const844 std::string recipe::result_name() const
845 {
846     std::string name = item::nname( result_ );
847     if( uistate.favorite_recipes.find( this->ident() ) != uistate.favorite_recipes.end() ) {
848         name = "* " + name;
849     }
850 
851     return name;
852 }
853 
will_be_blacklisted() const854 bool recipe::will_be_blacklisted() const
855 {
856     if( requirements_.is_blacklisted() ) {
857         return true;
858     }
859 
860     auto any_is_blacklisted = []( const std::vector<std::pair<requirement_id, int>> &reqs ) {
861         auto req_is_blacklisted = []( const std::pair<requirement_id, int> &req ) {
862             return req.first->is_blacklisted();
863         };
864 
865         return std::any_of( reqs.begin(), reqs.end(), req_is_blacklisted );
866     };
867 
868     return any_is_blacklisted( reqs_internal ) || any_is_blacklisted( reqs_external );
869 }
870 
get_component_filter(const recipe_filter_flags flags) const871 std::function<bool( const item & )> recipe::get_component_filter(
872     const recipe_filter_flags flags ) const
873 {
874     const item result = create_result();
875 
876     // Disallow crafting of non-perishables with rotten components
877     // Make an exception for items with the ALLOW_ROTTEN flag such as seeds
878     const bool recipe_forbids_rotten =
879         result.is_food() && !result.goes_bad() && !has_flag( "ALLOW_ROTTEN" );
880     const bool flags_forbid_rotten =
881         static_cast<bool>( flags & recipe_filter_flags::no_rotten );
882     std::function<bool( const item & )> rotten_filter = return_true<item>;
883     if( recipe_forbids_rotten || flags_forbid_rotten ) {
884         rotten_filter = []( const item & component ) {
885             return !component.rotten();
886         };
887     }
888 
889     // If the result is made hot, we can allow frozen components.
890     // EDIBLE_FROZEN components ( e.g. flour, chocolate ) are allowed as well
891     // Otherwise forbid them
892     std::function<bool( const item & )> frozen_filter = return_true<item>;
893     if( result.is_food() && !hot_result() ) {
894         frozen_filter = []( const item & component ) {
895             return !component.has_flag( flag_FROZEN ) || component.has_flag( flag_EDIBLE_FROZEN );
896         };
897     }
898 
899     // Disallow usage of non-full magazines as components
900     // This is primarily used to require a fully charged battery, but works for any magazine.
901     std::function<bool( const item & )> magazine_filter = return_true<item>;
902     if( has_flag( "NEED_FULL_MAGAZINE" ) ) {
903         magazine_filter = []( const item & component ) {
904             if( component.ammo_remaining() == 0 ) {
905                 return false;
906             }
907             return !component.is_magazine() ||
908                    ( component.ammo_remaining() >= component.ammo_capacity( component.ammo_data()->ammo->type ) );
909         };
910     }
911 
912     return [ rotten_filter, frozen_filter, magazine_filter ]( const item & component ) {
913         return is_crafting_component( component ) &&
914                rotten_filter( component ) &&
915                frozen_filter( component ) &&
916                magazine_filter( component );
917     };
918 }
919 
is_blueprint() const920 bool recipe::is_blueprint() const
921 {
922     return !blueprint.empty();
923 }
924 
get_blueprint() const925 const std::string &recipe::get_blueprint() const
926 {
927     return blueprint;
928 }
929 
blueprint_name() const930 const translation &recipe::blueprint_name() const
931 {
932     return bp_name;
933 }
934 
blueprint_resources() const935 const std::vector<itype_id> &recipe::blueprint_resources() const
936 {
937     return bp_resources;
938 }
939 
blueprint_provides() const940 const std::vector<std::pair<std::string, int>> &recipe::blueprint_provides() const
941 {
942     return bp_provides;
943 }
944 
blueprint_requires() const945 const std::vector<std::pair<std::string, int>>  &recipe::blueprint_requires() const
946 {
947     return bp_requires;
948 }
949 
blueprint_excludes() const950 const std::vector<std::pair<std::string, int>>  &recipe::blueprint_excludes() const
951 {
952     return bp_excludes;
953 }
954 
check_blueprint_requirements()955 void recipe::check_blueprint_requirements()
956 {
957     build_reqs total_reqs =
958         get_build_reqs_for_furn_ter_ids( get_changed_ids_from_update( blueprint ) );
959     requirement_data req_data_blueprint( blueprint_reqs->reqs );
960     requirement_data req_data_calc( total_reqs.reqs );
961     // do not consolidate req_data_blueprint: it actually changes the meaning of the requirement.
962     // instead we enforce specifying the exact consolidated requirement.
963     req_data_calc.consolidate();
964     if( blueprint_reqs->time != total_reqs.time || blueprint_reqs->skills != total_reqs.skills
965         || !req_data_blueprint.has_same_requirements_as( req_data_calc ) ) {
966         std::ostringstream os;
967         JsonOut jsout( os, /*pretty_print=*/true );
968 
969         jsout.start_object();
970 
971         jsout.member( "time" );
972         if( total_reqs.time % 100 == 0 ) {
973             dump_to_json_string( time_duration::from_turns( total_reqs.time / 100 ),
974                                  jsout, time_duration::units );
975         } else {
976             // cannot precisely represent the value using time_duration format,
977             // write integer instead.
978             jsout.write( total_reqs.time );
979         }
980 
981         jsout.member( "skills" );
982         jsout.start_array( /*wrap=*/!total_reqs.skills.empty() );
983         for( const std::pair<const skill_id, int> &p : total_reqs.skills ) {
984             jsout.start_array();
985             jsout.write( p.first );
986             jsout.write( p.second );
987             jsout.end_array();
988         }
989         jsout.end_array();
990 
991         jsout.member( "inline" );
992         req_data_calc.dump( jsout );
993 
994         jsout.end_object();
995 
996         debugmsg( "Specified blueprint requirements of %1$s does not match calculated requirements.\n"
997                   "Make one of the following changes to resolve this issue:\n"
998                   "- Specify \"check_blueprint_needs\": false to disable the check.\n"
999                   "- Remove \"blueprint_needs\" from the recipe json to have the requirements be autocalculated.\n"
1000                   "- Update \"blueprint_needs\" to the following value (you can use tools/update_blueprint_needs.py):\n"
1001                   // mark it for the auto-update python script
1002                   "~~~ auto-update-blueprint: %1$s\n"
1003                   "%2$s\n"
1004                   "~~~ end-auto-update",
1005                   ident_.str(), os.str() );
1006     }
1007 }
1008 
removes_raw() const1009 bool recipe::removes_raw() const
1010 {
1011     return create_result().is_comestible() && !create_result().has_flag( flag_RAW );
1012 }
1013 
hot_result() const1014 bool recipe::hot_result() const
1015 {
1016     // Check if the recipe tools make this food item hot upon making it.
1017     // We don't actually know which specific tool the player used/will use here, but
1018     // we're checking for a class of tools; because of the way requirements
1019     // processing works, the "surface_heat" id gets nuked into an actual
1020     // list of tools, see data/json/recipes/cooking_tools.json.
1021     //
1022     // Currently it's checking for a hotplate because that's a
1023     // suitable item in both the "surface_heat" and "water_boiling_heat"
1024     // tools, and it's usually the first item in a list of tools so if this
1025     // does get heated we'll find it right away.
1026     //
1027     // Atomic coffee is an outlier in that it is a hot drink that cannot be crafted
1028     // with any of the usual tools except the atomic coffee maker, which is why
1029     // the check includes this tool in addition to the hotplate.
1030     //
1031     // TODO: Make this less of a hack
1032     if( create_result().is_food() ) {
1033         const requirement_data::alter_tool_comp_vector &tool_lists = simple_requirements().get_tools();
1034         for( const std::vector<tool_comp> &tools : tool_lists ) {
1035             for( const tool_comp &t : tools ) {
1036                 if( ( t.type == itype_hotplate ) || ( t.type == itype_atomic_coffeepot ) ) {
1037                     return true;
1038                 }
1039             }
1040         }
1041     }
1042     return false;
1043 }
1044 
makes_amount() const1045 int recipe::makes_amount() const
1046 {
1047     int makes = 0;
1048     if( charges.has_value() ) {
1049         makes = charges.value();
1050     } else if( item::count_by_charges( result_ ) ) {
1051         makes = item::find_type( result_ )->charges_default();
1052     }
1053     // return either charges * mult or 1
1054     return makes ? makes * result_mult : 1 ;
1055 }
1056 
incorporate_build_reqs()1057 void recipe::incorporate_build_reqs()
1058 {
1059     if( !blueprint_reqs ) {
1060         return;
1061     }
1062 
1063     time += blueprint_reqs->time;
1064 
1065     for( const std::pair<const skill_id, int> &p : blueprint_reqs->skills ) {
1066         int &val = required_skills[p.first];
1067         val = std::max( val, p.second );
1068     }
1069 
1070     requirement_data req_data( blueprint_reqs->reqs );
1071     req_data.consolidate();
1072     const requirement_id req_id( "autocalc_blueprint_" + ident_.str() );
1073     requirement_data::save_requirement( req_data, req_id );
1074     reqs_internal.emplace_back( req_id, 1 );
1075 }
1076 
deserialize(JsonIn & jsin)1077 void recipe_proficiency::deserialize( JsonIn &jsin )
1078 {
1079     load( jsin.get_object() );
1080 }
1081 
load(const JsonObject & jo)1082 void recipe_proficiency::load( const JsonObject &jo )
1083 {
1084     jo.read( "proficiency", id );
1085     jo.read( "required", required );
1086     jo.read( "time_multiplier", time_multiplier );
1087     jo.read( "fail_multiplier", fail_multiplier );
1088     jo.read( "learning_time_multiplier", learning_time_mult );
1089     jo.read( "max_experience", max_experience );
1090 }
1091 
deserialize(JsonIn & jsin)1092 void book_recipe_data::deserialize( JsonIn &jsin )
1093 {
1094     load( jsin.get_object() );
1095 }
1096 
load(const JsonObject & jo)1097 void book_recipe_data::load( const JsonObject &jo )
1098 {
1099     jo.read( "skill_level", skill_req );
1100     jo.read( "recipe_name", alt_name );
1101     jo.read( "hidden", hidden );
1102 }
1103