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