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