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