1 #include "player.h" // IWYU pragma: associated
2 
3 #include <algorithm> //std::min
4 #include <cstddef>
5 #include <functional>
6 #include <new>
7 #include <string>
8 #include <unordered_map>
9 
10 #include "avatar.h"
11 #include "color.h"
12 #include "cursesdef.h"
13 #include "enums.h"
14 #include "input.h"
15 #include "inventory.h"
16 #include "mutation.h"
17 #include "output.h"
18 #include "popup.h"
19 #include "string_formatter.h"
20 #include "translations.h"
21 #include "ui_manager.h"
22 
23 // '!' and '=' are uses as default bindings in the menu
24 static const invlet_wrapper
25 mutation_chars( "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\"#&()*+./:;@[\\]^_{|}" );
26 
draw_exam_window(const catacurses::window & win,const int border_y)27 static void draw_exam_window( const catacurses::window &win, const int border_y )
28 {
29     const int width = getmaxx( win );
30     mvwputch( win, point( 0, border_y ), BORDER_COLOR, LINE_XXXO );
31     mvwhline( win, point( 1, border_y ), LINE_OXOX, width - 2 );
32     mvwputch( win, point( width - 1, border_y ), BORDER_COLOR, LINE_XOXX );
33 }
34 
35 static const auto shortcut_desc = []( const std::string &comment, const std::string &keys )
__anon7db633990102( const std::string &comment, const std::string &keys ) 36 {
37     return string_format( comment, string_format( "[<color_yellow>%s</color>]", keys ) );
38 };
39 
40 enum class mutation_menu_mode : int {
41     activating,
42     examining,
43     reassigning,
44 };
45 
show_mutations_titlebar(const catacurses::window & window,const mutation_menu_mode menu_mode,const input_context & ctxt)46 static void show_mutations_titlebar( const catacurses::window &window,
47                                      const mutation_menu_mode menu_mode, const input_context &ctxt )
48 {
49     werase( window );
50     std::string desc;
51     if( menu_mode == mutation_menu_mode::reassigning ) {
52         desc += std::string( _( "Reassigning." ) ) + "  " +
53                 _( "Select a mutation to reassign or press [<color_yellow>SPACE</color>] to cancel. " );
54     }
55     if( menu_mode == mutation_menu_mode::activating ) {
56         desc += colorize( _( "Activating" ),
57                           c_green ) + "  " + shortcut_desc( _( "%s to examine mutation, " ),
58                                   ctxt.get_desc( "TOGGLE_EXAMINE" ) );
59     }
60     if( menu_mode == mutation_menu_mode::examining ) {
61         desc += colorize( _( "Examining" ),
62                           c_light_blue ) + "  " + shortcut_desc( _( "%s to activate mutation, " ),
63                                   ctxt.get_desc( "TOGGLE_EXAMINE" ) );
64     }
65     if( menu_mode != mutation_menu_mode::reassigning ) {
66         desc += shortcut_desc( _( "%s to reassign invlet, " ), ctxt.get_desc( "REASSIGN" ) );
67     }
68     desc += shortcut_desc( _( "%s to change keybindings." ), ctxt.get_desc( "HELP_KEYBINDINGS" ) );
69     // NOLINTNEXTLINE(cata-use-named-point-constants)
70     fold_and_print( window, point( 1, 0 ), getmaxx( window ) - 1, c_white, desc );
71     wnoutrefresh( window );
72 }
73 
power_mutations()74 void avatar::power_mutations()
75 {
76     std::vector<trait_id> passive;
77     std::vector<trait_id> active;
78     for( std::pair<const trait_id, trait_data> &mut : my_mutations ) {
79         if( !mut.first->activated ) {
80             passive.push_back( mut.first );
81         } else {
82             active.push_back( mut.first );
83         }
84         // New mutations are initialized with no key at all, so we have to do this here.
85         if( mut.second.key == ' ' ) {
86             for( const auto &letter : mutation_chars ) {
87                 if( trait_by_invlet( letter ).is_null() ) {
88                     mut.second.key = letter;
89                     break;
90                 }
91             }
92         }
93     }
94 
95     // maximal number of rows in both columns
96     const int mutations_count = std::max( passive.size(), active.size() );
97 
98     const int TITLE_HEIGHT = 2;
99     const int DESCRIPTION_HEIGHT = 5;
100     // + lines with text in titlebar, local
101     const int HEADER_LINE_Y = TITLE_HEIGHT + 1;
102     const int list_start_y = HEADER_LINE_Y + 2;
103 
104     // Main window
105     /** Total required height is:
106     * top frame line:                                         + 1
107     * height of title window:                                 + TITLE_HEIGHT
108     * line after the title:                                   + 1
109     * line with active/passive mutation captions:               + 1
110     * height of the biggest list of active/passive mutations:   + mutations_count
111     * line before mutation description:                         + 1
112     * height of description window:                           + DESCRIPTION_HEIGHT
113     * bottom frame line:                                      + 1
114     * TOTAL: TITLE_HEIGHT + mutations_count + DESCRIPTION_HEIGHT + 5
115     */
116 
117     int HEIGHT = 0;
118     int WIDTH = 0;
119     catacurses::window wBio;
120 
121     int DESCRIPTION_LINE_Y = 0;
122     catacurses::window w_description;
123 
124     catacurses::window w_title;
125 
126     int second_column = 0;
127 
128     int scroll_position = 0;
129     int max_scroll_position = 0;
130     int list_height = 0;
131     mutation_menu_mode menu_mode = mutation_menu_mode::activating;
132     const auto recalc_max_scroll_position = [&]() {
133         list_height = ( menu_mode == mutation_menu_mode::examining ?
134                         DESCRIPTION_LINE_Y : HEIGHT - 1 ) - list_start_y;
135         max_scroll_position = mutations_count - list_height;
136         if( max_scroll_position < 0 ) {
137             scroll_position = 0;
138         } else if( scroll_position > max_scroll_position ) {
139             scroll_position = max_scroll_position;
140         }
141     };
142 
143     ui_adaptor ui;
144     ui.on_screen_resize( [&]( ui_adaptor & ui ) {
145         HEIGHT = std::min( TERMY, std::max( FULL_SCREEN_HEIGHT,
146                                             TITLE_HEIGHT + mutations_count + DESCRIPTION_HEIGHT + 5 ) );
147         WIDTH = FULL_SCREEN_WIDTH + ( TERMX - FULL_SCREEN_WIDTH ) / 2;
148         const point START( ( TERMX - WIDTH ) / 2, ( TERMY - HEIGHT ) / 2 );
149         wBio = catacurses::newwin( HEIGHT, WIDTH, START );
150 
151         // Description window @ the bottom of the bionic window
152         const int DESCRIPTION_START_Y = START.y + HEIGHT - DESCRIPTION_HEIGHT - 1;
153         DESCRIPTION_LINE_Y = DESCRIPTION_START_Y - START.y - 1;
154         w_description = catacurses::newwin( DESCRIPTION_HEIGHT, WIDTH - 2,
155                                             point( START.x + 1, DESCRIPTION_START_Y ) );
156 
157         // Title window
158         const int TITLE_START_Y = START.y + 1;
159         w_title = catacurses::newwin( TITLE_HEIGHT, WIDTH - 2,
160                                       point( START.x + 1, TITLE_START_Y ) );
161 
162         recalc_max_scroll_position();
163 
164         // X-coordinate of the list of active mutations
165         second_column = 32 + ( TERMX - FULL_SCREEN_WIDTH ) / 4;
166 
167         ui.position_from_window( wBio );
168     } );
169     ui.mark_resize();
170 
171     input_context ctxt( "MUTATIONS", keyboard_mode::keychar );
172     ctxt.register_updown();
173     ctxt.register_action( "ANY_INPUT" );
174     ctxt.register_action( "TOGGLE_EXAMINE" );
175     ctxt.register_action( "REASSIGN" );
176     ctxt.register_action( "HELP_KEYBINDINGS" );
177     ctxt.register_action( "QUIT" );
178 #if defined(__ANDROID__)
179     for( const auto &p : passive ) {
180         ctxt.register_manual_key( my_mutations[p].key, p.obj().name() );
181     }
182     for( const auto &a : active ) {
183         ctxt.register_manual_key( my_mutations[a].key, a.obj().name() );
184     }
185 #endif
186 
187     cata::optional<trait_id> examine_id;
188 
189     ui.on_redraw( [&]( const ui_adaptor & ) {
190         werase( wBio );
191         draw_border( wBio, BORDER_COLOR, _( " MUTATIONS " ) );
192         // Draw line under title
193         mvwhline( wBio, point( 1, HEADER_LINE_Y ), LINE_OXOX, WIDTH - 2 );
194         // Draw symbols to connect additional lines to border
195         mvwputch( wBio, point( 0, HEADER_LINE_Y ), BORDER_COLOR, LINE_XXXO ); // |-
196         mvwputch( wBio, point( WIDTH - 1, HEADER_LINE_Y ), BORDER_COLOR, LINE_XOXX ); // -|
197 
198         // Captions
199         mvwprintz( wBio, point( 2, HEADER_LINE_Y + 1 ), c_light_blue, _( "Passive:" ) );
200         mvwprintz( wBio, point( second_column, HEADER_LINE_Y + 1 ), c_light_blue, _( "Active:" ) );
201 
202         if( menu_mode == mutation_menu_mode::examining ) {
203             draw_exam_window( wBio, DESCRIPTION_LINE_Y );
204         }
205         nc_color type;
206         if( passive.empty() ) {
207             mvwprintz( wBio, point( 2, list_start_y ), c_light_gray, _( "None" ) );
208         } else {
209             for( int i = scroll_position; static_cast<size_t>( i ) < passive.size(); i++ ) {
210                 const mutation_branch &md = passive[i].obj();
211                 const trait_data &td = my_mutations[passive[i]];
212                 if( i - scroll_position == list_height ) {
213                     break;
214                 }
215                 type = has_base_trait( passive[i] ) ? c_cyan : c_light_cyan;
216                 mvwprintz( wBio, point( 2, list_start_y + i - scroll_position ),
217                            type, "%c %s", td.key, md.name() );
218             }
219         }
220 
221         if( active.empty() ) {
222             mvwprintz( wBio, point( second_column, list_start_y ), c_light_gray, _( "None" ) );
223         } else {
224             for( int i = scroll_position; static_cast<size_t>( i ) < active.size(); i++ ) {
225                 const mutation_branch &md = active[i].obj();
226                 const trait_data &td = my_mutations[active[i]];
227                 if( i - scroll_position == list_height ) {
228                     break;
229                 }
230                 if( td.powered ) {
231                     type = has_base_trait( active[i] ) ? c_green : c_light_green;
232                 } else {
233                     type = has_base_trait( active[i] ) ? c_red : c_light_red;
234                 }
235                 // TODO: track resource(s) used and specify
236                 mvwputch( wBio, point( second_column, list_start_y + i - scroll_position ),
237                           type, td.key );
238                 std::string mut_desc;
239                 mut_desc += md.name();
240                 if( md.cost > 0 && md.cooldown > 0 ) {
241                     //~ RU means Resource Units
242                     mut_desc += string_format( _( " - %d RU / %d turns" ),
243                                                md.cost, md.cooldown );
244                 } else if( md.cost > 0 ) {
245                     //~ RU means Resource Units
246                     mut_desc += string_format( _( " - %d RU" ), md.cost );
247                 } else if( md.cooldown > 0 ) {
248                     mut_desc += string_format( _( " - %d turns" ), md.cooldown );
249                 }
250                 if( td.powered ) {
251                     mut_desc += _( " - Active" );
252                 }
253                 mvwprintz( wBio, point( second_column + 2, list_start_y + i - scroll_position ),
254                            type, mut_desc );
255             }
256         }
257 
258         draw_scrollbar( wBio, scroll_position, list_height, mutations_count,
259                         point( 0, list_start_y ), c_white, true );
260         wnoutrefresh( wBio );
261         show_mutations_titlebar( w_title, menu_mode, ctxt );
262 
263         if( menu_mode == mutation_menu_mode::examining && examine_id.has_value() ) {
264             werase( w_description );
265             fold_and_print( w_description, point_zero, WIDTH - 2, c_light_blue, examine_id.value()->desc() );
266             wnoutrefresh( w_description );
267         }
268     } );
269 
270     bool exit = false;
271     while( !exit ) {
272         recalc_max_scroll_position();
273         ui_manager::redraw();
274         bool handled = false;
275         const std::string action = ctxt.handle_input();
276         const input_event evt = ctxt.get_raw_input();
277         if( evt.type == input_event_t::keyboard_char && !evt.sequence.empty() ) {
278             const int ch = evt.get_first_input();
279             const trait_id mut_id = trait_by_invlet( ch );
280             if( !mut_id.is_null() ) {
281                 const mutation_branch &mut_data = mut_id.obj();
282                 switch( menu_mode ) {
283                     case mutation_menu_mode::reassigning: {
284                         query_popup pop;
285                         pop.message( _( "%s; enter new letter." ),
286                                      mutation_branch::get_name( mut_id ) )
287                         .preferred_keyboard_mode( keyboard_mode::keychar )
288                         .context( "POPUP_WAIT" )
289                         .allow_cancel( true )
290                         .allow_anykey( true );
291 
292                         bool pop_exit = false;
293                         while( !pop_exit ) {
294                             const query_popup::result ret = pop.query();
295                             bool pop_handled = false;
296                             if( ret.evt.type == input_event_t::keyboard_char && !ret.evt.sequence.empty() ) {
297                                 const int newch = ret.evt.get_first_input();
298                                 if( mutation_chars.valid( newch ) ) {
299                                     const trait_id other_mut_id = trait_by_invlet( newch );
300                                     if( !other_mut_id.is_null() ) {
301                                         std::swap( my_mutations[mut_id].key, my_mutations[other_mut_id].key );
302                                     } else {
303                                         my_mutations[mut_id].key = newch;
304                                     }
305                                     pop_exit = true;
306                                     pop_handled = true;
307                                 }
308                             }
309                             if( !pop_handled ) {
310                                 if( ret.action == "QUIT" ) {
311                                     pop_exit = true;
312                                 } else if( ret.action != "HELP_KEYBINDINGS" &&
313                                            ret.evt.type == input_event_t::keyboard_char ) {
314                                     popup( _( "Invalid mutation letter.  Only those characters are valid:\n\n%s" ),
315                                            mutation_chars.get_allowed_chars() );
316                                 }
317                             }
318                         }
319 
320                         menu_mode = mutation_menu_mode::activating;
321                         examine_id = cata::nullopt;
322                         // TODO: show a message like when reassigning a key to an item?
323                         break;
324                     }
325                     case mutation_menu_mode::activating: {
326                         if( mut_data.activated ) {
327                             if( my_mutations[mut_id].powered ) {
328                                 add_msg_if_player( m_neutral, _( "You stop using your %s." ), mut_data.name() );
329 
330                                 deactivate_mutation( mut_id );
331                                 // Action done, leave screen
332                                 exit = true;
333                             } else if( ( !mut_data.hunger || get_kcal_percent() >= 0.8f ) &&
334                                        ( !mut_data.thirst || get_thirst() <= 400 ) &&
335                                        ( !mut_data.fatigue || get_fatigue() <= 400 ) ) {
336                                 add_msg_if_player( m_neutral, _( "You activate your %s." ), mut_data.name() );
337 
338                                 activate_mutation( mut_id );
339                                 // Action done, leave screen
340                                 exit = true;
341                             } else {
342                                 popup( _( "You don't have enough in you to activate your %s!" ), mut_data.name() );
343                             }
344                         } else {
345                             popup( _( "You cannot activate %s!  To read a description of "
346                                       "%s, press '!', then '%c'." ),
347                                    mut_data.name(), mut_data.name(), my_mutations[mut_id].key );
348                         }
349                         break;
350                     }
351                     case mutation_menu_mode::examining:
352                         // Describing mutations, not activating them!
353                         examine_id = mut_id;
354                         break;
355                 }
356                 handled = true;
357             } else if( mutation_chars.valid( ch ) ) {
358                 handled = true;
359             }
360         }
361         if( !handled ) {
362             if( action == "DOWN" ) {
363                 if( scroll_position < max_scroll_position ) {
364                     scroll_position++;
365                 }
366             } else if( action == "UP" ) {
367                 if( scroll_position > 0 ) {
368                     scroll_position--;
369                 }
370             } else if( action == "REASSIGN" ) {
371                 menu_mode = mutation_menu_mode::reassigning;
372                 examine_id = cata::nullopt;
373             } else if( action == "TOGGLE_EXAMINE" ) {
374                 // switches between activation and examination
375                 menu_mode = menu_mode == mutation_menu_mode::activating ?
376                             mutation_menu_mode::examining : mutation_menu_mode::activating;
377                 examine_id = cata::nullopt;
378             } else if( action == "QUIT" ) {
379                 exit = true;
380             }
381         }
382     }
383 }
384