1 #include "player.h" // IWYU pragma: associated
2 
3 #include <algorithm>
4 #include <array>
5 #include <cmath>
6 #include <cstddef>
7 #include <cstdlib>
8 #include <functional>
9 #include <memory>
10 #include <string>
11 
12 #include "addiction.h"
13 #include "avatar.h"
14 #include "bionics.h"
15 #include "bodypart.h"
16 #include "calendar.h"
17 #include "cata_utility.h"
18 #include "catacharset.h"
19 #include "color.h"
20 #include "cursesdef.h"
21 #include "debug.h"
22 #include "effect.h"
23 #include "enum_conversions.h"
24 #include "game.h"
25 #include "input.h"
26 #include "mutation.h"
27 #include "options.h"
28 #include "output.h"
29 #include "pimpl.h"
30 #include "pldata.h"
31 #include "profession.h"
32 #include "proficiency.h"
33 #include "skill.h"
34 #include "string_formatter.h"
35 #include "string_input_popup.h"
36 #include "translations.h"
37 #include "ui_manager.h"
38 #include "units.h"
39 #include "units_utility.h"
40 #include "weather.h"
41 #include "weather_type.h"
42 
43 static const skill_id skill_swimming( "swimming" );
44 
45 static const std::string title_STATS = translate_marker( "STATS" );
46 static const std::string title_ENCUMB = translate_marker( "ENCUMBRANCE AND WARMTH" );
47 static const std::string title_EFFECTS = translate_marker( "EFFECTS" );
48 static const std::string title_SPEED = translate_marker( "SPEED" );
49 static const std::string title_SKILLS = translate_marker( "SKILLS" );
50 static const std::string title_BIONICS = translate_marker( "BIONICS" );
51 static const std::string title_TRAITS = translate_marker( "TRAITS" );
52 static const std::string title_PROFICIENCIES = translate_marker( "PROFICIENCIES" );
53 
54 // use this instead of having to type out 26 spaces like before
55 static const std::string header_spaces( 26, ' ' );
56 static const unsigned int grid_width = 26;
57 
58 // Rescale temperature value to one that the player sees
temperature_print_rescaling(int temp)59 static int temperature_print_rescaling( int temp )
60 {
61     return ( temp / 100.0 ) * 2 - 100;
62 }
63 
should_combine_bps(const player & p,const bodypart_id & l,const bodypart_id & r,const item * selected_clothing)64 static bool should_combine_bps( const player &p, const bodypart_id &l, const bodypart_id &r,
65                                 const item *selected_clothing )
66 {
67     const encumbrance_data &enc_l = p.get_part_encumbrance_data( l );
68     const encumbrance_data &enc_r = p.get_part_encumbrance_data( r );
69 
70     return l != r && // are different parts
71            l ==  r->opposite_part && r == l->opposite_part && // are complementary parts
72            // same encumbrance & temperature
73            enc_l == enc_r &&
74            temperature_print_rescaling( p.get_part_temp_conv( l ) ) == temperature_print_rescaling(
75                p.get_part_temp_conv( r ) ) &&
76            // selected_clothing covers both or neither parts
77            ( !selected_clothing || ( selected_clothing->covers( l ) == selected_clothing->covers( r ) ) );
78 
79 }
80 
list_and_combine_bps(const player & p,const item * selected_clothing)81 static std::vector<std::pair<bodypart_id, bool>> list_and_combine_bps( const player &p,
82         const item *selected_clothing )
83 {
84     // bool represents whether the part has been combined with its other half
85     std::vector<std::pair<bodypart_id, bool>> bps;
86     for( const bodypart_id &bp : p.get_all_body_parts( get_body_part_flags::sorted ) ) {
87         // assuming that a body part has at most one other half
88         if( should_combine_bps( p, bp, bp->opposite_part.id(), selected_clothing ) ) {
89             if( std::find( bps.begin(), bps.end(), std::pair<bodypart_id, bool>( bp->opposite_part.id(),
90                            true ) ) == bps.end() ) {
91                 // only add one
92                 bps.emplace_back( bp, true );
93             }
94         } else {
95             bps.emplace_back( bp, false );
96         }
97     }
98     return bps;
99 }
100 
print_encumbrance(const catacurses::window & win,const int line,const item * const selected_clothing) const101 void player::print_encumbrance( const catacurses::window &win, const int line,
102                                 const item *const selected_clothing ) const
103 {
104     // bool represents whether the part has been combined with its other half
105     const std::vector<std::pair<bodypart_id, bool>> bps = list_and_combine_bps( *this,
106             selected_clothing );
107 
108     // width/height excluding title & scrollbar
109     const int height = getmaxy( win ) - 1;
110     bool draw_scrollbar = height < static_cast<int>( bps.size() );
111     const int width = getmaxx( win ) - ( draw_scrollbar ? 1 : 0 );
112     // index of the first printed bodypart from `bps`
113     const int firstline = clamp( line - height / 2, 0, std::max( 0,
114                                  static_cast<int>( bps.size() ) - height ) );
115 
116     /*** I chose to instead only display X+Y instead of X+Y=Z. More room was needed ***
117      *** for displaying triple digit encumbrance, due to new encumbrance system.    ***
118      *** If the player wants to see the total without having to do them maths, the  ***
119      *** armor layers ui shows everything they want :-) -Davek                      ***/
120     for( int i = 0; i < height; ++i ) {
121         int thisline = firstline + i;
122         if( thisline < 0 ) {
123             continue;
124         }
125         if( static_cast<size_t>( thisline ) >= bps.size() ) {
126             break;
127         }
128 
129         const bodypart_id &bp = bps[thisline].first;
130         const bool combine = bps[thisline].second;
131         const encumbrance_data &e = get_part_encumbrance_data( bp );
132 
133         const bool highlighted = selected_clothing ? selected_clothing->covers( bp ) : false;
134         std::string out = body_part_name_as_heading( bp, combine ? 2 : 1 );
135         if( utf8_width( out ) > 7 ) {
136             out = utf8_truncate( out, 7 );
137         }
138 
139         // Two different highlighting schemes, highlight if the line is selected as per line being set.
140         // Make the text green if this part is covered by the passed in item.
141         nc_color limb_color = thisline == line ?
142                               ( highlighted ? h_green : h_light_gray ) :
143                               ( highlighted ? c_green : c_light_gray );
144         mvwprintz( win, point( 1, 1 + i ), limb_color, "%s", out );
145         // accumulated encumbrance from clothing, plus extra encumbrance from layering
146         mvwprintz( win, point( 8, 1 + i ), encumb_color( e.encumbrance ), "%3d",
147                    e.encumbrance - e.layer_penalty );
148         // separator in low toned color
149         mvwprintz( win, point( 11, 1 + i ), c_light_gray, "+" );
150         // take into account the new encumbrance system for layers
151         mvwprintz( win, point( 12, 1 + i ), encumb_color( e.encumbrance ), "%-3d", e.layer_penalty );
152         // print warmth, tethered to right hand side of the window
153         mvwprintz( win, point( width - 6, 1 + i ), bodytemp_color( bp ), "(% 3d)",
154                    temperature_print_rescaling( get_part_temp_conv( bp ) ) );
155     }
156 
157     if( draw_scrollbar ) {
158         scrollbar().
159         offset_x( width ).
160         offset_y( 1 ).
161         content_size( bps.size() ).
162         viewport_pos( firstline ).
163         viewport_size( height ).
164         scroll_to_last( false ).
165         apply( win );
166     }
167 }
168 
swim_cost_text(int moves)169 static std::string swim_cost_text( int moves )
170 {
171     return string_format( _( "Swimming movement point cost: <color_white>%+d</color>\n" ), moves );
172 }
173 
run_cost_text(int moves)174 static std::string run_cost_text( int moves )
175 {
176     return string_format( _( "Movement point cost: <color_white>%+d</color>\n" ), moves );
177 }
178 
reload_cost_text(int moves)179 static std::string reload_cost_text( int moves )
180 {
181     return string_format( _( "Reloading movement point cost: <color_white>%+d</color>\n" ), moves );
182 }
183 
melee_cost_text(int moves)184 static std::string melee_cost_text( int moves )
185 {
186     return string_format(
187                _( "Melee and thrown attack movement point cost: <color_white>%+d</color>\n" ), moves );
188 }
melee_stamina_cost_text(int cost)189 static std::string melee_stamina_cost_text( int cost )
190 {
191     return string_format( _( "Melee stamina cost: <color_white>%+d</color>\n" ), cost );
192 }
mouth_stamina_cost_text(int cost)193 static std::string mouth_stamina_cost_text( int cost )
194 {
195     return string_format( _( "Stamina Regeneration: <color_white>%+d</color>\n" ), cost );
196 }
ranged_cost_text(double disp)197 static std::string ranged_cost_text( double disp )
198 {
199     return string_format( _( "Dispersion when using ranged attacks: <color_white>%+.1f</color>\n" ),
200                           disp );
201 }
dodge_skill_text(double mod)202 static std::string dodge_skill_text( double mod )
203 {
204     return string_format( _( "Dodge skill: <color_white>%+.1f</color>\n" ), mod );
205 }
206 
get_encumbrance(const player & p,const bodypart_id & bp,bool combine)207 static int get_encumbrance( const player &p, const bodypart_id &bp, bool combine )
208 {
209     // Body parts that can't combine with anything shouldn't print double values on combine
210     // This shouldn't happen, but handle this, just in case
211     const bool combines_with_other = bp->opposite_part != bp.id();
212     return p.encumb( bp ) * ( ( combine && combines_with_other ) ? 2 : 1 );
213 }
214 
get_encumbrance_description(const player & p,const bodypart_id & bp,bool combine)215 static std::string get_encumbrance_description( const player &p, const bodypart_id &bp,
216         bool combine )
217 {
218     std::string s;
219 
220     const int eff_encumbrance = get_encumbrance( p, bp, combine );
221 
222     switch( bp->token ) {
223         case bp_torso: {
224             const int melee_roll_pen = std::max( -eff_encumbrance, -80 );
225             s += string_format( _( "Melee attack rolls: <color_white>%+d%%</color>\n" ), melee_roll_pen );
226             s += dodge_skill_text( -( eff_encumbrance / 10.0 ) );
227             s += swim_cost_text( ( eff_encumbrance / 10.0 ) * ( 80 - p.get_skill_level(
228                                      skill_swimming ) * 3 ) );
229             s += melee_cost_text( eff_encumbrance );
230             break;
231         }
232         case bp_head:
233             s += _( "<color_magenta>Head encumbrance has no effect; it simply limits how much you can put on.</color>" );
234             break;
235         case bp_eyes:
236             s += string_format(
237                      _( "Perception when checking traps or firing ranged weapons: <color_white>%+d</color>\n"
238                         "Dispersion when throwing items: <color_white>%+d</color>" ),
239                      -( eff_encumbrance / 10 ),
240                      eff_encumbrance * 10 );
241             break;
242         case bp_mouth:
243             s += _( "<color_magenta>Covering your mouth will make it more difficult to breathe and catch your breath.</color>\n" );
244             s += mouth_stamina_cost_text( -( eff_encumbrance / 5 ) );
245             break;
246         case bp_arm_l:
247         case bp_arm_r:
248             s += _( "<color_magenta>Arm encumbrance affects stamina cost of melee attacks and accuracy with ranged weapons.</color>\n" );
249             s += melee_stamina_cost_text( eff_encumbrance );
250             s += ranged_cost_text( eff_encumbrance / 5.0 );
251             break;
252         case bp_hand_l:
253         case bp_hand_r:
254             s += _( "<color_magenta>Reduces the speed at which you can handle or manipulate items.</color>\n\n" );
255             s += reload_cost_text( ( eff_encumbrance / 10 ) * 15 );
256             s += string_format( _( "Dexterity when throwing items: <color_white>%+.1f</color>\n" ),
257                                 -( eff_encumbrance / 10.0f ) );
258             s += melee_cost_text( eff_encumbrance / 2 );
259             s += string_format( _( "Reduced gun aim speed: <color_white>%.1f</color>" ),
260                                 p.aim_speed_encumbrance_modifier() );
261             break;
262         case bp_leg_l:
263         case bp_leg_r:
264             s += run_cost_text( static_cast<int>( eff_encumbrance * 0.15 ) );
265             s += swim_cost_text( ( eff_encumbrance / 10 ) * ( 50 - p.get_skill_level(
266                                      skill_swimming ) * 2 ) / 2 );
267             s += dodge_skill_text( -eff_encumbrance / 10.0 / 4.0 );
268             break;
269         case bp_foot_l:
270         case bp_foot_r:
271             s += run_cost_text( static_cast<int>( eff_encumbrance * 0.25 ) );
272             break;
273         case num_bp:
274             break;
275     }
276 
277     return s;
278 }
279 
is_cqb_skill(const skill_id & id)280 static bool is_cqb_skill( const skill_id &id )
281 {
282     // TODO: this skill list here is used in other places as well. Useless redundancy and
283     // dependency. Maybe change it into a flag of the skill that indicates it's a skill used
284     // by the bionic?
285     static const std::array<skill_id, 5> cqb_skills = { {
286             skill_id( "melee" ), skill_id( "unarmed" ), skill_id( "cutting" ),
287             skill_id( "bashing" ), skill_id( "stabbing" ),
288         }
289     };
290     return std::find( cqb_skills.begin(), cqb_skills.end(), id ) != cqb_skills.end();
291 }
292 
293 namespace
294 {
295 enum class player_display_tab : int {
296     stats,
297     encumbrance,
298     skills,
299     traits,
300     bionics,
301     effects,
302     proficiencies,
303     num_tabs,
304 };
305 } // namespace
306 
next_tab(const player_display_tab tab)307 static player_display_tab next_tab( const player_display_tab tab )
308 {
309     if( static_cast<int>( tab ) + 1 < static_cast<int>( player_display_tab::num_tabs ) ) {
310         return static_cast<player_display_tab>( static_cast<int>( tab ) + 1 );
311     } else {
312         return static_cast<player_display_tab>( 0 );
313     }
314 }
315 
prev_tab(const player_display_tab tab)316 static player_display_tab prev_tab( const player_display_tab tab )
317 {
318     if( static_cast<int>( tab ) > 0 ) {
319         return static_cast<player_display_tab>( static_cast<int>( tab ) - 1 );
320     } else {
321         return static_cast<player_display_tab>( static_cast<int>( player_display_tab::num_tabs ) - 1 );
322     }
323 }
324 
draw_proficiencies_tab(const catacurses::window & win,const unsigned line,const Character & guy,const player_display_tab curtab)325 static void draw_proficiencies_tab( const catacurses::window &win, const unsigned line,
326                                     const Character &guy, const player_display_tab curtab )
327 {
328     werase( win );
329     const std::vector<display_proficiency> profs = guy.display_proficiencies();
330     bool focused = curtab == player_display_tab::proficiencies;
331     const nc_color title_color = focused ? h_light_gray : c_light_gray;
332     center_print( win, 0, title_color, _( title_PROFICIENCIES ) );
333     const int height = getmaxy( win ) - 1;
334     const int width = getmaxx( win ) - 1;
335     bool draw_scrollbar = profs.size() > static_cast<size_t>( height );
336     int y = 1;
337     int start = draw_scrollbar ? line : 0;
338     for( size_t i = start; i < profs.size(); ++i ) {
339         if( y > height ) {
340             break;
341         }
342         std::string name;
343         const display_proficiency &cur = profs[i];
344         if( !cur.known && cur.id->can_learn() ) {
345             static_assert( grid_width == 26, "Reminder to update formatting"
346                            "for this string when grid width changes" );
347             name = string_format( "%s %2.0f%%",
348                                   left_justify( trim_by_length( cur.id->name(), width - 4 ), 21 ),
349                                   std::floor( cur.practice * 100 ) );
350         } else {
351             name = trim_by_length( cur.id->name(), width );
352         }
353         const nc_color col = focused && i == line ? hilite( cur.color ) : cur.color;
354         y += fold_and_print( win, point( 0, y ), width, col, name );
355     }
356 
357     if( draw_scrollbar ) {
358         scrollbar()
359         .offset_x( width )
360         .offset_y( 1 )
361         .content_size( profs.size() )
362         .viewport_pos( line )
363         .viewport_size( height )
364         .scroll_to_last( false )
365         .apply( win );
366     }
367 
368     wnoutrefresh( win );
369 }
370 
draw_proficiencies_info(const catacurses::window & w_info,const unsigned line,const Character & guy)371 static void draw_proficiencies_info( const catacurses::window &w_info, const unsigned line,
372                                      const Character &guy )
373 {
374     werase( w_info );
375     const std::vector<display_proficiency> profs = guy.display_proficiencies();
376     if( line < profs.size() ) {
377         const display_proficiency &cur = profs[line];
378         std::string progress;
379         if( cur.known ) {
380             progress = _( "You know this proficiency." );
381         } else {
382             progress = string_format( _( "You are %2.1f%% of the way towards learning this proficiency." ),
383                                       cur.practice * 100 );
384             if( debug_mode ) {
385                 progress += string_format( "\nYou have spent %s practicing this proficiency.",
386                                            to_string( cur.spent ) );
387             }
388         }
389         int y = 0;
390         y += fold_and_print( w_info, point( 1, y ), getmaxx( w_info ) - 1, cur.color, cur.id->name() );
391         y += fold_and_print( w_info, point( 1, y ), getmaxx( w_info ) - 1, c_cyan, progress );
392         fold_and_print( w_info, point( 1, y ), getmaxx( w_info ) - 1, c_white, cur.id->description() );
393     }
394     wnoutrefresh( w_info );
395 }
396 
draw_stats_tab(const catacurses::window & w_stats,const player & you,const unsigned int line,const player_display_tab curtab)397 static void draw_stats_tab( const catacurses::window &w_stats,
398                             const player &you, const unsigned int line, const player_display_tab curtab )
399 {
400     werase( w_stats );
401     const nc_color title_col = curtab == player_display_tab::stats ? h_light_gray : c_light_gray;
402     center_print( w_stats, 0, title_col, _( title_STATS ) );
403 
404     const auto line_color = [curtab, line]( const unsigned int line_to_draw ) {
405         if( curtab == player_display_tab::stats && line == line_to_draw ) {
406             return h_light_gray;
407         } else {
408             return c_light_gray;
409         }
410     };
411 
412     // Stats
413     const auto display_stat = [&w_stats]( const char *name, int cur, int max, int line_n,
414     const nc_color & col ) {
415         nc_color cstatus;
416         if( cur <= 0 ) {
417             cstatus = c_dark_gray;
418         } else if( cur < max / 2 ) {
419             cstatus = c_red;
420         } else if( cur < max ) {
421             cstatus = c_light_red;
422         } else if( cur == max ) {
423             cstatus = c_white;
424         } else if( cur < max * 1.5 ) {
425             cstatus = c_light_green;
426         } else {
427             cstatus = c_green;
428         }
429 
430         mvwprintz( w_stats, point( 1, line_n ), col, name );
431         mvwprintz( w_stats, point( 18, line_n ), cstatus, "%2d", cur );
432         mvwprintz( w_stats, point( 21, line_n ), c_light_gray, "(%2d)", max );
433     };
434 
435     display_stat( _( "Strength:" ), you.get_str(), you.get_str_base(), 1, line_color( 0 ) );
436     display_stat( _( "Dexterity:" ), you.get_dex(), you.get_dex_base(), 2, line_color( 1 ) );
437     display_stat( _( "Intelligence:" ), you.get_int(), you.get_int_base(), 3, line_color( 2 ) );
438     display_stat( _( "Perception:" ), you.get_per(), you.get_per_base(), 4, line_color( 3 ) );
439     mvwprintz( w_stats, point( 1, 5 ), line_color( 4 ), _( "Weight:" ) );
440     right_print( w_stats, 5, 1, c_light_gray, you.get_weight_string() );
441     mvwprintz( w_stats, point( 1, 6 ), line_color( 5 ), _( "Height:" ) );
442     mvwprintz( w_stats, point( 25 - utf8_width( you.height_string() ), 6 ), c_light_gray,
443                you.height_string() );
444     mvwprintz( w_stats, point( 1, 7 ), line_color( 6 ), _( "Age:" ) );
445     mvwprintz( w_stats, point( 25 - utf8_width( you.age_string() ), 7 ), c_light_gray,
446                you.age_string() );
447     mvwprintz( w_stats, point( 1, 8 ), line_color( 7 ), _( "Blood type:" ) );
448     mvwprintz( w_stats, point( 25 - utf8_width( io::enum_to_string( you.my_blood_type ) +
449                                ( you.blood_rh_factor ? "+" : "-" ) ), 8 ),
450                c_light_gray,
451                io::enum_to_string( you.my_blood_type ) + ( you.blood_rh_factor ? "+" : "-" ) );
452 
453     wnoutrefresh( w_stats );
454 }
455 
draw_stats_info(const catacurses::window & w_info,const player & you,const unsigned int line)456 static void draw_stats_info( const catacurses::window &w_info,
457                              const player &you, const unsigned int line )
458 {
459     werase( w_info );
460     nc_color col_temp = c_light_gray;
461 
462     if( line == 0 ) {
463         // NOLINTNEXTLINE(cata-use-named-point-constants)
464         fold_and_print( w_info, point( 1, 0 ), FULL_SCREEN_WIDTH - 2, c_magenta,
465                         _( "Strength affects your melee damage, the amount of weight you can carry, your total HP, "
466                            "your resistance to many diseases, and the effectiveness of actions which require brute force." ) );
467         print_colored_text( w_info, point( 1, 3 ), col_temp, c_light_gray,
468                             string_format( _( "Base HP: <color_white>%d</color>" ),
469                                            you.get_part_hp_max( bodypart_id( "torso" ) ) ) );
470         print_colored_text( w_info, point( 1, 4 ), col_temp, c_light_gray,
471                             string_format( _( "Carry weight (%s): <color_white>%.1f</color>" ), weight_units(),
472                                            convert_weight( you.weight_capacity() ) ) );
473         print_colored_text( w_info, point( 1, 5 ), col_temp, c_light_gray,
474                             string_format( _( "Bash damage: <color_white>%.1f</color>" ), you.bonus_damage( false ) ) );
475     } else if( line == 1 ) {
476         // NOLINTNEXTLINE(cata-use-named-point-constants)
477         fold_and_print( w_info, point( 1, 0 ), FULL_SCREEN_WIDTH - 2, c_magenta,
478                         _( "Dexterity affects your chance to hit in melee combat, helps you steady your "
479                            "gun for ranged combat, and enhances many actions that require finesse." ) );
480         print_colored_text( w_info, point( 1, 3 ), col_temp, c_light_gray,
481                             string_format( _( "Melee to-hit bonus: <color_white>%+.1lf</color>" ), you.get_melee_hit_base() ) );
482         print_colored_text( w_info, point( 1, 4 ), col_temp, c_light_gray,
483                             string_format( _( "Ranged penalty: <color_white>%+d</color>" ),
484                                            -std::abs( you.ranged_dex_mod() ) ) );
485         print_colored_text( w_info, point( 1, 5 ), col_temp, c_light_gray,
486                             string_format( _( "Throwing penalty per target's dodge: <color_white>%+d</color>" ),
487                                            you.throw_dispersion_per_dodge( false ) ) );
488     } else if( line == 2 ) {
489         // NOLINTNEXTLINE(cata-use-named-point-constants)
490         fold_and_print( w_info, point( 1, 0 ), FULL_SCREEN_WIDTH - 2, c_magenta,
491                         _( "Intelligence is less important in most situations, but it is vital for more complex tasks like "
492                            "electronics crafting.  It also affects how much skill you can pick up from reading a book." ) );
493         if( you.rust_rate() ) {
494             print_colored_text( w_info, point( 1, 3 ), col_temp, c_light_gray,
495                                 string_format( _( "Skill rust delay: <color_white>%d%%</color>" ), you.rust_rate() ) );
496         }
497         print_colored_text( w_info, point( 1, 4 ), col_temp, c_light_gray,
498                             string_format( _( "Read times: <color_white>%d%%</color>" ), you.read_speed( false ) ) );
499         print_colored_text( w_info, point( 1, 5 ), col_temp, c_light_gray,
500                             string_format( _( "Crafting bonus: <color_white>%d%%</color>" ), you.get_int() ) );
501     } else if( line == 3 ) {
502         // NOLINTNEXTLINE(cata-use-named-point-constants)
503         fold_and_print( w_info, point( 1, 0 ), FULL_SCREEN_WIDTH - 2, c_magenta,
504                         _( "Perception is the most important stat for ranged combat.  It's also used for "
505                            "detecting traps and other things of interest." ) );
506         print_colored_text( w_info, point( 1, 4 ), col_temp, c_light_gray,
507                             string_format( _( "Trap detection level: <color_white>%d</color>" ), you.get_per() ) );
508         if( you.ranged_per_mod() > 0 ) {
509             print_colored_text( w_info, point( 1, 5 ), col_temp, c_light_gray,
510                                 string_format( _( "Aiming penalty: <color_white>%+d</color>" ), -you.ranged_per_mod() ) );
511         }
512     } else if( line == 4 ) {
513         // NOLINTNEXTLINE(cata-use-named-point-constants)
514         const int lines = fold_and_print( w_info, point( 1, 0 ), FULL_SCREEN_WIDTH - 2, c_magenta,
515                                           _( "Your weight is a general indicator of how much fat your body has stored up,"
516                                              " which in turn shows how prepared you are to survive for a time without food."
517                                              "  Having too much, or too little, can be unhealthy." ) );
518         fold_and_print( w_info, point( 1, 1 + lines ), FULL_SCREEN_WIDTH - 2, c_light_gray,
519                         you.get_weight_long_description() );
520     } else if( line == 5 ) {
521         // NOLINTNEXTLINE(cata-use-named-point-constants)
522         const int lines = fold_and_print( w_info, point( 1, 0 ), FULL_SCREEN_WIDTH - 2, c_magenta,
523                                           _( "Your height.  Simply how tall you are." ) );
524         fold_and_print( w_info, point( 1, 1 + lines ), FULL_SCREEN_WIDTH - 2, c_light_gray,
525                         you.height_string() );
526     } else if( line == 6 ) {
527         // NOLINTNEXTLINE(cata-use-named-point-constants)
528         const int lines = fold_and_print( w_info, point( 1, 0 ), FULL_SCREEN_WIDTH - 2, c_magenta,
529                                           _( "This is how old you are." ) );
530         fold_and_print( w_info, point( 1, 1 + lines ), FULL_SCREEN_WIDTH - 2, c_light_gray,
531                         you.age_string() );
532     } else if( line == 7 ) {
533         // NOLINTNEXTLINE(cata-use-named-point-constants)
534         const int lines = fold_and_print( w_info, point( 1, 0 ), FULL_SCREEN_WIDTH - 2, c_magenta,
535                                           _( "This is your blood type and Rh factor." ) );
536         fold_and_print( w_info, point( 1, 1 + lines ), FULL_SCREEN_WIDTH - 2, c_light_gray,
537                         string_format( _( "Blood type: %s" ), io::enum_to_string( you.my_blood_type ) ) );
538         fold_and_print( w_info, point( 1, 2 + lines ), FULL_SCREEN_WIDTH - 2, c_light_gray,
539                         string_format( _( "Rh factor: %s" ),
540                                        you.blood_rh_factor ? _( "positive (+)" ) : _( "negative (-)" ) ) );
541     }
542     wnoutrefresh( w_info );
543 }
544 
draw_encumbrance_tab(const catacurses::window & w_encumb,const player & you,const unsigned int line,const player_display_tab curtab)545 static void draw_encumbrance_tab( const catacurses::window &w_encumb,
546                                   const player &you, const unsigned int line, const player_display_tab curtab )
547 {
548     werase( w_encumb );
549     const bool is_current_tab = curtab == player_display_tab::encumbrance;
550     const nc_color title_col = is_current_tab ? h_light_gray : c_light_gray;
551     center_print( w_encumb, 0, title_col, _( title_ENCUMB ) );
552     if( is_current_tab ) {
553         you.print_encumbrance( w_encumb, line );
554     } else {
555         you.print_encumbrance( w_encumb );
556     }
557     wnoutrefresh( w_encumb );
558 }
559 
draw_encumbrance_info(const catacurses::window & w_info,const player & you,const unsigned int line)560 static void draw_encumbrance_info( const catacurses::window &w_info,
561                                    const player &you, const unsigned int line )
562 {
563     const std::vector<std::pair<bodypart_id, bool>> bps = list_and_combine_bps( you, nullptr );
564 
565     werase( w_info );
566     bodypart_id bp;
567     bool combined_here = false;
568     if( line < bps.size() ) {
569         bp = bps[line].first;
570         combined_here = bps[line].second;
571     }
572     const std::string s = get_encumbrance_description( you, bp, combined_here );
573     // NOLINTNEXTLINE(cata-use-named-point-constants)
574     fold_and_print( w_info, point( 1, 0 ), FULL_SCREEN_WIDTH - 2, c_light_gray, s );
575     wnoutrefresh( w_info );
576 }
577 
draw_traits_tab(const catacurses::window & w_traits,const unsigned int line,const player_display_tab curtab,const std::vector<trait_id> & traitslist,const size_t trait_win_size_y)578 static void draw_traits_tab( const catacurses::window &w_traits,
579                              const unsigned int line, const player_display_tab curtab,
580                              const std::vector<trait_id> &traitslist,
581                              const size_t trait_win_size_y )
582 {
583     werase( w_traits );
584     const bool is_current_tab = curtab == player_display_tab::traits;
585     const nc_color title_col = is_current_tab ? h_light_gray : c_light_gray;
586     center_print( w_traits, 0, title_col, _( title_TRAITS ) );
587 
588     size_t min = 0;
589     size_t max = 0;
590 
591     if( !is_current_tab || line <= ( trait_win_size_y - 2 ) / 2 ) {
592         min = 0;
593         max = trait_win_size_y - 1;
594         if( traitslist.size() < max ) {
595             max = traitslist.size();
596         }
597     } else if( line >= traitslist.size() - trait_win_size_y / 2 ) {
598         min = ( traitslist.size() < trait_win_size_y - 1 ? 0 : traitslist.size() - trait_win_size_y + 1 );
599         max = traitslist.size();
600     } else {
601         min = line - ( trait_win_size_y - 2 ) / 2;
602         max = line + ( trait_win_size_y + 1 ) / 2;
603         if( traitslist.size() < max ) {
604             max = traitslist.size();
605         }
606     }
607 
608     for( size_t i = min; i < max; i++ ) {
609         const auto &mdata = traitslist[i].obj();
610         const nc_color color = mdata.get_display_color();
611         trim_and_print( w_traits, point( 1, static_cast<int>( 1 + i - min ) ), getmaxx( w_traits ) - 1,
612                         is_current_tab && i == line ? hilite( color ) : color, mdata.name() );
613     }
614     wnoutrefresh( w_traits );
615 }
616 
draw_traits_info(const catacurses::window & w_info,const unsigned int line,const std::vector<trait_id> & traitslist)617 static void draw_traits_info( const catacurses::window &w_info, const unsigned int line,
618                               const std::vector<trait_id> &traitslist )
619 {
620     werase( w_info );
621     if( line < traitslist.size() ) {
622         const auto &mdata = traitslist[line].obj();
623         // NOLINTNEXTLINE(cata-use-named-point-constants)
624         fold_and_print( w_info, point( 1, 0 ), FULL_SCREEN_WIDTH - 2, c_light_gray, string_format(
625                             "%s: %s", colorize( mdata.name(), mdata.get_display_color() ), traitslist[line]->desc() ) );
626     }
627     wnoutrefresh( w_info );
628 }
629 
draw_bionics_tab(const catacurses::window & w_bionics,const player & you,const unsigned int line,const player_display_tab curtab,const std::vector<bionic> & bionicslist,const size_t bionics_win_size_y)630 static void draw_bionics_tab( const catacurses::window &w_bionics,
631                               const player &you, const unsigned int line, const player_display_tab curtab,
632                               const std::vector<bionic> &bionicslist, const size_t bionics_win_size_y )
633 {
634     werase( w_bionics );
635     const bool is_current_tab = curtab == player_display_tab::bionics;
636     const nc_color title_col = is_current_tab ? h_light_gray : c_light_gray;
637     center_print( w_bionics, 0, title_col, _( title_BIONICS ) );
638     int power_amount;
639     std::string power_unit;
640     if( you.get_power_level() < 1_J ) {
641         power_amount = units::to_millijoule( you.get_power_level() );
642         power_unit = pgettext( "energy unit: millijoule", "mJ" );
643     } else if( you.get_power_level() < 1_kJ ) {
644         power_amount = units::to_joule( you.get_power_level() );
645         power_unit = pgettext( "energy unit: joule", "J" );
646     } else {
647         power_amount = units::to_kilojoule( you.get_power_level() );
648         power_unit = pgettext( "energy unit: kilojoule", "kJ" );
649     }
650     // NOLINTNEXTLINE(cata-use-named-point-constants)
651     trim_and_print( w_bionics, point( 1, 1 ), getmaxx( w_bionics ) - 1, c_white,
652                     string_format( _( "Power: <color_light_blue>%1$d %2$s</color>"
653                                       " / <color_light_blue>%3$d kJ</color>" ),
654                                    power_amount, power_unit, units::to_kilojoule( you.get_max_power_level() ) ) );
655 
656     const size_t useful_y = bionics_win_size_y - 2;
657     const size_t half_y = useful_y / 2;
658 
659     size_t min = 0;
660     size_t max = 0;
661 
662     if( !is_current_tab || line <= half_y ) { // near the top
663         min = 0;
664         max = std::min( bionicslist.size(), useful_y );
665     } else if( line >= bionicslist.size() - half_y ) { // near the bottom
666         min = ( bionicslist.size() <= useful_y ? 0 : bionicslist.size() - useful_y );
667         max = bionicslist.size();
668     } else { // scrolling
669         min = line - half_y;
670         max = std::min( bionicslist.size(), line + useful_y - half_y );
671     }
672 
673     for( size_t i = min; i < max; i++ ) {
674         trim_and_print( w_bionics, point( 1, static_cast<int>( 2 + i - min ) ), getmaxx( w_bionics ) - 1,
675                         is_current_tab && i == line ? hilite( c_white ) : c_white, "%s", bionicslist[i].info().name );
676     }
677     wnoutrefresh( w_bionics );
678 }
679 
draw_bionics_info(const catacurses::window & w_info,const unsigned int line,const std::vector<bionic> & bionicslist)680 static void draw_bionics_info( const catacurses::window &w_info, const unsigned int line,
681                                const std::vector<bionic> &bionicslist )
682 {
683     werase( w_info );
684     if( line < bionicslist.size() ) {
685         // NOLINTNEXTLINE(cata-use-named-point-constants)
686         fold_and_print( w_info, point( 1, 0 ), FULL_SCREEN_WIDTH - 2, c_light_gray, "%s",
687                         bionicslist[line].info().description );
688     }
689     wnoutrefresh( w_info );
690 }
691 
draw_effects_tab(const catacurses::window & w_effects,const unsigned int line,const player_display_tab curtab,const std::vector<std::pair<std::string,std::string>> & effect_name_and_text,const size_t effect_win_size_y)692 static void draw_effects_tab( const catacurses::window &w_effects,
693                               const unsigned int line, const player_display_tab curtab,
694                               const std::vector<std::pair<std::string, std::string>> &effect_name_and_text,
695                               const size_t effect_win_size_y )
696 {
697     werase( w_effects );
698     const bool is_current_tab = curtab == player_display_tab::effects;
699     const nc_color title_col = is_current_tab ? h_light_gray : c_light_gray;
700     center_print( w_effects, 0, title_col, _( title_EFFECTS ) );
701 
702     const size_t half_y = ( effect_win_size_y - 1 ) / 2;
703 
704     size_t min = 0;
705     size_t max = 0;
706 
707     const size_t actual_size = effect_name_and_text.size();
708 
709     if( !is_current_tab || line <= half_y ) {
710         min = 0;
711         max = effect_win_size_y - 1;
712         if( actual_size < max ) {
713             max = actual_size;
714         }
715     } else if( line >= actual_size - half_y ) {
716         min = ( actual_size < effect_win_size_y - 1 ? 0 : actual_size - effect_win_size_y + 1 );
717         max = actual_size;
718     } else {
719         min = line - half_y;
720         max = line - half_y + effect_win_size_y - 1;
721         if( actual_size < max ) {
722             max = actual_size;
723         }
724     }
725 
726     for( size_t i = min; i < max; i++ ) {
727         trim_and_print( w_effects, point( 0, static_cast<int>( 1 + i - min ) ), getmaxx( w_effects ) - 1,
728                         is_current_tab && i == line ? h_light_gray : c_light_gray, effect_name_and_text[i].first );
729     }
730     wnoutrefresh( w_effects );
731 }
732 
draw_effects_info(const catacurses::window & w_info,const unsigned int line,const std::vector<std::pair<std::string,std::string>> & effect_name_and_text)733 static void draw_effects_info( const catacurses::window &w_info, const unsigned int line,
734                                const std::vector<std::pair<std::string, std::string>> &effect_name_and_text )
735 {
736     werase( w_info );
737     const size_t actual_size = effect_name_and_text.size();
738     if( line < actual_size ) {
739         // NOLINTNEXTLINE(cata-use-named-point-constants)
740         fold_and_print( w_info, point( 1, 0 ), FULL_SCREEN_WIDTH - 2, c_light_gray,
741                         effect_name_and_text[line].second );
742     }
743     wnoutrefresh( w_info );
744 }
745 
746 struct HeaderSkill {
747     const Skill *skill;
748     bool is_header;
HeaderSkillHeaderSkill749     HeaderSkill( const Skill *skill, bool is_header ): skill( skill ), is_header( is_header ) {
750     }
751 };
752 
draw_skills_tab(const catacurses::window & w_skills,player & you,unsigned int line,const player_display_tab curtab,std::vector<HeaderSkill> & skillslist,const size_t skill_win_size_y)753 static void draw_skills_tab( const catacurses::window &w_skills,
754                              player &you, unsigned int line, const player_display_tab curtab,
755                              std::vector<HeaderSkill> &skillslist,
756                              const size_t skill_win_size_y )
757 {
758     const int col_width = 25;
759     if( line == 0 ) { //can't point to a header;
760         line = 1;
761     }
762 
763     werase( w_skills );
764     const bool is_current_tab = curtab == player_display_tab::skills;
765     nc_color cstatus = is_current_tab ? h_light_gray : c_light_gray;
766     center_print( w_skills, 0, cstatus, _( title_SKILLS ) );
767 
768     size_t min = 0;
769     size_t max = 0;
770 
771     const size_t half_y = ( skill_win_size_y - 1 ) / 2;
772 
773     if( !is_current_tab || line <= half_y ) {
774         min = 0;
775     } else if( line >= skillslist.size() - half_y ) {
776         min = ( skillslist.size() < skill_win_size_y - 1 ? 0 : skillslist.size() -
777                 skill_win_size_y + 1 );
778     } else {
779         min = line - half_y;
780     }
781     max = std::min( min + skill_win_size_y - 1, skillslist.size() );
782 
783     int y_pos = 1;
784     for( size_t i = min; i < max; i++, y_pos++ ) {
785         const Skill *aSkill = skillslist[i].skill;
786         const SkillLevel &level = you.get_skill_level_object( aSkill->ident() );
787 
788         if( skillslist[i].is_header ) {
789             const SkillDisplayType t = SkillDisplayType::get_skill_type( aSkill->display_category() );
790             std::string type_name = t.display_string();
791             mvwprintz( w_skills, point( 0, y_pos ), c_light_gray, header_spaces );
792             center_print( w_skills, y_pos, c_yellow, type_name );
793         } else {
794             const bool can_train = level.can_train();
795             const bool training = level.isTraining();
796             const bool rusting = level.isRusting();
797             int exercise = level.exercise();
798             int level_num = level.level();
799             bool locked = false;
800             if( you.has_active_bionic( bionic_id( "bio_cqb" ) ) && is_cqb_skill( aSkill->ident() ) ) {
801                 level_num = 5;
802                 exercise = 0;
803                 locked = true;
804             }
805             if( is_current_tab && i == line ) {
806                 if( locked ) {
807                     cstatus = h_yellow;
808                 } else if( !can_train ) {
809                     cstatus = rusting ? h_light_red : h_white;
810                 } else if( exercise >= 100 ) {
811                     cstatus = training ? h_pink : h_magenta;
812                 } else if( rusting ) {
813                     cstatus = training ? h_light_red : h_red;
814                 } else {
815                     cstatus = training ? h_light_blue : h_blue;
816                 }
817                 mvwprintz( w_skills, point( 1, y_pos ), cstatus, std::string( col_width, ' ' ) );
818             } else {
819                 if( locked ) {
820                     cstatus = c_yellow;
821                 } else if( rusting ) {
822                     cstatus = training ? c_light_red : c_red;
823                 } else if( !can_train ) {
824                     cstatus = c_white;
825                 } else {
826                     cstatus = training ? c_light_blue : c_blue;
827                 }
828                 mvwprintz( w_skills, point( 1, y_pos ), c_light_gray, std::string( col_width, ' ' ) );
829             }
830             mvwprintz( w_skills, point( 1, y_pos ), cstatus, "%s:", aSkill->name() );
831             if( aSkill->ident() == skill_id( "dodge" ) ) {
832                 mvwprintz( w_skills, point( 14, y_pos ), cstatus, "%4.1f/%-2d(%2d%%)",
833                            you.get_dodge(), level_num, exercise < 0 ? 0 : exercise );
834             } else {
835                 mvwprintz( w_skills, point( 19, y_pos ), cstatus, "%-2d(%2d%%)",
836                            level_num,
837                            ( exercise < 0 ? 0 : exercise ) );
838             }
839         }
840     }
841 
842     if( is_current_tab && skillslist.size() > skill_win_size_y - 1 ) {
843         draw_scrollbar( w_skills, line - 1, skill_win_size_y - 1, skillslist.size() - 1,
844                         // NOLINTNEXTLINE(cata-use-named-point-constants)
845                         point( 0, 1 ) );
846     }
847     wnoutrefresh( w_skills );
848 }
849 
draw_skills_info(const catacurses::window & w_info,unsigned int line,const std::vector<HeaderSkill> & skillslist)850 static void draw_skills_info( const catacurses::window &w_info, unsigned int line,
851                               const std::vector<HeaderSkill> &skillslist )
852 {
853     werase( w_info );
854     if( line < 1 ) {
855         line = 1;
856     }
857     const Skill *selectedSkill = nullptr;
858     if( line < skillslist.size() && !skillslist[line].is_header ) {
859         selectedSkill = skillslist[line].skill;
860     }
861 
862     werase( w_info );
863 
864     if( selectedSkill ) {
865         // NOLINTNEXTLINE(cata-use-named-point-constants)
866         fold_and_print( w_info, point( 1, 0 ), FULL_SCREEN_WIDTH - 2, c_light_gray,
867                         selectedSkill->description() );
868     }
869     wnoutrefresh( w_info );
870 }
871 
draw_speed_tab(const catacurses::window & w_speed,const player & you,const std::map<std::string,int> & speed_effects)872 static void draw_speed_tab( const catacurses::window &w_speed,
873                             const player &you,
874                             const std::map<std::string, int> &speed_effects )
875 {
876     werase( w_speed );
877     // Finally, draw speed.
878     center_print( w_speed, 0, c_light_gray, _( title_SPEED ) );
879     // NOLINTNEXTLINE(cata-use-named-point-constants)
880     mvwprintz( w_speed, point( 1, 1 ), c_light_gray, _( "Base Move Cost:" ) );
881     mvwprintz( w_speed, point( 1, 2 ), c_light_gray, _( "Current Speed:" ) );
882     int newmoves = you.get_speed();
883     int pen = 0;
884     unsigned int line = 3;
885     if( you.weight_carried() > you.weight_capacity() ) {
886         pen = 25 * ( you.weight_carried() - you.weight_capacity() ) / ( you.weight_capacity() );
887         mvwprintz( w_speed, point( 1, line ), c_red,
888                    pgettext( "speed penalty", "Overburdened        -%2d%%" ), pen );
889         line++;
890     }
891     pen = you.get_pain_penalty().speed;
892     if( pen >= 1 ) {
893         mvwprintz( w_speed, point( 1, line ), c_red,
894                    pgettext( "speed penalty", "Pain                -%2d%%" ), pen );
895         line++;
896     }
897     if( you.get_thirst() > 40 ) {
898         pen = std::abs( player::thirst_speed_penalty( you.get_thirst() ) );
899         mvwprintz( w_speed, point( 1, line ), c_red,
900                    pgettext( "speed penalty", "Thirst              -%2d%%" ), pen );
901         line++;
902     }
903     if( you.kcal_speed_penalty() < 0 ) {
904         pen = std::abs( you.kcal_speed_penalty() );
905         const std::string inanition = you.get_bmi() < character_weight_category::underweight ?
906                                       _( "Starving" ) : _( "Underfed" );
907         //~ %s: Starving/Underfed (already left-justified), %2d: speed penalty
908         mvwprintz( w_speed, point( 1, line ), c_red, pgettext( "speed penalty", "%s-%2d%%" ),
909                    left_justify( inanition, 20 ), pen );
910         line++;
911     }
912     if( you.has_trait( trait_id( "SUNLIGHT_DEPENDENT" ) ) && !g->is_in_sunlight( you.pos() ) ) {
913         pen = ( g->light_level( you.posz() ) >= 12 ? 5 : 10 );
914         mvwprintz( w_speed, point( 1, line ), c_red,
915                    pgettext( "speed penalty", "Out of Sunlight     -%2d%%" ), pen );
916         line++;
917     }
918 
919     const float temperature_speed_modifier = you.mutation_value( "temperature_speed_modifier" );
920     if( temperature_speed_modifier != 0 ) {
921         nc_color pen_color;
922         std::string pen_sign;
923         const int player_local_temp = get_weather().get_temperature( you.pos() );
924         if( you.has_trait( trait_id( "COLDBLOOD4" ) ) && player_local_temp > 65 ) {
925             pen_color = c_green;
926             pen_sign = "+";
927         } else if( player_local_temp < 65 ) {
928             pen_color = c_red;
929             pen_sign = "-";
930         }
931         if( !pen_sign.empty() ) {
932             pen = ( player_local_temp - 65 ) * temperature_speed_modifier;
933             mvwprintz( w_speed, point( 1, line ), pen_color,
934                        //~ %s: sign of bonus/penalty, %2d: speed bonus/penalty
935                        pgettext( "speed modifier", "Cold-Blooded        %s%2d%%" ), pen_sign, std::abs( pen ) );
936             line++;
937         }
938     }
939 
940     int quick_bonus = static_cast<int>( newmoves - ( newmoves / 1.1 ) );
941     int bio_speed_bonus = quick_bonus;
942     if( you.has_trait( trait_id( "QUICK" ) ) && you.has_bionic( bionic_id( "bio_speed" ) ) ) {
943         bio_speed_bonus = static_cast<int>( newmoves / 1.1 - ( newmoves / 1.1 / 1.1 ) );
944         std::swap( quick_bonus, bio_speed_bonus );
945     }
946     if( you.has_trait( trait_id( "QUICK" ) ) ) {
947         mvwprintz( w_speed, point( 1, line ), c_green,
948                    pgettext( "speed bonus", "Quick               +%2d%%" ), quick_bonus );
949         line++;
950     }
951     if( you.has_bionic( bionic_id( "bio_speed" ) ) ) {
952         mvwprintz( w_speed, point( 1, line ), c_green,
953                    pgettext( "speed bonus", "Bionic Speed        +%2d%%" ), bio_speed_bonus );
954         line++;
955     }
956 
957     for( const std::pair<const std::string, int> &speed_effect : speed_effects ) {
958         nc_color col = ( speed_effect.second > 0 ? c_green : c_red );
959         mvwprintz( w_speed, point( 1, line ), col, "%s", speed_effect.first );
960         mvwprintz( w_speed, point( 21, line ), col, ( speed_effect.second > 0 ? "+" : "-" ) );
961         mvwprintz( w_speed, point( std::abs( speed_effect.second ) >= 10 ? 22 : 23, line ), col, "%d%%",
962                    std::abs( speed_effect.second ) );
963         line++;
964     }
965 
966     int runcost = you.run_cost( 100 );
967     nc_color col = ( runcost <= 100 ? c_green : c_red );
968     mvwprintz( w_speed, point( 21 + ( runcost >= 100 ? 0 : ( runcost < 10 ? 2 : 1 ) ), 1 ), col,
969                "%d", runcost );
970     col = ( newmoves >= 100 ? c_green : c_red );
971     mvwprintz( w_speed, point( 21 + ( newmoves >= 100 ? 0 : ( newmoves < 10 ? 2 : 1 ) ), 2 ), col,
972                "%d", newmoves );
973     wnoutrefresh( w_speed );
974 }
975 
draw_info_window(const catacurses::window & w_info,const player & you,const unsigned int line,const player_display_tab curtab,const std::vector<trait_id> & traitslist,const std::vector<bionic> & bionicslist,const std::vector<std::pair<std::string,std::string>> & effect_name_and_text,const std::vector<HeaderSkill> & skillslist)976 static void draw_info_window( const catacurses::window &w_info, const player &you,
977                               const unsigned int line, const player_display_tab curtab,
978                               const std::vector<trait_id> &traitslist,
979                               const std::vector<bionic> &bionicslist,
980                               const std::vector<std::pair<std::string, std::string>> &effect_name_and_text,
981                               const std::vector<HeaderSkill> &skillslist )
982 {
983     switch( curtab ) {
984         case player_display_tab::stats:
985             draw_stats_info( w_info, you, line );
986             break;
987         case player_display_tab::encumbrance:
988             draw_encumbrance_info( w_info, you, line );
989             break;
990         case player_display_tab::skills:
991             draw_skills_info( w_info, line, skillslist );
992             break;
993         case player_display_tab::traits:
994             draw_traits_info( w_info, line, traitslist );
995             break;
996         case player_display_tab::bionics:
997             draw_bionics_info( w_info, line, bionicslist );
998             break;
999         case player_display_tab::effects:
1000             draw_effects_info( w_info, line, effect_name_and_text );
1001             break;
1002         case player_display_tab::proficiencies:
1003             draw_proficiencies_info( w_info, line, you );
1004             break;
1005         case player_display_tab::num_tabs:
1006             abort();
1007     }
1008 }
1009 
draw_tip(const catacurses::window & w_tip,const player & you,const std::string & race,const input_context & ctxt)1010 static void draw_tip( const catacurses::window &w_tip, const player &you,
1011                       const std::string &race, const input_context &ctxt )
1012 {
1013     werase( w_tip );
1014 
1015     // Print name and header
1016     if( you.custom_profession.empty() ) {
1017         if( you.crossed_threshold() ) {
1018             //~ player info window: 1s - name, 2s - gender, 3s - Prof or Mutation name
1019             mvwprintz( w_tip, point_zero, c_white, _( " %1$s | %2$s | %3$s" ), you.name,
1020                        you.male ? _( "Male" ) : _( "Female" ), race );
1021         } else if( you.prof == nullptr || you.prof == profession::generic() ) {
1022             // Regular person. Nothing interesting.
1023             //~ player info window: 1s - name, 2s - gender '|' - field separator.
1024             mvwprintz( w_tip, point_zero, c_white, _( " %1$s | %2$s" ), you.name,
1025                        you.male ? _( "Male" ) : _( "Female" ) );
1026         } else {
1027             mvwprintz( w_tip, point_zero, c_white, _( " %1$s | %2$s | %3$s" ), you.name,
1028                        you.male ? _( "Male" ) : _( "Female" ), you.prof->gender_appropriate_name( you.male ) );
1029         }
1030     } else {
1031         mvwprintz( w_tip, point_zero, c_white, _( " %1$s | %2$s | %3$s" ), you.name,
1032                    you.male ? _( "Male" ) : _( "Female" ), you.custom_profession );
1033     }
1034 
1035     right_print( w_tip, 0, 1, c_light_gray, string_format(
1036                      _( "[<color_yellow>%s</color>]" ),
1037                      ctxt.get_desc( "HELP_KEYBINDINGS" ) ) );
1038 
1039     right_print( w_tip, 0, 0, c_light_gray, string_format( "%s", LINE_XOXO_S ) );
1040 
1041     wnoutrefresh( w_tip );
1042 }
1043 
handle_player_display_action(player & you,unsigned int & line,player_display_tab & curtab,input_context & ctxt,const ui_adaptor & ui_tip,const ui_adaptor & ui_info,const ui_adaptor & ui_stats,const ui_adaptor & ui_encumb,const ui_adaptor & ui_traits,const ui_adaptor & ui_bionics,const ui_adaptor & ui_effects,const ui_adaptor & ui_skills,const ui_adaptor & ui_proficiencies,const std::vector<trait_id> & traitslist,const std::vector<bionic> & bionicslist,const std::vector<std::pair<std::string,std::string>> & effect_name_and_text,const std::vector<HeaderSkill> & skillslist)1044 static bool handle_player_display_action( player &you, unsigned int &line,
1045         player_display_tab &curtab, input_context &ctxt, const ui_adaptor &ui_tip,
1046         const ui_adaptor &ui_info, const ui_adaptor &ui_stats, const ui_adaptor &ui_encumb,
1047         const ui_adaptor &ui_traits, const ui_adaptor &ui_bionics, const ui_adaptor &ui_effects,
1048         const ui_adaptor &ui_skills, const ui_adaptor &ui_proficiencies,
1049         const std::vector<trait_id> &traitslist, const std::vector<bionic> &bionicslist,
1050         const std::vector<std::pair<std::string, std::string>> &effect_name_and_text,
1051         const std::vector<HeaderSkill> &skillslist )
1052 {
1053     const auto invalidate_tab = [&]( const player_display_tab tab ) {
1054         switch( tab ) {
1055             case player_display_tab::stats:
1056                 ui_stats.invalidate_ui();
1057                 break;
1058             case player_display_tab::encumbrance:
1059                 ui_encumb.invalidate_ui();
1060                 break;
1061             case player_display_tab::traits:
1062                 ui_traits.invalidate_ui();
1063                 break;
1064             case player_display_tab::bionics:
1065                 ui_bionics.invalidate_ui();
1066                 break;
1067             case player_display_tab::effects:
1068                 ui_effects.invalidate_ui();
1069                 break;
1070             case player_display_tab::skills:
1071                 ui_skills.invalidate_ui();
1072                 break;
1073             case player_display_tab::proficiencies:
1074                 ui_proficiencies.invalidate_ui();
1075                 break;
1076             case player_display_tab::num_tabs:
1077                 abort();
1078         }
1079     };
1080 
1081     unsigned int line_beg = 0;
1082     unsigned int line_end = 0;
1083     switch( curtab ) {
1084         case player_display_tab::stats:
1085             line_end = 8;
1086             break;
1087         case player_display_tab::encumbrance: {
1088             const std::vector<std::pair<bodypart_id, bool>> bps = list_and_combine_bps( you, nullptr );
1089             line_end = bps.size();
1090             break;
1091         }
1092         case player_display_tab::traits:
1093             line_end = traitslist.size();
1094             break;
1095         case player_display_tab::bionics:
1096             line_end = bionicslist.size();
1097             break;
1098         case player_display_tab::effects:
1099             line_end = effect_name_and_text.size();
1100             break;
1101         case player_display_tab::skills:
1102             line_beg = 1; // skip first header
1103             line_end = skillslist.size();
1104             break;
1105         case player_display_tab::proficiencies:
1106             line_end = you.display_proficiencies().size();
1107             break;
1108         case player_display_tab::num_tabs:
1109             abort();
1110     }
1111     if( line_beg >= line_end || line < line_beg ) {
1112         line = line_beg;
1113     } else if( line > line_end - 1 ) {
1114         line = line_end - 1;
1115     }
1116 
1117     bool done = false;
1118     std::string action = ctxt.handle_input();
1119 
1120     if( action == "UP" ) {
1121         if( line > line_beg ) {
1122             --line;
1123         } else {
1124             line = line_end - 1;
1125         }
1126         if( curtab == player_display_tab::skills && skillslist[line].is_header ) {
1127             --line;
1128         }
1129         invalidate_tab( curtab );
1130         ui_info.invalidate_ui();
1131     } else if( action == "DOWN" ) {
1132         if( line + 1 < line_end ) {
1133             ++line;
1134         } else {
1135             line = line_beg;
1136         }
1137         if( curtab == player_display_tab::skills && skillslist[line].is_header ) {
1138             ++line;
1139         }
1140         invalidate_tab( curtab );
1141         ui_info.invalidate_ui();
1142     } else if( action == "NEXT_TAB" || action == "PREV_TAB" ) {
1143         line = 0;
1144         invalidate_tab( curtab );
1145         curtab = action == "NEXT_TAB" ? next_tab( curtab ) : prev_tab( curtab );
1146         invalidate_tab( curtab );
1147         ui_info.invalidate_ui();
1148     } else if( action == "QUIT" ) {
1149         done = true;
1150     } else if( action == "CONFIRM" ) {
1151         switch( curtab ) {
1152             default:
1153                 break;
1154             case player_display_tab::stats:
1155                 if( line < 4 && get_option<bool>( "STATS_THROUGH_KILLS" ) && you.is_avatar() ) {
1156                     you.as_avatar()->upgrade_stat_prompt( static_cast<character_stat>( line ) );
1157                 }
1158                 invalidate_tab( curtab );
1159                 break;
1160             case player_display_tab::skills: {
1161                 const Skill *selectedSkill = nullptr;
1162                 if( line < skillslist.size() && !skillslist[line].is_header ) {
1163                     selectedSkill = skillslist[line].skill;
1164                 }
1165                 if( selectedSkill ) {
1166                     you.get_skill_level_object( selectedSkill->ident() ).toggleTraining();
1167                 }
1168                 invalidate_tab( curtab );
1169                 break;
1170             }
1171         }
1172     } else if( action == "CHANGE_PROFESSION_NAME" ) {
1173         string_input_popup popup;
1174         popup.title( _( "Profession Name: " ) )
1175         .width( 25 )
1176         .text( "" )
1177         .max_length( 25 )
1178         .query();
1179 
1180         you.custom_profession = popup.text();
1181         ui_tip.invalidate_ui();
1182     }
1183     return done;
1184 }
1185 
disp_info()1186 void player::disp_info()
1187 {
1188     std::vector<std::pair<std::string, std::string>> effect_name_and_text;
1189     for( auto &elem : *effects ) {
1190         for( auto &_effect_it : elem.second ) {
1191             const std::string tmp = _effect_it.second.disp_name();
1192             if( tmp.empty() ) {
1193                 continue;
1194             }
1195             effect_name_and_text.push_back( { tmp, _effect_it.second.disp_desc() } );
1196         }
1197     }
1198     if( get_perceived_pain() > 0 ) {
1199         const stat_mod ppen = get_pain_penalty();
1200         std::string pain_text;
1201         const auto add_if = [&]( const int amount, const char *const name ) {
1202             if( amount > 0 ) {
1203                 pain_text += string_format( name, amount ) + "   ";
1204             }
1205         };
1206         add_if( ppen.strength, _( "Strength -%d" ) );
1207         add_if( ppen.dexterity, _( "Dexterity -%d" ) );
1208         add_if( ppen.intelligence, _( "Intelligence -%d" ) );
1209         add_if( ppen.perception, _( "Perception -%d" ) );
1210         add_if( ppen.speed, _( "Speed -%d %%" ) );
1211         effect_name_and_text.push_back( { _( "Pain" ), pain_text } );
1212     }
1213 
1214     const float bmi = get_bmi();
1215 
1216     if( bmi < character_weight_category::underweight ) {
1217         std::string starvation_name;
1218         std::string starvation_text;
1219 
1220         if( bmi < character_weight_category::emaciated ) {
1221             starvation_name = _( "Severely Malnourished" );
1222             starvation_text =
1223                 _( "Your body is severely weakened by starvation.  You might die if you don't start eating regular meals!\n\n" );
1224         } else {
1225             starvation_name = _( "Malnourished" );
1226             starvation_text =
1227                 _( "Your body is weakened by starvation.  Only time and regular meals will help you recover.\n\n" );
1228         }
1229 
1230         if( bmi < character_weight_category::underweight ) {
1231             const float str_penalty = 1.0f - ( ( bmi - 13.0f ) / 3.0f );
1232             starvation_text += std::string( _( "Strength" ) ) + " -" + string_format( "%2.0f%%\n",
1233                                str_penalty * 100.0f );
1234             starvation_text += std::string( _( "Dexterity" ) ) + " -" + string_format( "%2.0f%%\n",
1235                                str_penalty * 50.0f );
1236             starvation_text += std::string( _( "Intelligence" ) ) + " -" + string_format( "%2.0f%%",
1237                                str_penalty * 50.0f );
1238         }
1239 
1240         effect_name_and_text.push_back( { starvation_name, starvation_text } );
1241     }
1242 
1243     if( has_trait( trait_id( "TROGLO" ) ) && g->is_in_sunlight( pos() ) &&
1244         get_weather().weather_id->sun_intensity >= sun_intensity_type::high ) {
1245         effect_name_and_text.push_back( { _( "In Sunlight" ),
1246                                           _( "The sunlight irritates you.\n"
1247                                              "Strength - 1;    Dexterity - 1;    Intelligence - 1;    Perception - 1" )
1248                                         } );
1249     } else if( has_trait( trait_id( "TROGLO2" ) ) && g->is_in_sunlight( pos() ) ) {
1250         effect_name_and_text.push_back( { _( "In Sunlight" ),
1251                                           _( "The sunlight irritates you badly.\n"
1252                                              "Strength - 2;    Dexterity - 2;    Intelligence - 2;    Perception - 2" )
1253                                         } );
1254     } else if( has_trait( trait_id( "TROGLO3" ) ) && g->is_in_sunlight( pos() ) ) {
1255         effect_name_and_text.push_back( { _( "In Sunlight" ),
1256                                           _( "The sunlight irritates you terribly.\n"
1257                                              "Strength - 4;    Dexterity - 4;    Intelligence - 4;    Perception - 4" )
1258                                         } );
1259     }
1260 
1261     for( auto &elem : addictions ) {
1262         if( elem.sated < 0_turns && elem.intensity >= MIN_ADDICTION_LEVEL ) {
1263             effect_name_and_text.push_back( { addiction_name( elem ), addiction_text( elem ) } );
1264         }
1265     }
1266 
1267     const unsigned int effect_win_size_y = 1 + static_cast<unsigned>( effect_name_and_text.size() );
1268 
1269     std::vector<trait_id> traitslist = get_mutations( false );
1270     std::sort( traitslist.begin(), traitslist.end(), trait_display_sort );
1271     const unsigned int trait_win_size_y_max = 1 + static_cast<unsigned>( traitslist.size() );
1272 
1273     std::vector<bionic> bionicslist = *my_bionics;
1274     const unsigned int bionics_win_size_y_max = 2 + bionicslist.size();
1275 
1276     const std::vector<const Skill *> player_skill = Skill::get_skills_sorted_by(
1277     [&]( const Skill & a, const Skill & b ) {
1278         skill_displayType_id type_a = a.display_category();
1279         skill_displayType_id type_b = b.display_category();
1280 
1281         return localized_compare( std::make_pair( type_a, a.name() ),
1282                                   std::make_pair( type_b, b.name() ) );
1283     } );
1284 
1285     std::vector<HeaderSkill> skillslist;
1286     skill_displayType_id prev_type = skill_displayType_id::NULL_ID();
1287     for( const auto &s : player_skill ) {
1288         if( s->display_category() != prev_type ) {
1289             prev_type = s->display_category();
1290             skillslist.emplace_back( s, true );
1291         }
1292         skillslist.emplace_back( s, false );
1293     }
1294     const unsigned int skill_win_size_y_max = 1 + skillslist.size();
1295     const unsigned int info_win_size_y = 6;
1296 
1297     const unsigned int grid_height = 9;
1298 
1299     const unsigned int infooffsetytop = grid_height + 2;
1300     unsigned int infooffsetybottom = infooffsetytop + 1 + info_win_size_y;
1301 
1302     const auto calculate_trait_and_bionic_height = [&]() {
1303         const unsigned int maxy = static_cast<unsigned>( TERMY );
1304         unsigned int trait_win_size_y = trait_win_size_y_max;
1305         unsigned int bionics_win_size_y = bionics_win_size_y_max;
1306         if( ( bionics_win_size_y_max + 1 + trait_win_size_y_max + infooffsetybottom ) > maxy ) {
1307             // maximum space for either window if they're both the same size
1308             unsigned max_shared_y = ( maxy - infooffsetybottom - 1 ) / 2;
1309             if( std::min( bionics_win_size_y_max, trait_win_size_y_max ) > max_shared_y ) {
1310                 // both are larger than the shared size
1311                 bionics_win_size_y = max_shared_y;
1312                 trait_win_size_y = maxy - infooffsetybottom - 1 - bionics_win_size_y;
1313             } else if( trait_win_size_y_max <= max_shared_y ) {
1314                 // trait window is less than the shared size, so give space to bionics
1315                 bionics_win_size_y = maxy - infooffsetybottom - 1 - trait_win_size_y_max;
1316             } else {
1317                 // bionics window is less than the shared size
1318                 trait_win_size_y = maxy - infooffsetybottom - 1 - bionics_win_size_y;
1319             }
1320         }
1321         return std::make_pair( trait_win_size_y, bionics_win_size_y );
1322     };
1323 
1324     // Print name and header
1325     // Post-humanity trumps your pre-Cataclysm life
1326     // Unless you have a custom profession.
1327     std::string race;
1328     if( custom_profession.empty() ) {
1329         if( crossed_threshold() ) {
1330             for( const trait_id &mut : get_mutations() ) {
1331                 const mutation_branch &mdata = mut.obj();
1332                 if( mdata.threshold ) {
1333                     race = mdata.name();
1334                     break;
1335                 }
1336             }
1337         }
1338     }
1339 
1340     input_context ctxt( "PLAYER_INFO" );
1341     ctxt.register_updown();
1342     ctxt.register_action( "NEXT_TAB", to_translation( "Cycle to next category" ) );
1343     ctxt.register_action( "PREV_TAB", to_translation( "Cycle to previous category" ) );
1344     ctxt.register_action( "QUIT" );
1345     ctxt.register_action( "CONFIRM", to_translation( "Toggle skill training / Upgrade stat" ) );
1346     ctxt.register_action( "CHANGE_PROFESSION_NAME", to_translation( "Change profession name" ) );
1347     ctxt.register_action( "HELP_KEYBINDINGS" );
1348 
1349     std::map<std::string, int> speed_effects;
1350     for( auto &elem : *effects ) {
1351         for( std::pair<const bodypart_id, effect> &_effect_it : elem.second ) {
1352             effect &it = _effect_it.second;
1353             bool reduced = resists_effect( it );
1354             int move_adjust = it.get_mod( "SPEED", reduced );
1355             if( move_adjust != 0 ) {
1356                 const std::string dis_text = it.get_speed_name();
1357                 speed_effects[dis_text] += move_adjust;
1358             }
1359         }
1360     }
1361 
1362     border_helper borders;
1363 
1364     player_display_tab curtab = player_display_tab::stats;
1365     unsigned int line = 0;
1366 
1367     catacurses::window w_tip;
1368     ui_adaptor ui_tip;
1369     ui_tip.on_screen_resize( [&]( ui_adaptor & ui_tip ) {
1370         w_tip = catacurses::newwin( 1, FULL_SCREEN_WIDTH + 1, point_zero );
1371         ui_tip.position_from_window( w_tip );
1372     } );
1373     ui_tip.mark_resize();
1374     ui_tip.on_redraw( [&]( const ui_adaptor & ) {
1375         draw_tip( w_tip, *this, race, ctxt );
1376     } );
1377 
1378     catacurses::window w_stats;
1379     catacurses::window w_stats_border;
1380     border_helper::border_info &border_stats = borders.add_border();
1381     ui_adaptor ui_stats;
1382     ui_stats.on_screen_resize( [&]( ui_adaptor & ui_stats ) {
1383         // NOLINTNEXTLINE(cata-use-named-point-constants)
1384         w_stats = catacurses::newwin( grid_height, grid_width, point( 0, 1 ) );
1385         // Every grid draws the bottom and right borders. The top and left borders
1386         // are either not displayed or drawn by another grid.
1387         // NOLINTNEXTLINE(cata-use-named-point-constants)
1388         w_stats_border = catacurses::newwin( grid_height + 1, grid_width + 1, point( 0, 1 ) );
1389         // But we need to specify the full border for border_helper to calculate the
1390         // border connection.
1391         // NOLINTNEXTLINE(cata-use-named-point-constants)
1392         border_stats.set( point( -1, 0 ), point( grid_width + 2, grid_height + 2 ) );
1393         ui_stats.position_from_window( w_stats_border );
1394     } );
1395     ui_stats.mark_resize();
1396     ui_stats.on_redraw( [&]( const ui_adaptor & ) {
1397         borders.draw_border( w_stats_border );
1398         wnoutrefresh( w_stats_border );
1399         draw_stats_tab( w_stats, *this, line, curtab );
1400     } );
1401 
1402     unsigned int trait_win_size_y = 0;
1403     catacurses::window w_traits;
1404     catacurses::window w_traits_border;
1405     border_helper::border_info &border_traits = borders.add_border();
1406     ui_adaptor ui_traits;
1407     ui_traits.on_screen_resize( [&]( ui_adaptor & ui_traits ) {
1408         trait_win_size_y = calculate_trait_and_bionic_height().first;
1409         w_traits = catacurses::newwin( trait_win_size_y, grid_width,
1410                                        point( grid_width + 1, infooffsetybottom ) );
1411         w_traits_border = catacurses::newwin( trait_win_size_y + 1, grid_width + 1,
1412                                               point( grid_width + 1, infooffsetybottom ) );
1413         border_traits.set( point( grid_width, infooffsetybottom - 1 ),
1414                            point( grid_width + 2, trait_win_size_y + 2 ) );
1415         ui_traits.position_from_window( w_traits_border );
1416     } );
1417     ui_traits.mark_resize();
1418     ui_traits.on_redraw( [&]( const ui_adaptor & ) {
1419         borders.draw_border( w_traits_border );
1420         wnoutrefresh( w_traits_border );
1421         draw_traits_tab( w_traits, line, curtab, traitslist, trait_win_size_y );
1422     } );
1423 
1424     unsigned int bionics_win_size_y = 0;
1425     catacurses::window w_bionics;
1426     catacurses::window w_bionics_border;
1427     border_helper::border_info &border_bionics = borders.add_border();
1428     ui_adaptor ui_bionics;
1429     ui_bionics.on_screen_resize( [&]( ui_adaptor & ui_bionics ) {
1430         bionics_win_size_y = calculate_trait_and_bionic_height().second;
1431         w_bionics = catacurses::newwin( bionics_win_size_y, grid_width,
1432                                         point( grid_width + 1,
1433                                                infooffsetybottom + trait_win_size_y + 1 ) );
1434         w_bionics_border = catacurses::newwin( bionics_win_size_y + 1, grid_width + 1,
1435                                                point( grid_width + 1,
1436                                                        infooffsetybottom + trait_win_size_y + 1 ) );
1437         border_bionics.set( point( grid_width, infooffsetybottom + trait_win_size_y ),
1438                             point( grid_width + 2, bionics_win_size_y + 2 ) );
1439         ui_bionics.position_from_window( w_bionics_border );
1440     } );
1441     ui_bionics.mark_resize();
1442     ui_bionics.on_redraw( [&]( const ui_adaptor & ) {
1443         borders.draw_border( w_bionics_border );
1444         wnoutrefresh( w_bionics_border );
1445         draw_bionics_tab( w_bionics, *this, line, curtab, bionicslist, bionics_win_size_y );
1446     } );
1447 
1448     catacurses::window w_encumb;
1449     catacurses::window w_encumb_border;
1450     border_helper::border_info &border_encumb = borders.add_border();
1451     ui_adaptor ui_encumb;
1452     ui_encumb.on_screen_resize( [&]( ui_adaptor & ui_encumb ) {
1453         w_encumb = catacurses::newwin( grid_height, grid_width, point( grid_width + 1, 1 ) );
1454         w_encumb_border = catacurses::newwin( grid_height + 1, grid_width + 1, point( grid_width + 1, 1 ) );
1455         border_encumb.set( point( grid_width, 0 ), point( grid_width + 2, grid_height + 2 ) );
1456         ui_encumb.position_from_window( w_encumb_border );
1457     } );
1458     ui_encumb.mark_resize();
1459     ui_encumb.on_redraw( [&]( const ui_adaptor & ) {
1460         borders.draw_border( w_encumb_border );
1461         wnoutrefresh( w_encumb_border );
1462         draw_encumbrance_tab( w_encumb, *this, line, curtab );
1463     } );
1464 
1465     catacurses::window w_effects;
1466     catacurses::window w_effects_border;
1467     border_helper::border_info &border_effects = borders.add_border();
1468     ui_adaptor ui_effects;
1469     ui_effects.on_screen_resize( [&]( ui_adaptor & ui_effects ) {
1470         w_effects = catacurses::newwin( effect_win_size_y, grid_width,
1471                                         point( grid_width * 2 + 2, infooffsetybottom ) );
1472         w_effects_border = catacurses::newwin( effect_win_size_y + 1, grid_width + 2,
1473                                                point( grid_width * 2 + 1, infooffsetybottom ) );
1474         border_effects.set( point( grid_width * 2 + 1, infooffsetybottom - 1 ),
1475                             point( grid_width + 2, effect_win_size_y + 2 ) );
1476         ui_effects.position_from_window( w_effects_border );
1477     } );
1478     ui_effects.mark_resize();
1479     ui_effects.on_redraw( [&]( const ui_adaptor & ) {
1480         borders.draw_border( w_effects_border );
1481         wnoutrefresh( w_effects_border );
1482         draw_effects_tab( w_effects, line, curtab, effect_name_and_text, effect_win_size_y );
1483     } );
1484 
1485     unsigned int proficiency_win_size_y = 0;
1486     const point profstart = point( grid_width * 2 + 2, infooffsetybottom + effect_win_size_y + 1 );
1487     catacurses::window w_proficiencies;
1488     catacurses::window w_proficiencies_border;
1489     border_helper::border_info &border_proficiencies = borders.add_border();
1490     ui_adaptor ui_proficiencies;
1491     ui_proficiencies.on_screen_resize( [&]( ui_adaptor & ui_proficiencies ) {
1492         const unsigned int maxy = static_cast<unsigned>( TERMY );
1493         proficiency_win_size_y = std::min( display_proficiencies().size(),
1494                                            static_cast<size_t>( maxy - ( infooffsetybottom + effect_win_size_y ) ) ) + 1;
1495         w_proficiencies = catacurses::newwin( proficiency_win_size_y, grid_width,
1496                                               profstart );
1497         w_proficiencies_border = catacurses::newwin( proficiency_win_size_y + 1, grid_width + 2,
1498                                  profstart + point_west );
1499         border_proficiencies.set( profstart + point_north_west, point( grid_width + 2,
1500                                   proficiency_win_size_y + 2 ) );
1501         ui_proficiencies.position_from_window( w_proficiencies_border );
1502     } );
1503     ui_proficiencies.mark_resize();
1504     ui_proficiencies.on_redraw( [&]( const ui_adaptor & ) {
1505         borders.draw_border( w_proficiencies_border );
1506         wnoutrefresh( w_proficiencies_border );
1507         draw_proficiencies_tab( w_proficiencies, line, *this, curtab );
1508     } );
1509 
1510     unsigned int skill_win_size_y = 0;
1511     catacurses::window w_skills;
1512     catacurses::window w_skills_border;
1513     border_helper::border_info &border_skills = borders.add_border();
1514     ui_adaptor ui_skills;
1515     ui_skills.on_screen_resize( [&]( ui_adaptor & ui_skills ) {
1516         const unsigned int maxy = static_cast<unsigned>( TERMY );
1517         skill_win_size_y = skill_win_size_y_max;
1518         if( skill_win_size_y + infooffsetybottom > maxy ) {
1519             skill_win_size_y = maxy - infooffsetybottom;
1520         }
1521         w_skills = catacurses::newwin( skill_win_size_y, grid_width,
1522                                        point( 0, infooffsetybottom ) );
1523         w_skills_border = catacurses::newwin( skill_win_size_y + 1, grid_width + 1,
1524                                               point( 0, infooffsetybottom ) );
1525         border_skills.set( point( -1, infooffsetybottom - 1 ),
1526                            point( grid_width + 2, skill_win_size_y + 2 ) );
1527         ui_skills.position_from_window( w_skills_border );
1528     } );
1529     ui_skills.mark_resize();
1530     ui_skills.on_redraw( [&]( const ui_adaptor & ) {
1531         borders.draw_border( w_skills_border );
1532         wnoutrefresh( w_skills_border );
1533         draw_skills_tab( w_skills, *this, line, curtab, skillslist, skill_win_size_y );
1534     } );
1535 
1536     catacurses::window w_info;
1537     catacurses::window w_info_border;
1538     border_helper::border_info &border_info = borders.add_border();
1539     ui_adaptor ui_info;
1540     ui_info.on_screen_resize( [&]( ui_adaptor & ui_info ) {
1541         w_info = catacurses::newwin( info_win_size_y, FULL_SCREEN_WIDTH,
1542                                      point( 0, infooffsetytop ) );
1543         w_info_border = catacurses::newwin( info_win_size_y + 1, FULL_SCREEN_WIDTH + 1,
1544                                             point( 0, infooffsetytop ) );
1545         border_info.set( point( -1, infooffsetytop - 1 ),
1546                          point( FULL_SCREEN_WIDTH + 2, info_win_size_y + 2 ) );
1547         ui_info.position_from_window( w_info_border );
1548     } );
1549     ui_info.mark_resize();
1550     ui_info.on_redraw( [&]( const ui_adaptor & ) {
1551         borders.draw_border( w_info_border );
1552         wnoutrefresh( w_info_border );
1553         draw_info_window( w_info, *this, line, curtab,
1554                           traitslist, bionicslist, effect_name_and_text, skillslist );
1555     } );
1556 
1557     catacurses::window w_speed;
1558     catacurses::window w_speed_border;
1559     border_helper::border_info &border_speed = borders.add_border();
1560     ui_adaptor ui_speed;
1561     ui_speed.on_screen_resize( [&]( ui_adaptor & ui_speed ) {
1562         w_speed = catacurses::newwin( grid_height, grid_width, point( grid_width * 2 + 2, 1 ) );
1563         w_speed_border = catacurses::newwin( grid_height + 1, grid_width + 1,
1564                                              point( grid_width * 2 + 2, 1 ) );
1565         border_speed.set( point( grid_width * 2 + 1, 0 ),
1566                           point( grid_width + 2, grid_height + 2 ) );
1567         ui_speed.position_from_window( w_speed_border );
1568     } );
1569     ui_speed.mark_resize();
1570     ui_speed.on_redraw( [&]( const ui_adaptor & ) {
1571         borders.draw_border( w_speed_border );
1572         wnoutrefresh( w_speed_border );
1573         draw_speed_tab( w_speed, *this, speed_effects );
1574     } );
1575 
1576     bool done = false;
1577 
1578     do {
1579         ui_manager::redraw_invalidated();
1580 
1581         done = handle_player_display_action( *this, line, curtab, ctxt, ui_tip, ui_info, ui_stats,
1582                                              ui_encumb, ui_traits, ui_bionics, ui_effects, ui_skills, ui_proficiencies, traitslist, bionicslist,
1583                                              effect_name_and_text, skillslist );
1584     } while( !done );
1585 }
1586