1 #include "crafting_gui.h"
2
3 #include <algorithm>
4 #include <chrono>
5 #include <cstddef>
6 #include <cstring>
7 #include <functional>
8 #include <iterator>
9 #include <map>
10 #include <new>
11 #include <set>
12 #include <string>
13 #include <utility>
14 #include <vector>
15
16 #include "calendar.h"
17 #include "cata_utility.h"
18 #include "catacharset.h"
19 #include "character.h"
20 #include "color.h"
21 #include "crafting.h"
22 #include "cursesdef.h"
23 #include "input.h"
24 #include "inventory.h"
25 #include "item.h"
26 #include "itype.h"
27 #include "json.h"
28 #include "optional.h"
29 #include "output.h"
30 #include "point.h"
31 #include "popup.h"
32 #include "recipe.h"
33 #include "recipe_dictionary.h"
34 #include "requirements.h"
35 #include "string_formatter.h"
36 #include "string_input_popup.h"
37 #include "translations.h"
38 #include "type_id.h"
39 #include "ui.h"
40 #include "ui_manager.h"
41 #include "uistate.h"
42
43 static const std::string flag_BLIND_EASY( "BLIND_EASY" );
44 static const std::string flag_BLIND_HARD( "BLIND_HARD" );
45
46 class npc;
47
48 enum TAB_MODE {
49 NORMAL,
50 FILTERED,
51 BATCH
52 };
53
54 // TODO: Convert these globals to handling categories via generic_factory?
55 static std::vector<std::string> craft_cat_list;
56 static std::map<std::string, std::vector<std::string> > craft_subcat_list;
57 static std::map<std::string, std::string> normalized_names;
58
59 static bool query_is_yes( const std::string &query );
60 static void draw_hidden_amount( const catacurses::window &w, int amount, int num_recipe );
61 static void draw_can_craft_indicator( const catacurses::window &w, const recipe &rec );
62 static void draw_recipe_tabs( const catacurses::window &w, const std::string &tab,
63 TAB_MODE mode = NORMAL );
64 static void draw_recipe_subtabs( const catacurses::window &w, const std::string &tab,
65 const std::string &subtab,
66 const recipe_subset &available_recipes, TAB_MODE mode = NORMAL );
67
68 static std::string peek_related_recipe( const recipe *current, const recipe_subset &available );
69 static int related_menu_fill( uilist &rmenu,
70 const std::vector<std::pair<itype_id, std::string>> &related_recipes,
71 const recipe_subset &available );
72
get_cat_unprefixed(const std::string & prefixed_name)73 static std::string get_cat_unprefixed( const std::string &prefixed_name )
74 {
75 return prefixed_name.substr( 3, prefixed_name.size() - 3 );
76 }
77
load_recipe_category(const JsonObject & jsobj)78 void load_recipe_category( const JsonObject &jsobj )
79 {
80 const std::string category = jsobj.get_string( "id" );
81 const bool is_hidden = jsobj.get_bool( "is_hidden", false );
82
83 if( category.find( "CC_" ) != 0 ) {
84 jsobj.throw_error( "Crafting category id has to be prefixed with 'CC_'" );
85 }
86
87 if( !is_hidden &&
88 std::find( craft_cat_list.begin(), craft_cat_list.end(), category ) == craft_cat_list.end() ) {
89 craft_cat_list.push_back( category );
90 }
91
92 const std::string cat_name = get_cat_unprefixed( category );
93
94 craft_subcat_list[category].clear();
95 for( const std::string subcat_id : jsobj.get_array( "recipe_subcategories" ) ) {
96 if( subcat_id.find( "CSC_" + cat_name + "_" ) != 0 && subcat_id != "CSC_ALL" ) {
97 jsobj.throw_error( "Crafting sub-category id has to be prefixed with CSC_<category_name>_" );
98 }
99 if( find( craft_subcat_list[category].begin(), craft_subcat_list[category].end(),
100 subcat_id ) == craft_subcat_list[category].end() ) {
101 craft_subcat_list[category].push_back( subcat_id );
102 }
103 }
104 }
105
get_subcat_unprefixed(const std::string & cat,const std::string & prefixed_name)106 static std::string get_subcat_unprefixed( const std::string &cat, const std::string &prefixed_name )
107 {
108 std::string prefix = "CSC_" + get_cat_unprefixed( cat ) + "_";
109
110 if( prefixed_name.find( prefix ) == 0 ) {
111 return prefixed_name.substr( prefix.size(), prefixed_name.size() - prefix.size() );
112 }
113
114 return prefixed_name == "CSC_ALL" ? translate_marker( "ALL" ) : translate_marker( "NONCRAFT" );
115 }
116
translate_all()117 static void translate_all()
118 {
119 normalized_names.clear();
120 for( const auto &cat : craft_cat_list ) {
121 normalized_names[cat] = _( get_cat_unprefixed( cat ) );
122
123 for( const auto &subcat : craft_subcat_list[cat] ) {
124 normalized_names[subcat] = _( get_subcat_unprefixed( cat, subcat ) );
125 }
126 }
127 }
128
reset_recipe_categories()129 void reset_recipe_categories()
130 {
131 craft_cat_list.clear();
132 craft_subcat_list.clear();
133 }
134
135 namespace
136 {
137 struct availability {
availability__anonbd0d344b0111::availability138 explicit availability( const recipe *r, int batch_size = 1 ) {
139 Character &player = get_player_character();
140 const inventory &inv = player.crafting_inventory();
141 auto all_items_filter = r->get_component_filter( recipe_filter_flags::none );
142 auto no_rotten_filter = r->get_component_filter( recipe_filter_flags::no_rotten );
143 const deduped_requirement_data &req = r->deduped_requirements();
144 has_proficiencies = r->character_has_required_proficiencies( player );
145 can_craft = req.can_make_with_inventory(
146 inv, all_items_filter, batch_size, craft_flags::start_only ) && has_proficiencies;
147 can_craft_non_rotten = req.can_make_with_inventory(
148 inv, no_rotten_filter, batch_size, craft_flags::start_only );
149 const requirement_data &simple_req = r->simple_requirements();
150 apparently_craftable = simple_req.can_make_with_inventory(
151 inv, all_items_filter, batch_size, craft_flags::start_only );
152 proficiency_time_maluses = r->proficiency_time_maluses( player );
153 proficiency_failure_maluses = r->proficiency_failure_maluses( player );
154 has_all_skills = r->skill_used.is_null() ||
155 player.get_skill_level( r->skill_used ) >= r->difficulty;
156 for( const std::pair<const skill_id, int> &e : r->required_skills ) {
157 if( player.get_skill_level( e.first ) < e.second ) {
158 has_all_skills = false;
159 break;
160 }
161 }
162 }
163 bool can_craft;
164 bool can_craft_non_rotten;
165 bool apparently_craftable;
166 bool has_proficiencies;
167 bool has_all_skills;
168 float proficiency_time_maluses;
169 float proficiency_failure_maluses;
170
selected_color__anonbd0d344b0111::availability171 nc_color selected_color() const {
172 if( !can_craft ) {
173 return h_dark_gray;
174 } else if( !can_craft_non_rotten ) {
175 return has_all_skills ? h_brown : h_red;
176 } else {
177 return has_all_skills ? h_white : h_yellow;
178 }
179 }
180
color__anonbd0d344b0111::availability181 nc_color color( bool ignore_missing_skills = false ) const {
182 if( !can_craft ) {
183 return c_dark_gray;
184 } else if( !can_craft_non_rotten ) {
185 return has_all_skills || ignore_missing_skills ? c_brown : c_red;
186 } else {
187 return has_all_skills || ignore_missing_skills ? c_white : c_yellow;
188 }
189 }
190 };
191 } // namespace
192
recipe_info(const recipe & recp,const availability & avail,Character & guy,const std::string qry_comps,const int batch_size,const int fold_width,const nc_color & color)193 static std::vector<std::string> recipe_info(
194 const recipe &recp,
195 const availability &avail,
196 Character &guy,
197 const std::string qry_comps,
198 const int batch_size,
199 const int fold_width,
200 const nc_color &color )
201 {
202 std::ostringstream oss;
203
204 oss << string_format( _( "Primary skill: %s\n" ),
205 recp.primary_skill_string( &guy, false ) );
206
207 oss << string_format( _( "Other skills: %s\n" ),
208 recp.required_skills_string( &guy, false, false ) );
209
210 oss << string_format( _( "Proficiencies Required: %s\n" ),
211 recp.required_proficiencies_string( &guy ) );
212
213 const std::string used_profs = recp.used_proficiencies_string( &guy );
214 if( !used_profs.empty() ) {
215 oss << string_format( _( "Proficiencies Used: %s\n" ), used_profs );
216 }
217 const std::string missing_profs = recp.missing_proficiencies_string( &guy );
218 if( !missing_profs.empty() ) {
219 oss << string_format( _( "Proficiencies Missing: %s\n" ), missing_profs );
220 }
221
222 const int expected_turns = guy.expected_time_to_craft( recp, batch_size )
223 / to_moves<int>( 1_turns );
224 oss << string_format( _( "Time to complete: <color_cyan>%s</color>\n" ),
225 to_string( time_duration::from_turns( expected_turns ) ) );
226
227 oss << string_format( _( "Batch time savings: <color_cyan>%s</color>\n" ),
228 recp.batch_savings_string() );
229
230 const int makes = recp.makes_amount();
231 if( makes > 1 ) {
232 oss << string_format( _( "Recipe makes: <color_cyan>%d</color>\n" ), makes );
233 }
234
235 oss << string_format( _( "Craftable in the dark? <color_cyan>%s</color>\n" ),
236 recp.has_flag( flag_BLIND_EASY ) ? _( "Easy" ) :
237 recp.has_flag( flag_BLIND_HARD ) ? _( "Hard" ) :
238 _( "Impossible" ) );
239
240 std::string nearby_string;
241 const inventory &crafting_inv = guy.crafting_inventory();
242 const int nearby_amount = crafting_inv.count_item( recp.result() );
243 if( nearby_amount == 0 ) {
244 nearby_string = "<color_light_gray>0</color>";
245 } else if( nearby_amount > 9000 ) {
246 // at some point you get too many to count at a glance and just know you have a lot
247 nearby_string = _( "<color_red>It's Over 9000!!!</color>" );
248 } else {
249 nearby_string = string_format( "<color_yellow>%d</color>", nearby_amount );
250 }
251 oss << string_format( _( "Nearby: %s\n" ), nearby_string );
252
253 const bool can_craft_this = avail.can_craft;
254 if( can_craft_this && !avail.can_craft_non_rotten ) {
255 oss << _( "<color_red>Will use rotten ingredients</color>\n" );
256 }
257 const bool too_complex = recp.deduped_requirements().is_too_complex();
258 if( can_craft_this && too_complex ) {
259 oss << _( "Due to the complex overlapping requirements, this "
260 "recipe <color_yellow>may appear to be craftable "
261 "when it is not</color>.\n" );
262 }
263 if( !can_craft_this && avail.apparently_craftable && avail.has_proficiencies ) {
264 oss << _( "<color_red>Cannot be crafted because the same item is needed "
265 "for multiple components</color>\n" );
266 }
267 const float time_maluses = avail.proficiency_time_maluses;
268 const float fail_maluses = avail.proficiency_failure_maluses;
269 if( time_maluses != 1.0 || fail_maluses != 1.0 ) {
270 oss << string_format( _( "<color_yellow>This recipe will take %.1fx as long as normal, "
271 "and be %.1fx more likely to incur failures, because you "
272 "lack some of the proficiencies used.\n" ), time_maluses, fail_maluses );
273 }
274 if( !can_craft_this && !avail.has_proficiencies ) {
275 oss << _( "<color_red>Cannot be crafted because you lack"
276 " the required proficiencies.</color>\n" );
277 }
278
279 if( recp.has_byproducts() ) {
280 oss << _( "Byproducts:\n" );
281 for( const std::pair<const itype_id, int> &bp : recp.byproducts ) {
282 const itype *t = item::find_type( bp.first );
283 int amount = bp.second * batch_size;
284 if( t->count_by_charges() ) {
285 amount *= t->charges_default();
286 oss << string_format( "> %s (%d)\n", t->nname( 1 ), amount );
287 } else {
288 oss << string_format( "> %d %s\n", amount,
289 t->nname( static_cast<unsigned int>( amount ) ) );
290 }
291 }
292 }
293
294 std::vector<std::string> result = foldstring( oss.str(), fold_width );
295
296 const requirement_data &req = recp.simple_requirements();
297 const std::vector<std::string> tools = req.get_folded_tools_list(
298 fold_width, color, crafting_inv, batch_size );
299 const std::vector<std::string> comps = req.get_folded_components_list(
300 fold_width, color, crafting_inv, recp.get_component_filter(), batch_size, qry_comps );
301 result.insert( result.end(), tools.begin(), tools.end() );
302 result.insert( result.end(), comps.begin(), comps.end() );
303
304 oss = std::ostringstream();
305 if( !guy.knows_recipe( &recp ) ) {
306 oss << _( "Recipe not memorized yet\n" );
307 const std::set<itype_id> books_with_recipe = guy.get_books_for_recipe( crafting_inv, &recp );
308 const std::string enumerated_books =
309 enumerate_as_string( books_with_recipe.begin(), books_with_recipe.end(),
310 []( const itype_id & type_id ) {
311 return colorize( item::nname( type_id ), c_cyan );
312 } );
313 oss << string_format( _( "Written in: %s\n" ), enumerated_books );
314 }
315 std::vector<std::string> tmp = foldstring( oss.str(), fold_width );
316 result.insert( result.end(), tmp.begin(), tmp.end() );
317
318 return result;
319 }
320
select_crafting_recipe(int & batch_size_out)321 const recipe *select_crafting_recipe( int &batch_size_out )
322 {
323 struct {
324 const recipe *recp = nullptr;
325 std::string qry_comps;
326 int batch_size;
327 int fold_width;
328 std::vector<std::string> text;
329 } recipe_info_cache;
330 int recipe_info_scroll = 0;
331
332 const auto cached_recipe_info =
333 [&](
334 const recipe & recp,
335 const availability & avail,
336 Character & guy,
337 const std::string qry_comps,
338 const int batch_size,
339 const int fold_width,
340 const nc_color & color
341 ) -> const std::vector<std::string> & { // *NOPAD*
342 if( recipe_info_cache.recp != &recp
343 || recipe_info_cache.qry_comps != qry_comps
344 || recipe_info_cache.batch_size != batch_size
345 || recipe_info_cache.fold_width != fold_width )
346 {
347 recipe_info_cache.recp = &recp;
348 recipe_info_cache.qry_comps = qry_comps;
349 recipe_info_cache.batch_size = batch_size;
350 recipe_info_cache.fold_width = fold_width;
351 recipe_info_cache.text = recipe_info(
352 recp, avail, guy, qry_comps, batch_size, fold_width, color );
353 }
354 return recipe_info_cache.text;
355 };
356
357 struct {
358 const recipe *last_recipe = nullptr;
359 item dummy;
360 } item_info_cache;
361 int item_info_scroll = 0;
362 int item_info_scroll_popup = 0;
363
364 const auto item_info_data_from_recipe =
365 [&]( const recipe * rec, const int count, int &scroll_pos ) {
366 if( item_info_cache.last_recipe != rec ) {
367 item_info_cache.last_recipe = rec;
368 item_info_cache.dummy = rec->create_result();
369 item_info_cache.dummy.set_var( "recipe_exemplar", rec->ident().str() );
370 item_info_scroll = 0;
371 item_info_scroll_popup = 0;
372 }
373 std::vector<iteminfo> info;
374 item_info_cache.dummy.info( true, info, count );
375 item_info_data data( item_info_cache.dummy.tname( count ),
376 item_info_cache.dummy.type_name( count ),
377 info, {}, scroll_pos );
378 return data;
379 };
380
381 // always re-translate the category names in case the language has changed
382 translate_all();
383
384 const int headHeight = 3;
385 const int subHeadHeight = 2;
386
387 bool isWide = false;
388 int width = 0;
389 int dataLines = 0;
390 int dataHalfLines = 0;
391 int dataHeight = 0;
392 int item_info_width = 0;
393
394 input_context ctxt( "CRAFTING" );
395 ctxt.register_cardinal();
396 ctxt.register_action( "QUIT" );
397 ctxt.register_action( "CONFIRM" );
398 ctxt.register_action( "SCROLL_RECIPE_INFO_UP" );
399 ctxt.register_action( "SCROLL_RECIPE_INFO_DOWN" );
400 ctxt.register_action( "PAGE_UP", to_translation( "Fast scroll up" ) );
401 ctxt.register_action( "PAGE_DOWN", to_translation( "Fast scroll down" ) );
402 ctxt.register_action( "SCROLL_ITEM_INFO_UP" );
403 ctxt.register_action( "SCROLL_ITEM_INFO_DOWN" );
404 ctxt.register_action( "PREV_TAB" );
405 ctxt.register_action( "NEXT_TAB" );
406 ctxt.register_action( "FILTER" );
407 ctxt.register_action( "RESET_FILTER" );
408 ctxt.register_action( "TOGGLE_FAVORITE" );
409 ctxt.register_action( "HELP_RECIPE" );
410 ctxt.register_action( "HELP_KEYBINDINGS" );
411 ctxt.register_action( "CYCLE_BATCH" );
412 ctxt.register_action( "RELATED_RECIPES" );
413 ctxt.register_action( "HIDE_SHOW_RECIPE" );
414
415 catacurses::window w_head;
416 catacurses::window w_subhead;
417 catacurses::window w_data;
418 catacurses::window w_iteminfo;
419 std::vector<std::string> keybinding_tips;
420 int keybinding_x = 0;
421 ui_adaptor ui;
422 ui.on_screen_resize( [&]( ui_adaptor & ui ) {
423 const int freeWidth = TERMX - FULL_SCREEN_WIDTH;
424 isWide = ( TERMX > FULL_SCREEN_WIDTH && freeWidth > 15 );
425
426 width = isWide ? ( freeWidth > FULL_SCREEN_WIDTH ? FULL_SCREEN_WIDTH * 2 : TERMX ) :
427 FULL_SCREEN_WIDTH;
428 const int wStart = ( TERMX - width ) / 2;
429
430 // Keybinding tips
431 static const translation inline_fmt = to_translation(
432 //~ %1$s: action description text before key,
433 //~ %2$s: key description,
434 //~ %3$s: action description text after key.
435 "keybinding", "%1$s[<color_yellow>%2$s</color>]%3$s" );
436 static const translation separate_fmt = to_translation(
437 //~ %1$s: key description,
438 //~ %2$s: action description.
439 "keybinding", "[<color_yellow>%1$s</color>]%2$s" );
440 std::vector<std::string> act_descs;
441 const auto add_action_desc = [&]( const std::string & act, const std::string & txt ) {
442 act_descs.emplace_back( ctxt.get_desc( act, txt, input_context::allow_all_keys,
443 inline_fmt, separate_fmt ) );
444 };
445 add_action_desc( "CONFIRM", pgettext( "crafting gui", "Craft" ) );
446 add_action_desc( "HELP_RECIPE", pgettext( "crafting gui", "Describe" ) );
447 add_action_desc( "FILTER", pgettext( "crafting gui", "Filter" ) );
448 add_action_desc( "RESET_FILTER", pgettext( "crafting gui", "Reset filter" ) );
449 add_action_desc( "HIDE_SHOW_RECIPE", pgettext( "crafting gui", "Show/hide" ) );
450 add_action_desc( "RELATED_RECIPES", pgettext( "crafting gui", "Related" ) );
451 add_action_desc( "TOGGLE_FAVORITE", pgettext( "crafting gui", "Favorite" ) );
452 add_action_desc( "CYCLE_BATCH", pgettext( "crafting gui", "Batch" ) );
453 add_action_desc( "HELP_KEYBINDINGS", pgettext( "crafting gui", "Keybindings" ) );
454 keybinding_x = isWide ? 5 : 2;
455 keybinding_tips = foldstring( enumerate_as_string( act_descs, enumeration_conjunction::none ),
456 width - keybinding_x * 2 );
457
458 const int tailHeight = keybinding_tips.size() + 2;
459 dataLines = TERMY - ( headHeight + subHeadHeight ) - tailHeight;
460 dataHalfLines = dataLines / 2;
461 dataHeight = TERMY - ( headHeight + subHeadHeight );
462
463 w_head = catacurses::newwin( headHeight, width, point( wStart, 0 ) );
464 w_subhead = catacurses::newwin( subHeadHeight, width, point( wStart, 3 ) );
465 w_data = catacurses::newwin( dataHeight, width, point( wStart,
466 headHeight + subHeadHeight ) );
467
468 if( isWide ) {
469 item_info_width = width - FULL_SCREEN_WIDTH - 1;
470 const int item_info_height = dataHeight - tailHeight;
471 const point item_info( wStart + width - item_info_width, headHeight + subHeadHeight );
472
473 w_iteminfo = catacurses::newwin( item_info_height, item_info_width,
474 item_info );
475 } else {
476 item_info_width = 0;
477 w_iteminfo = {};
478 }
479
480 ui.position( point( wStart, 0 ), point( width, TERMY ) );
481 } );
482 ui.mark_resize();
483
484 list_circularizer<std::string> tab( craft_cat_list );
485 list_circularizer<std::string> subtab( craft_subcat_list[tab.cur()] );
486 std::vector<const recipe *> current;
487 std::vector<availability> available;
488 int line = 0;
489 bool recalc = true;
490 bool keepline = false;
491 bool done = false;
492 bool batch = false;
493 bool show_hidden = false;
494 size_t num_hidden = 0;
495 int num_recipe = 0;
496 int batch_line = 0;
497 const recipe *chosen = nullptr;
498
499 Character &player_character = get_player_character();
500 const inventory &crafting_inv = player_character.crafting_inventory();
501 const std::vector<npc *> helpers = player_character.get_crafting_helpers();
502 std::string filterstring;
503
504 const auto &available_recipes = player_character.get_available_recipes( crafting_inv, &helpers );
505 std::map<const recipe *, availability> availability_cache;
506
507 ui.on_redraw( [&]( const ui_adaptor & ) {
508 const TAB_MODE m = ( batch ) ? BATCH : ( filterstring.empty() ) ? NORMAL : FILTERED;
509 draw_recipe_tabs( w_head, tab.cur(), m );
510 draw_recipe_subtabs( w_subhead, tab.cur(), subtab.cur(), available_recipes, m );
511
512 if( !show_hidden ) {
513 draw_hidden_amount( w_head, num_hidden, num_recipe );
514 }
515
516 // Clear the screen of recipe data, and draw it anew
517 werase( w_data );
518
519 for( size_t i = 0; i < keybinding_tips.size(); ++i ) {
520 nc_color dummy = c_white;
521 print_colored_text( w_data, point( keybinding_x, dataLines + 1 + i ),
522 dummy, c_white, keybinding_tips[i] );
523 }
524
525 // Draw borders
526 for( int i = 1; i < width - 1; ++i ) { // -
527 mvwputch( w_data, point( i, dataHeight - 1 ), BORDER_COLOR, LINE_OXOX );
528 }
529 for( int i = 0; i < dataHeight - 1; ++i ) { // |
530 mvwputch( w_data, point( 0, i ), BORDER_COLOR, LINE_XOXO );
531 mvwputch( w_data, point( width - 1, i ), BORDER_COLOR, LINE_XOXO );
532 }
533 mvwputch( w_data, point( 0, dataHeight - 1 ), BORDER_COLOR, LINE_XXOO ); // |_
534 mvwputch( w_data, point( width - 1, dataHeight - 1 ), BORDER_COLOR, LINE_XOOX ); // _|
535
536 const int max_recipe_name_width = 27;
537 cata::optional<point> cursor_pos;
538 int recmin = 0, recmax = current.size();
539 if( recmax > dataLines ) {
540 if( line <= recmin + dataHalfLines ) {
541 for( int i = recmin; i < recmin + dataLines; ++i ) {
542 std::string tmp_name = current[i]->result_name();
543 if( batch ) {
544 tmp_name = string_format( _( "%2dx %s" ), i + 1, tmp_name );
545 }
546 mvwprintz( w_data, point( 2, i - recmin ), c_dark_gray, "" ); // Clear the line
547 const bool highlight = i == line;
548 const nc_color col = highlight ? available[i].selected_color() : available[i].color();
549 const point print_from( 2, i - recmin );
550 if( highlight ) {
551 cursor_pos = print_from;
552 }
553 mvwprintz( w_data, print_from, col, trim_by_length( tmp_name, max_recipe_name_width ) );
554 }
555 } else if( line >= recmax - dataHalfLines ) {
556 for( int i = recmax - dataLines; i < recmax; ++i ) {
557 std::string tmp_name = current[i]->result_name();
558 if( batch ) {
559 tmp_name = string_format( _( "%2dx %s" ), i + 1, tmp_name );
560 }
561 mvwprintz( w_data, point( 2, dataLines + i - recmax ), c_light_gray, "" ); // Clear the line
562 const bool highlight = i == line;
563 const nc_color col = highlight ? available[i].selected_color() : available[i].color();
564 const point print_from( 2, dataLines + i - recmax );
565 if( highlight ) {
566 cursor_pos = print_from;
567 }
568 mvwprintz( w_data, print_from, col,
569 trim_by_length( tmp_name, max_recipe_name_width ) );
570 }
571 } else {
572 for( int i = line - dataHalfLines; i < line - dataHalfLines + dataLines; ++i ) {
573 std::string tmp_name = current[i]->result_name();
574 if( batch ) {
575 tmp_name = string_format( _( "%2dx %s" ), i + 1, tmp_name );
576 }
577 mvwprintz( w_data, point( 2, dataHalfLines + i - line ), c_light_gray, "" ); // Clear the line
578 const bool highlight = i == line;
579 const nc_color col = highlight ? available[i].selected_color() : available[i].color();
580 const point print_from( 2, dataHalfLines + i - line );
581 if( highlight ) {
582 cursor_pos = print_from;
583 }
584 mvwprintz( w_data, print_from, col,
585 trim_by_length( tmp_name, max_recipe_name_width ) );
586 }
587 }
588 } else {
589 for( int i = 0; i < static_cast<int>( current.size() ) && i < dataHeight + 1; ++i ) {
590 std::string tmp_name = current[i]->result_name();
591 if( batch ) {
592 tmp_name = string_format( _( "%2dx %s" ), i + 1, tmp_name );
593 }
594 const bool highlight = i == line;
595 const nc_color col = highlight ? available[i].selected_color() : available[i].color();
596 const point print_from( 2, i );
597 if( highlight ) {
598 cursor_pos = print_from;
599 }
600 mvwprintz( w_data, print_from, col, trim_by_length( tmp_name, max_recipe_name_width ) );
601 }
602 }
603
604 const int batch_size = batch ? line + 1 : 1;
605 if( !current.empty() ) {
606 const recipe &recp = *current[line];
607
608 draw_can_craft_indicator( w_head, recp );
609 wnoutrefresh( w_head );
610
611 const availability &avail = available[line];
612 // border + padding + name + padding
613 const int xpos = 1 + 1 + max_recipe_name_width + 3;
614 const int fold_width = FULL_SCREEN_WIDTH - xpos - 2;
615 const nc_color color = avail.color( true );
616 const std::string qry = trim( filterstring );
617 std::string qry_comps;
618 if( qry.compare( 0, 2, "c:" ) == 0 ) {
619 qry_comps = qry.substr( 2 );
620 }
621
622 const std::vector<std::string> &info = cached_recipe_info(
623 recp, avail, player_character, qry_comps, batch_size, fold_width, color );
624
625 const int total_lines = info.size();
626 if( recipe_info_scroll < 0 ) {
627 recipe_info_scroll = 0;
628 } else if( recipe_info_scroll + dataLines > total_lines ) {
629 recipe_info_scroll = std::max( 0, total_lines - dataLines );
630 }
631 for( int i = recipe_info_scroll;
632 i < std::min( recipe_info_scroll + dataLines, total_lines );
633 ++i ) {
634 nc_color dummy = color;
635 print_colored_text( w_data, point( xpos, i - recipe_info_scroll ),
636 dummy, color, info[i] );
637 }
638
639 if( total_lines > dataLines ) {
640 scrollbar().offset_x( xpos + fold_width + 1 ).content_size( total_lines )
641 .viewport_pos( recipe_info_scroll ).viewport_size( dataLines )
642 .apply( w_data );
643 }
644 }
645
646 draw_scrollbar( w_data, line, dataLines, recmax, point_zero );
647 wnoutrefresh( w_data );
648
649 if( isWide && !current.empty() ) {
650 item_info_data data = item_info_data_from_recipe( current[line], batch_size, item_info_scroll );
651 data.without_getch = true;
652 data.without_border = true;
653 data.scrollbar_left = false;
654 data.use_full_win = true;
655 draw_item_info( w_iteminfo, data );
656 }
657
658 if( cursor_pos ) {
659 // place the cursor at the selected item name as expected by screen readers
660 wmove( w_data, cursor_pos.value() );
661 wnoutrefresh( w_data );
662 }
663 } );
664
665 do {
666 if( recalc ) {
667 // When we switch tabs, redraw the header
668 recalc = false;
669 if( !keepline ) {
670 line = 0;
671 } else {
672 keepline = false;
673 }
674
675 show_hidden = false;
676 available.clear();
677
678 if( batch ) {
679 current.clear();
680 for( int i = 1; i <= 50; i++ ) {
681 current.push_back( chosen );
682 available.push_back( availability( chosen, i ) );
683 }
684 } else {
685 static_popup popup;
686 auto last_update = std::chrono::steady_clock::now();
687 static constexpr std::chrono::milliseconds update_interval( 500 );
688
689 std::function<void( size_t, size_t )> progress_callback =
690 [&]( size_t at, size_t out_of ) {
691 auto now = std::chrono::steady_clock::now();
692 if( now - last_update < update_interval ) {
693 return;
694 }
695 last_update = now;
696 double percent = 100.0 * at / out_of;
697 popup.message( _( "Searching… %3.0f%%\n" ), percent );
698 ui_manager::redraw();
699 refresh_display();
700 };
701
702 std::vector<const recipe *> picking;
703 if( !filterstring.empty() ) {
704 auto qry = trim( filterstring );
705 size_t qry_begin = 0;
706 size_t qry_end = 0;
707 recipe_subset filtered_recipes = available_recipes;
708 do {
709 // Find next ','
710 qry_end = qry.find_first_of( ',', qry_begin );
711
712 auto qry_filter_str = trim( qry.substr( qry_begin, qry_end - qry_begin ) );
713 // Process filter
714 if( qry_filter_str.size() > 2 && qry_filter_str[1] == ':' ) {
715 switch( qry_filter_str[0] ) {
716 case 't':
717 filtered_recipes = filtered_recipes.reduce( qry_filter_str.substr( 2 ),
718 recipe_subset::search_type::tool, progress_callback );
719 break;
720
721 case 'c':
722 filtered_recipes = filtered_recipes.reduce( qry_filter_str.substr( 2 ),
723 recipe_subset::search_type::component, progress_callback );
724 break;
725
726 case 's':
727 filtered_recipes = filtered_recipes.reduce( qry_filter_str.substr( 2 ),
728 recipe_subset::search_type::skill, progress_callback );
729 break;
730
731 case 'p':
732 filtered_recipes = filtered_recipes.reduce( qry_filter_str.substr( 2 ),
733 recipe_subset::search_type::primary_skill, progress_callback );
734 break;
735
736 case 'Q':
737 filtered_recipes = filtered_recipes.reduce( qry_filter_str.substr( 2 ),
738 recipe_subset::search_type::quality, progress_callback );
739 break;
740
741 case 'q':
742 filtered_recipes = filtered_recipes.reduce( qry_filter_str.substr( 2 ),
743 recipe_subset::search_type::quality_result, progress_callback );
744 break;
745
746 case 'd':
747 filtered_recipes = filtered_recipes.reduce( qry_filter_str.substr( 2 ),
748 recipe_subset::search_type::description_result, progress_callback );
749 break;
750
751 case 'm': {
752 const recipe_subset &learned = player_character.get_learned_recipes();
753 recipe_subset temp_subset;
754 if( query_is_yes( qry_filter_str ) ) {
755 temp_subset = available_recipes.intersection( learned );
756 } else {
757 temp_subset = available_recipes.difference( learned );
758 }
759 filtered_recipes = filtered_recipes.intersection( temp_subset );
760 break;
761 }
762
763 case 'P':
764 filtered_recipes = filtered_recipes.reduce( qry_filter_str.substr( 2 ),
765 recipe_subset::search_type::proficiency, progress_callback );
766 break;
767
768 default:
769 current.clear();
770 }
771 } else if( qry_filter_str.size() > 1 && qry_filter_str[0] == '-' ) {
772 filtered_recipes = filtered_recipes.reduce( qry_filter_str.substr( 1 ),
773 recipe_subset::search_type::exclude_name, progress_callback );
774 } else {
775 filtered_recipes = filtered_recipes.reduce( qry_filter_str );
776 }
777
778 qry_begin = qry_end + 1;
779 } while( qry_end != std::string::npos );
780 picking.insert( picking.end(), filtered_recipes.begin(), filtered_recipes.end() );
781 } else if( subtab.cur() == "CSC_*_FAVORITE" ) {
782 picking = available_recipes.favorite();
783 } else if( subtab.cur() == "CSC_*_RECENT" ) {
784 picking = available_recipes.recent();
785 } else if( subtab.cur() == "CSC_*_HIDDEN" ) {
786 current = available_recipes.hidden();
787 show_hidden = true;
788 } else {
789 picking = available_recipes.in_category( tab.cur(), subtab.cur() != "CSC_ALL" ? subtab.cur() : "" );
790 }
791
792 if( !show_hidden ) {
793 current.clear();
794 for( const recipe *i : picking ) {
795 if( uistate.hidden_recipes.find( i->ident() ) == uistate.hidden_recipes.end() ) {
796 current.push_back( i );
797 }
798 }
799 num_hidden = picking.size() - current.size();
800 num_recipe = picking.size();
801 }
802
803 available.reserve( current.size() );
804 // cache recipe availability on first display
805 for( const recipe *e : current ) {
806 if( !availability_cache.count( e ) ) {
807 availability_cache.emplace( e, availability( e ) );
808 }
809 }
810
811 if( subtab.cur() != "CSC_*_RECENT" ) {
812 std::stable_sort( current.begin(), current.end(),
813 []( const recipe * a, const recipe * b ) {
814 return b->difficulty < a->difficulty;
815 } );
816
817 std::stable_sort( current.begin(), current.end(),
818 [&]( const recipe * a, const recipe * b ) {
819 return availability_cache.at( a ).can_craft &&
820 !availability_cache.at( b ).can_craft;
821 } );
822 }
823
824 std::transform( current.begin(), current.end(),
825 std::back_inserter( available ), [&]( const recipe * e ) {
826 return availability_cache.at( e );
827 } );
828 }
829
830 // current/available have been rebuilt, make sure our cursor is still in range
831 if( current.empty() ) {
832 line = 0;
833 } else {
834 line = std::min( line, static_cast<int>( current.size() ) - 1 );
835 }
836 }
837
838 ui_manager::redraw();
839 const int scroll_item_info_lines = catacurses::getmaxy( w_iteminfo ) - 4;
840 const std::string action = ctxt.handle_input();
841 const int recmax = static_cast<int>( current.size() );
842 const int scroll_rate = recmax > 20 ? 10 : 3;
843 if( action == "SCROLL_RECIPE_INFO_UP" ) {
844 recipe_info_scroll -= dataLines;
845 } else if( action == "SCROLL_RECIPE_INFO_DOWN" ) {
846 recipe_info_scroll += dataLines;
847 } else if( action == "LEFT" ) {
848 std::string start = subtab.cur();
849 do {
850 subtab.prev();
851 } while( subtab.cur() != start && available_recipes.empty_category( tab.cur(),
852 subtab.cur() != "CSC_ALL" ? subtab.cur() : "" ) );
853 recalc = true;
854 } else if( action == "SCROLL_ITEM_INFO_UP" ) {
855 item_info_scroll -= scroll_item_info_lines;
856 } else if( action == "SCROLL_ITEM_INFO_DOWN" ) {
857 item_info_scroll += scroll_item_info_lines;
858 } else if( action == "PREV_TAB" ) {
859 tab.prev();
860 // Default ALL
861 subtab = list_circularizer<std::string>( craft_subcat_list[tab.cur()] );
862 recalc = true;
863 } else if( action == "RIGHT" ) {
864 std::string start = subtab.cur();
865 do {
866 subtab.next();
867 } while( subtab.cur() != start && available_recipes.empty_category( tab.cur(),
868 subtab.cur() != "CSC_ALL" ? subtab.cur() : "" ) );
869 recalc = true;
870 } else if( action == "NEXT_TAB" ) {
871 tab.next();
872 // Default ALL
873 subtab = list_circularizer<std::string>( craft_subcat_list[tab.cur()] );
874 recalc = true;
875 } else if( action == "DOWN" ) {
876 line++;
877 } else if( action == "UP" ) {
878 line--;
879 } else if( action == "PAGE_DOWN" ) {
880 if( line == recmax - 1 ) {
881 line = 0;
882 } else if( line + scroll_rate >= recmax ) {
883 line = recmax - 1;
884 } else {
885 line += +scroll_rate;
886 }
887 } else if( action == "PAGE_UP" ) {
888 if( line == 0 ) {
889 line = recmax - 1;
890 } else if( line <= scroll_rate ) {
891 line = 0;
892 } else {
893 line += -scroll_rate;
894 }
895 } else if( action == "CONFIRM" ) {
896 if( available.empty() || !available[line].can_craft ) {
897 popup( _( "You can't do that! Press [<color_yellow>ESC</color>]!" ) );
898 } else if( !player_character.check_eligible_containers_for_crafting( *current[line],
899 ( batch ) ? line + 1 : 1 ) ) {
900 // popup is already inside check
901 } else {
902 chosen = current[line];
903 batch_size_out = ( batch ) ? line + 1 : 1;
904 done = true;
905 }
906 } else if( action == "HELP_RECIPE" ) {
907 if( current.empty() ) {
908 popup( _( "Nothing selected! Press [<color_yellow>ESC</color>]!" ) );
909 recalc = true;
910 continue;
911 }
912 item_info_data data = item_info_data_from_recipe( current[line], 1, item_info_scroll_popup );
913 data.handle_scrolling = true;
914 draw_item_info( []() -> catacurses::window {
915 const int width = std::min( TERMX, FULL_SCREEN_WIDTH );
916 const int height = std::min( TERMY, FULL_SCREEN_HEIGHT );
917 return catacurses::newwin( height, width, point( ( TERMX - width ) / 2, ( TERMY - height ) / 2 ) );
918 }, data );
919
920 recalc = true;
921 keepline = true;
922 } else if( action == "FILTER" ) {
923 struct SearchPrefix {
924 char key;
925 std::string example;
926 std::string description;
927 };
928 std::vector<SearchPrefix> prefixes = {
929 //~ Example result description search term
930 { 'q', _( "metal sawing" ), _( "<color_cyan>quality</color> of resulting item" ) },
931 { 'd', _( "reach attack" ), _( "<color_cyan>full description</color> of resulting item (slow)" ) },
932 { 'c', _( "two by four" ), _( "<color_cyan>component</color> required to craft" ) },
933 { 'p', _( "tailoring" ), _( "<color_cyan>primary skill</color> used to craft" ) },
934 { 's', _( "cooking" ), _( "<color_cyan>any skill</color> used to craft" ) },
935 { 'Q', _( "fine bolt turning" ), _( "<color_cyan>quality</color> required to craft" ) },
936 { 't', _( "soldering iron" ), _( "<color_cyan>tool</color> required to craft" ) },
937 { 'm', _( "yes" ), _( "recipes which are <color_cyan>memorized</color> or not" ) },
938 { 'P', _( "Blacksmithing" ), _( "<color_cyan>proficiency</color> used to craft" ) },
939 };
940 int max_example_length = 0;
941 for( const auto &prefix : prefixes ) {
942 max_example_length = std::max( max_example_length, utf8_width( prefix.example ) );
943 }
944 std::string spaces( max_example_length, ' ' );
945
946 std::string description =
947 _( "The default is to search result names. Some single-character prefixes "
948 "can be used with a colon <color_red>:</color> to search in other ways. Additional filters "
949 "are separated by commas <color_red>,</color>.\n"
950 "\n\n"
951 "<color_white>Examples:</color>\n" );
952
953 {
954 std::string example_name = _( "shirt" );
955 int padding = max_example_length - utf8_width( example_name );
956 description += string_format(
957 _( " <color_white>%s</color>%.*s %s\n" ),
958 example_name, padding, spaces,
959 _( "<color_cyan>name</color> of resulting item" ) );
960
961 std::string example_exclude = _( "clean" );
962 padding = max_example_length - utf8_width( example_exclude );
963 description += string_format(
964 _( " <color_yellow>-</color><color_white>%s</color>%.*s %s\n" ),
965 example_exclude, padding, spaces,
966 _( "<color_cyan>names</color> to exclude" ) );
967 }
968
969 for( const auto &prefix : prefixes ) {
970 int padding = max_example_length - utf8_width( prefix.example );
971 description += string_format(
972 _( " <color_yellow>%c</color><color_white>:%s</color>%.*s %s\n" ),
973 prefix.key, prefix.example, padding, spaces, prefix.description );
974 }
975
976 description +=
977 _( "\nUse <color_red>up/down arrow</color> to go through your search history." );
978 description += "\n\n\n";
979
980 string_input_popup popup;
981 popup
982 .title( _( "Search:" ) )
983 .width( 85 )
984 .description( description )
985 .desc_color( c_light_gray )
986 .identifier( "craft_recipe_filter" )
987 .hist_use_uilist( false )
988 .edit( filterstring );
989
990 if( popup.confirmed() ) {
991 recalc = true;
992 if( batch ) {
993 // exit from batch selection
994 batch = false;
995 line = batch_line;
996 }
997 }
998 } else if( action == "QUIT" ) {
999 chosen = nullptr;
1000 done = true;
1001 } else if( action == "RESET_FILTER" ) {
1002 filterstring.clear();
1003 recalc = true;
1004 } else if( action == "CYCLE_BATCH" ) {
1005 if( current.empty() ) {
1006 popup( _( "Nothing selected! Press [<color_yellow>ESC</color>]!" ) );
1007 recalc = true;
1008 continue;
1009 }
1010 batch = !batch;
1011 if( batch ) {
1012 batch_line = line;
1013 chosen = current[batch_line];
1014 } else {
1015 line = batch_line;
1016 keepline = true;
1017 }
1018 recalc = true;
1019 } else if( action == "TOGGLE_FAVORITE" ) {
1020 keepline = true;
1021 recalc = true;
1022 if( current.empty() ) {
1023 popup( _( "Nothing selected! Press [<color_yellow>ESC</color>]!" ) );
1024 continue;
1025 }
1026 if( uistate.favorite_recipes.find( current[line]->ident() ) != uistate.favorite_recipes.end() ) {
1027 uistate.favorite_recipes.erase( current[line]->ident() );
1028 } else {
1029 uistate.favorite_recipes.insert( current[line]->ident() );
1030 }
1031 } else if( action == "HIDE_SHOW_RECIPE" ) {
1032 if( current.empty() ) {
1033 popup( _( "Nothing selected! Press [<color_yellow>ESC</color>]!" ) );
1034 recalc = true;
1035 continue;
1036 }
1037 if( show_hidden ) {
1038 uistate.hidden_recipes.erase( current[line]->ident() );
1039 } else {
1040 uistate.hidden_recipes.insert( current[line]->ident() );
1041 }
1042
1043 recalc = true;
1044 } else if( action == "RELATED_RECIPES" ) {
1045 if( current.empty() ) {
1046 popup( _( "Nothing selected! Press [<color_yellow>ESC</color>]!" ) );
1047 recalc = true;
1048 continue;
1049 }
1050 std::string recipe_name = peek_related_recipe( current[line], available_recipes );
1051 if( recipe_name.empty() ) {
1052 keepline = true;
1053 } else {
1054 filterstring = recipe_name;
1055 }
1056
1057 recalc = true;
1058 } else if( action == "HELP_KEYBINDINGS" ) {
1059 // Regenerate keybinding tips
1060 ui.mark_resize();
1061 }
1062 if( line < 0 ) {
1063 line = current.size() - 1;
1064 } else if( line >= static_cast<int>( current.size() ) ) {
1065 line = 0;
1066 }
1067 } while( !done );
1068
1069 return chosen;
1070 }
1071
peek_related_recipe(const recipe * current,const recipe_subset & available)1072 std::string peek_related_recipe( const recipe *current, const recipe_subset &available )
1073 {
1074 auto compare_second =
1075 []( const std::pair<itype_id, std::string> &a,
1076 const std::pair<itype_id, std::string> &b ) {
1077 return localized_compare( a.second, b.second );
1078 };
1079
1080 // current recipe components
1081 std::vector<std::pair<itype_id, std::string>> related_components;
1082 const requirement_data &req = current->simple_requirements();
1083 for( const std::vector<item_comp> &comp_list : req.get_components() ) {
1084 for( const item_comp &a : comp_list ) {
1085 related_components.push_back( { a.type, item::nname( a.type, 1 ) } );
1086 }
1087 }
1088 std::sort( related_components.begin(), related_components.end(), compare_second );
1089 // current recipe result
1090 std::vector<std::pair<itype_id, std::string>> related_results;
1091 item tmp = current->create_result();
1092 // use this item
1093 const itype_id tid = tmp.typeId();
1094 const std::set<const recipe *> &known_recipes =
1095 get_player_character().get_learned_recipes().of_component( tid );
1096 for( const auto &b : known_recipes ) {
1097 if( available.contains( b ) ) {
1098 related_results.push_back( { b->result(), b->result_name() } );
1099 }
1100 }
1101 std::stable_sort( related_results.begin(), related_results.end(), compare_second );
1102
1103 uilist rel_menu;
1104 int np_last = -1;
1105 if( !related_components.empty() ) {
1106 rel_menu.addentry( ++np_last, false, -1, _( "COMPONENTS" ) );
1107 }
1108 np_last = related_menu_fill( rel_menu, related_components, available );
1109 if( !related_results.empty() ) {
1110 rel_menu.addentry( ++np_last, false, -1, _( "RESULTS" ) );
1111 }
1112
1113 related_menu_fill( rel_menu, related_results, available );
1114
1115 rel_menu.settext( _( "Related recipes:" ) );
1116 rel_menu.query();
1117 if( rel_menu.ret != UILIST_CANCEL ) {
1118 return rel_menu.entries[rel_menu.ret].txt.substr( strlen( "─ " ) );
1119 }
1120
1121 return "";
1122 }
1123
related_menu_fill(uilist & rmenu,const std::vector<std::pair<itype_id,std::string>> & related_recipes,const recipe_subset & available)1124 int related_menu_fill( uilist &rmenu,
1125 const std::vector<std::pair<itype_id, std::string>> &related_recipes,
1126 const recipe_subset &available )
1127 {
1128 const std::vector<uilist_entry> &entries = rmenu.entries;
1129 int np_last = entries.empty() ? -1 : entries.back().retval;
1130
1131 if( related_recipes.empty() ) {
1132 return np_last;
1133 }
1134
1135 std::string recipe_name_prev;
1136 for( const std::pair<itype_id, std::string> &p : related_recipes ) {
1137
1138 // we have different recipes with the same names
1139 // list only one of them as we show and filter by name only
1140 std::string recipe_name = p.second;
1141 if( recipe_name == recipe_name_prev ) {
1142 continue;
1143 }
1144 recipe_name_prev = recipe_name;
1145
1146 std::vector<const recipe *> current_part = available.search_result( p.first );
1147 if( !current_part.empty() ) {
1148
1149 bool different_recipes = false;
1150
1151 // 1st pass: check if we need to add group
1152 for( size_t recipe_n = 0; recipe_n < current_part.size(); recipe_n++ ) {
1153 if( current_part[recipe_n]->result_name() != recipe_name ) {
1154 // add group
1155 rmenu.addentry( ++np_last, false, -1, recipe_name );
1156 different_recipes = true;
1157 break;
1158 } else if( recipe_n == current_part.size() - 1 ) {
1159 // only one result
1160 rmenu.addentry( ++np_last, true, -1, "─ " + recipe_name );
1161 }
1162 }
1163
1164 if( different_recipes ) {
1165 std::string prev_item_name;
1166 // 2nd pass: add different recipes
1167 for( size_t recipe_n = 0; recipe_n < current_part.size(); recipe_n++ ) {
1168 std::string cur_item_name = current_part[recipe_n]->result_name();
1169 if( cur_item_name != prev_item_name ) {
1170 std::string sym = recipe_n == current_part.size() - 1 ? "└ " : "├ ";
1171 rmenu.addentry( ++np_last, true, -1, sym + cur_item_name );
1172 }
1173 prev_item_name = cur_item_name;
1174 }
1175 }
1176 }
1177 }
1178
1179 return np_last;
1180 }
1181
query_is_yes(const std::string & query)1182 static bool query_is_yes( const std::string &query )
1183 {
1184 const std::string subquery = query.substr( 2 );
1185
1186 return subquery == "yes" || subquery == "y" || subquery == "1" ||
1187 subquery == "true" || subquery == "t" || subquery == "on" ||
1188 subquery == _( "yes" );
1189 }
1190
draw_hidden_amount(const catacurses::window & w,int amount,int num_recipe)1191 static void draw_hidden_amount( const catacurses::window &w, int amount, int num_recipe )
1192 {
1193 if( amount == 1 ) {
1194 right_print( w, 1, 1, c_red, string_format( _( "* %s hidden recipe - %s in category *" ), amount,
1195 num_recipe ) );
1196 } else if( amount >= 2 ) {
1197 right_print( w, 1, 1, c_red, string_format( _( "* %s hidden recipes - %s in category *" ), amount,
1198 num_recipe ) );
1199 } else if( amount == 0 ) {
1200 right_print( w, 1, 1, c_green, string_format( _( "* No hidden recipe - %s in category *" ),
1201 num_recipe ) );
1202 }
1203 }
1204
1205 // Anchors top-right
draw_can_craft_indicator(const catacurses::window & w,const recipe & rec)1206 static void draw_can_craft_indicator( const catacurses::window &w, const recipe &rec )
1207 {
1208 Character &player_character = get_player_character();
1209 // Draw text
1210 if( player_character.lighting_craft_speed_multiplier( rec ) <= 0.0f ) {
1211 right_print( w, 0, 1, i_red, _( "too dark to craft" ) );
1212 } else if( player_character.crafting_speed_multiplier( rec ) <= 0.0f ) {
1213 // Technically not always only too sad, but must be too sad
1214 right_print( w, 0, 1, i_red, _( "too sad to craft" ) );
1215 } else if( player_character.crafting_speed_multiplier( rec ) < 1.0f ) {
1216 right_print( w, 0, 1, i_yellow, string_format( _( "crafting is slow %d%%" ),
1217 static_cast<int>( player_character.crafting_speed_multiplier( rec ) * 100 ) ) );
1218 } else {
1219 right_print( w, 0, 1, i_green, _( "craftable" ) );
1220 }
1221 }
1222
draw_recipe_tabs(const catacurses::window & w,const std::string & tab,TAB_MODE mode)1223 static void draw_recipe_tabs( const catacurses::window &w, const std::string &tab, TAB_MODE mode )
1224 {
1225 werase( w );
1226
1227 switch( mode ) {
1228 case NORMAL: {
1229 draw_tabs( w, normalized_names, craft_cat_list, tab );
1230 break;
1231 }
1232 case FILTERED:
1233 mvwhline( w, point( 0, getmaxy( w ) - 1 ), LINE_OXOX, getmaxx( w ) - 1 );
1234 mvwputch( w, point( 0, getmaxy( w ) - 1 ), BORDER_COLOR, LINE_OXXO ); // |^
1235 mvwputch( w, point( getmaxx( w ) - 1, getmaxy( w ) - 1 ), BORDER_COLOR, LINE_OOXX ); // ^|
1236 draw_tab( w, 2, _( "Searched" ), true );
1237 break;
1238 case BATCH:
1239 mvwhline( w, point( 0, getmaxy( w ) - 1 ), LINE_OXOX, getmaxx( w ) - 1 );
1240 mvwputch( w, point( 0, getmaxy( w ) - 1 ), BORDER_COLOR, LINE_OXXO ); // |^
1241 mvwputch( w, point( getmaxx( w ) - 1, getmaxy( w ) - 1 ), BORDER_COLOR, LINE_OOXX ); // ^|
1242 draw_tab( w, 2, _( "Batch" ), true );
1243 break;
1244 }
1245
1246 wnoutrefresh( w );
1247 }
1248
draw_recipe_subtabs(const catacurses::window & w,const std::string & tab,const std::string & subtab,const recipe_subset & available_recipes,TAB_MODE mode)1249 static void draw_recipe_subtabs( const catacurses::window &w, const std::string &tab,
1250 const std::string &subtab,
1251 const recipe_subset &available_recipes, TAB_MODE mode )
1252 {
1253 werase( w );
1254 int width = getmaxx( w );
1255 for( int i = 0; i < width; i++ ) {
1256 if( i == 0 ) {
1257 mvwputch( w, point( i, 2 ), BORDER_COLOR, LINE_XXXO ); // |-
1258 } else if( i == width ) { // TODO: that is always false!
1259 mvwputch( w, point( i, 2 ), BORDER_COLOR, LINE_XOXX ); // -|
1260 } else {
1261 mvwputch( w, point( i, 2 ), BORDER_COLOR, LINE_OXOX ); // -
1262 }
1263 }
1264
1265 for( int i = 0; i < 3; i++ ) {
1266 mvwputch( w, point( 0, i ), BORDER_COLOR, LINE_XOXO ); // |
1267 mvwputch( w, point( width - 1, i ), BORDER_COLOR, LINE_XOXO ); // |
1268 }
1269
1270 switch( mode ) {
1271 case NORMAL: {
1272 // Draw the tabs on each other
1273 int pos_x = 2;
1274 // Step between tabs, two for tabs border
1275 int tab_step = 3;
1276 for( const auto &stt : craft_subcat_list[tab] ) {
1277 bool empty = available_recipes.empty_category( tab, stt != "CSC_ALL" ? stt : "" );
1278 draw_subtab( w, pos_x, normalized_names[stt], subtab == stt, true, empty );
1279 pos_x += utf8_width( normalized_names[stt] ) + tab_step;
1280 }
1281 break;
1282 }
1283 case FILTERED:
1284 case BATCH:
1285 werase( w );
1286 for( int i = 0; i < 3; i++ ) {
1287 mvwputch( w, point( 0, i ), BORDER_COLOR, LINE_XOXO ); // |
1288 mvwputch( w, point( width - 1, i ), BORDER_COLOR, LINE_XOXO ); // |
1289 }
1290 break;
1291 }
1292
1293 wnoutrefresh( w );
1294 }
1295
1296 template<typename T>
lcmatch_any(const std::vector<std::vector<T>> & list_of_list,const std::string & filter)1297 bool lcmatch_any( const std::vector< std::vector<T> > &list_of_list, const std::string &filter )
1298 {
1299 for( auto &list : list_of_list ) {
1300 for( auto &comp : list ) {
1301 if( lcmatch( item::nname( comp.type ), filter ) ) {
1302 return true;
1303 }
1304 }
1305 }
1306 return false;
1307 }
1308
subcategories_for_category(const std::string & category)1309 const std::vector<std::string> *subcategories_for_category( const std::string &category )
1310 {
1311 auto it = craft_subcat_list.find( category );
1312 if( it != craft_subcat_list.end() ) {
1313 return &it->second;
1314 }
1315 return nullptr;
1316 }
1317