diff --git a/data/raw/keybindings.json b/data/raw/keybindings.json index 89c0d8c942eb7..b67d8fa08b73c 100644 --- a/data/raw/keybindings.json +++ b/data/raw/keybindings.json @@ -1275,6 +1275,12 @@ "name": "Customize character", "bindings": [ { "input_method": "keyboard_any", "key": "y" } ] }, + { + "type": "keybinding", + "id": "MEDICAL_MENU", + "name": "Open Medical Menu", + "bindings": [ { "input_method": "keyboard_char", "key": "@" }, { "input_method": "keyboard_code", "key": "2", "mod": [ "shift" ] } ] + }, { "type": "keybinding", "id": "SAVE_TEMPLATE", @@ -1859,6 +1865,13 @@ "name": "Toggle activate/examine", "bindings": [ { "input_method": "keyboard_char", "key": "!" }, { "input_method": "keyboard_code", "key": "1", "mod": [ "shift" ] } ] }, + { + "type": "keybinding", + "id": "APPLY", + "category": "MEDICAL", + "name": "Apply and/or Use Item", + "bindings": [ { "input_method": "keyboard_char", "key": "A" }, { "input_method": "keyboard_code", "key": "a", "mod": [ "shift" ] } ] + }, { "type": "keybinding", "name": "Pause", @@ -2225,6 +2238,13 @@ "id": "mutations", "bindings": [ { "input_method": "keyboard_any", "key": "[" } ] }, + { + "type": "keybinding", + "name": "View Medical", + "category": "DEFAULTMODE", + "id": "medical", + "bindings": [ { "input_method": "keyboard_char", "key": "N" }, { "input_method": "keyboard_code", "key": "n", "mod": [ "shift" ] } ] + }, { "type": "keybinding", "name": "Re-layer armor/clothing", diff --git a/src/action.cpp b/src/action.cpp index a5e9dcb058ccb..d504fb265d5b8 100644 --- a/src/action.cpp +++ b/src/action.cpp @@ -309,6 +309,8 @@ std::string action_ident( action_id act ) return "factions"; case ACTION_SCORES: return "scores"; + case ACTION_MEDICAL: + return "medical"; case ACTION_MORALE: return "morale"; case ACTION_MESSAGES: @@ -431,6 +433,7 @@ bool can_action_change_worldstate( const action_id act ) case ACTION_SCORES: case ACTION_FACTIONS: case ACTION_MORALE: + case ACTION_MEDICAL: case ACTION_MESSAGES: case ACTION_HELP: case ACTION_MAIN_MENU: @@ -947,6 +950,7 @@ action_id handle_action_menu() REGISTER_ACTION( ACTION_SCORES ); REGISTER_ACTION( ACTION_FACTIONS ); REGISTER_ACTION( ACTION_MORALE ); + REGISTER_ACTION( ACTION_MEDICAL ); REGISTER_ACTION( ACTION_MESSAGES ); REGISTER_ACTION( ACTION_DIARY ); } else if( category == _( "Misc" ) ) { diff --git a/src/action.h b/src/action.h index 2503e4ee28606..d3fefb7aecf08 100644 --- a/src/action.h +++ b/src/action.h @@ -256,6 +256,8 @@ enum action_id : int { ACTION_FACTIONS, /** Display morale effects screen */ ACTION_MORALE, + /** Displays medical menu */ + ACTION_MEDICAL, /** Display messages screen */ ACTION_MESSAGES, /** Display help screen */ diff --git a/src/avatar.h b/src/avatar.h index 72060c6c48e5a..14f348fe7ea65 100644 --- a/src/avatar.h +++ b/src/avatar.h @@ -143,6 +143,8 @@ class avatar : public Character /** Provides the window and detailed morale data */ void disp_morale(); + /** Opens the medical window */ + void disp_medical(); /** Resets stats, and applies effects in an idempotent manner */ void reset_stats() override; /** Resets all missions before saving character to template */ diff --git a/src/game.cpp b/src/game.cpp index d2aba8a66d2d2..a94bea632bb01 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -2320,6 +2320,7 @@ input_context get_default_mode_input_context() ctxt.register_action( "drop_adj" ); ctxt.register_action( "bionics" ); ctxt.register_action( "mutations" ); + ctxt.register_action( "medical" ); ctxt.register_action( "sort_armor" ); ctxt.register_action( "wait" ); ctxt.register_action( "craft" ); diff --git a/src/handle_action.cpp b/src/handle_action.cpp index 06924468d1a8a..8c7d65e150470 100644 --- a/src/handle_action.cpp +++ b/src/handle_action.cpp @@ -2461,6 +2461,10 @@ bool game::do_regular_action( action_id &act, avatar &player_character, player_character.disp_morale(); break; + case ACTION_MEDICAL: + player_character.disp_medical(); + break; + case ACTION_MESSAGES: Messages::display_messages(); break; diff --git a/src/medical_ui.cpp b/src/medical_ui.cpp new file mode 100644 index 0000000000000..b7ebb1ae1d275 --- /dev/null +++ b/src/medical_ui.cpp @@ -0,0 +1,1005 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "action.h" +#include "addiction.h" +#include "avatar_action.h" +#include "creature.h" +#include "character.h" +#include "character_modifier.h" +#include "display.h" +#include "effect.h" +#include "flag.h" +#include "game.h" +#include "output.h" +#include "ui_manager.h" +#include "vitamin.h" +#include "weather.h" + +static const efftype_id effect_bite( "bite" ); +static const efftype_id effect_bleed( "bleed" ); +static const efftype_id effect_infected( "infected" ); +static const efftype_id effect_mending( "mending" ); + +static const trait_id trait_COLDBLOOD4( "COLDBLOOD4" ); +static const trait_id trait_NOPAIN( "NOPAIN" ); +static const trait_id trait_SUNLIGHT_DEPENDENT( "SUNLIGHT_DEPENDENT" ); +static const trait_id trait_TROGLO( "TROGLO" ); +static const trait_id trait_TROGLO2( "TROGLO2" ); +static const trait_id trait_TROGLO3( "TROGLO3" ); + +enum class medical_tab_mode { + TAB_SUMMARY +}; + +class selection_line +{ + public: + selection_line() = default; + selection_line( const std::string text, const std::string &desc_str, const int max_width ) + : desc_str( desc_str ) { + std::vector textformatted = foldstring( text, max_width, + ']' ); + row_count = textformatted.size(); + if( row_count > 1 ) { + //If there are too many tags, display them neatly on a new line. + std::string print_line = string_format( "%s\n", textformatted[0] ); + for( int i = 1; i < row_count; i++ ) { + if( i != row_count ) { + print_line += string_format( "->%s\n", textformatted[i] ); + } else { + print_line += string_format( "->%s", textformatted[i] ); + } + } + header_str = print_line; + } else { + header_str = text; + } + } + + std::string print() { + if( highlight_line ) { + header_str = colorize( ">", h_white ) + header_str; + } + return header_str; + } + + std::string description() { + return desc_str; + } + + void set_detail( const std::string &header, const std::string &detail ) { + detail_str = std::pair( header, detail ); + } + + std::pair get_detail() { + return detail_str; + } + + int get_row_count() { + return row_count; + } + + void set_highlight() { + highlight_line = true; + } + + bool highlighted() { + return highlight_line; + } + + private: + std::string header_str; + std::string desc_str; + std::pair detail_str; + int row_count; + bool highlight_line = false; +}; + +class medical_column +{ + public: + medical_column() = default; + medical_column( const int column_id, const point COLUMN_START, + const std::pair COLUMN_BOUNDS ) + : column_id( column_id ), COLUMN_BOUNDS( COLUMN_BOUNDS ), COLUMN_START( COLUMN_START ) {} + + void draw_column( const catacurses::window &window, const int BORDER_START, + const int BORDER_END ) { + mvwvline( window, point( COLUMN_START.x, BORDER_START ), LINE_XOXO, + BORDER_END - 4 ); // | + mvwputch( window, point( COLUMN_START.x, BORDER_END - 1 ), BORDER_COLOR, + LINE_XXOX ); // _|_ + } + + void print_column( const catacurses::window &window, const int LINE_START, const int MAX_HEIGHT ) { + int linerow = 0; + int selectionrow = 0; + + for( selection_line &line : column_lines ) { + const point row_start( + line.highlighted() ? + COLUMN_START.x + left_padding - 1 : + COLUMN_START.x + left_padding, + COLUMN_START.y + linerow + ); + + if( row_start.y - LINE_START >= MAX_HEIGHT ) { + break; + } + + if( linerow >= LINE_START ) { + fold_and_print( window, row_start - point( 0, LINE_START ), max_width(), + c_light_gray, line.print() ); + linerow += line.get_row_count(); + } else { + linerow++; + } + ++selectionrow; + } + COLUMN_ROWS = { selectionrow, linerow }; + } + + void add_column_line( selection_line line ) { + column_lines.emplace_back( line ); + COLUMN_ROWS.second += line.get_row_count(); + } + + selection_line set_highlight( int y ) { + int offset = y % column_lines.size(); + column_lines[offset].set_highlight(); + return column_lines[offset]; + } + + int selection_count() { + return COLUMN_ROWS.first; + } + + int row_count() { + return COLUMN_ROWS.second; + } + + int max_width() { + return COLUMN_BOUNDS.first - COLUMN_START.x - left_padding; + } + + bool empty() { + return column_lines.empty(); + } + + bool current_column( const int selected_id ) { + return column_id == selected_id; + } + + std::pair detail_str( int y ) { + std::pair ret; + if( y < static_cast( column_lines.size() ) ) { + int offset = y % column_lines.size(); + ret = column_lines[offset].get_detail(); + } + return ret; + } + + private: + int column_id; + const int left_padding = 2; + std::pair COLUMN_ROWS = { 0, 0 }; // Selection Lines - Print Lines + std::pair COLUMN_BOUNDS; // Left Bound - Right Bound + point COLUMN_START; + std::vectorcolumn_lines; + std::string column_title; +}; + +static std::string coloured_stat_display( int statCur, int statMax ) +{ + nc_color cstatus; + if( statCur <= 0 ) { + cstatus = c_dark_gray; + } else if( statCur < statMax / 2 ) { + cstatus = c_red; + } else if( statCur < statMax ) { + cstatus = c_light_red; + } else if( statCur == statMax ) { + cstatus = c_white; + } else if( statCur < statMax * 1.5 ) { + cstatus = c_light_green; + } else { + cstatus = c_green; + } + std::string cur = colorize( string_format( _( "%2d" ), statCur ), cstatus ); + return string_format( _( "%s (%s)" ), cur, statMax ); +} + +static void draw_medical_titlebar( const catacurses::window &window, avatar *player, + const int WIDTH ) +{ + input_context ctxt( "MEDICAL", keyboard_mode::keychar ); + const Character &you = *player->as_character(); + + werase( window ); + draw_border( window, BORDER_COLOR, _( " MEDICAL " ) ); + + // Tabs + const std::vector> tabs = { + { medical_tab_mode::TAB_SUMMARY, string_format( _( "SUMMARY" ) ) } + }; + draw_tabs( window, tabs, medical_tab_mode::TAB_SUMMARY ); + + const int TAB_WIDTH = 12; + + // Draw symbols to connect additional lines to border + + int width = getmaxx( window ); + int height = getmaxy( window ); + for( int i = 1; i < height - 1; ++i ) { + // | + mvwputch( window, point( 0, i ), BORDER_COLOR, LINE_XOXO ); + // | + mvwputch( window, point( width - 1, i ), BORDER_COLOR, LINE_XOXO ); + } + // |- + mvwputch( window, point( 0, height - 1 ), BORDER_COLOR, LINE_XXXO ); + // -| + mvwputch( window, point( width - 1, height - 1 ), BORDER_COLOR, LINE_XOXX ); + + int right_indent = 2; + int cur_str_pos = 0; + + // Pain Indicator + auto pain_descriptor = display::pain_text_color( *player ); + if( !pain_descriptor.first.empty() ) { + const std::string pain_str = string_format( _( "In %s" ), pain_descriptor.first ); + + cur_str_pos = right_print( window, 1, right_indent, pain_descriptor.second, pain_str ); + + // Borders + for( int i = 1; i < getmaxy( window ) - 1; i++ ) { + mvwputch( window, point( cur_str_pos - 2, i ), BORDER_COLOR, LINE_XOXO ); // | + } + mvwputch( window, point( cur_str_pos - 2, 0 ), BORDER_COLOR, LINE_OXXX ); // ^|^ + mvwputch( window, point( cur_str_pos - 2, 2 ), BORDER_COLOR, LINE_XXOX ); // _|_ + right_indent += utf8_width( remove_color_tags( pain_str ) ) + 3; + } + + const std::pair hunger_pair = display::hunger_text_color( you ); + const std::pair thirst_pair = display::thirst_text_color( you ); + const std::pair fatigue_pair = display::fatigue_text_color( you ); + + // Hunger + if( !hunger_pair.first.empty() ) { + cur_str_pos = right_print( window, 1, right_indent, hunger_pair.second, hunger_pair.first ); + + // Borders + for( int i = 1; i < getmaxy( window ) - 1; i++ ) { + mvwputch( window, point( cur_str_pos - 2, i ), BORDER_COLOR, LINE_XOXO ); // | + } + mvwputch( window, point( cur_str_pos - 2, 0 ), BORDER_COLOR, LINE_OXXX ); // ^|^ + mvwputch( window, point( cur_str_pos - 2, 2 ), BORDER_COLOR, LINE_XXOX ); // _|_ + + right_indent += utf8_width( hunger_pair.first ) + 3; + } + + // Thirst + if( !thirst_pair.first.empty() ) { + cur_str_pos = right_print( window, 1, right_indent, thirst_pair.second, thirst_pair.first ); + + // Borders + for( int i = 1; i < getmaxy( window ) - 1; i++ ) { + mvwputch( window, point( cur_str_pos - 2, i ), BORDER_COLOR, LINE_XOXO ); // | + } + mvwputch( window, point( cur_str_pos - 2, 0 ), BORDER_COLOR, LINE_OXXX ); // ^|^ + mvwputch( window, point( cur_str_pos - 2, 2 ), BORDER_COLOR, LINE_XXOX ); // _|_ + + right_indent += utf8_width( thirst_pair.first ) + 3; + } + + // Fatigue + if( !fatigue_pair.first.empty() ) { + cur_str_pos = right_print( window, 1, right_indent, fatigue_pair.second, fatigue_pair.first ); + + // Borders + for( int i = 1; i < getmaxy( window ) - 1; i++ ) { + mvwputch( window, point( cur_str_pos - 2, i ), BORDER_COLOR, LINE_XOXO ); // | + } + mvwputch( window, point( cur_str_pos - 2, 0 ), BORDER_COLOR, LINE_OXXX ); // ^|^ + mvwputch( window, point( cur_str_pos - 2, 2 ), BORDER_COLOR, LINE_XXOX ); // _|_ + + right_indent += utf8_width( fatigue_pair.first ) + 3; + } + + // Hotkey Helper + std::string desc; + desc = string_format( _( + "[%s/%s]Scroll Info [%s]Apply Item [%s]Keybindings" ), + ctxt.get_desc( "SCROLL_INFOBOX_UP" ), ctxt.get_desc( "SCROLL_INFOBOX_DOWN" ), + ctxt.get_desc( "APPLY" ), ctxt.get_desc( "HELP_KEYBINDINGS" ) ); + + const int details_width = utf8_width( remove_color_tags( desc ) ) + 3; + const int max_width = right_indent + TAB_WIDTH; + if( WIDTH - max_width > details_width ) { + // If the window runs out of room, we won't print keybindings. + right_print( window, 1, right_indent, c_white, desc ); + } + + const std::string TITLE_STR = " MEDICAL "; + + // Window Title + if( WIDTH - ( details_width + utf8_width( TITLE_STR ) > WIDTH / 2 ) ) { + center_print( window, 0, c_blue, _( TITLE_STR ) ); + } +} + +// Displays a summary of each bodypart's health, including a display for a few 'statuses' +static medical_column draw_health_summary( const int column_count, avatar *player, + const point COLUMN_START, + const std::pair COLUMN_BOUNDS ) +{ + medical_column health_column = medical_column( column_count, COLUMN_START, COLUMN_BOUNDS ); + const int max_width = health_column.max_width(); + + for( const bodypart_id &part : player->get_all_body_parts( get_body_part_flags::sorted ) ) { + std::string header; // Bodypart Title + std::string hp_str; // Bodypart HP + std::string detail; + std::string description; + + const int bleed_intensity = player->get_effect_int( effect_bleed, part ); + const bool bleeding = bleed_intensity > 0; + const bool bitten = player->has_effect( effect_bite, part.id() ); + const bool infected = player->has_effect( effect_infected, part.id() ); + const bool no_feeling = player->has_trait( trait_NOPAIN ); + const int maximal_hp = player->get_part_hp_max( part ); + const int current_hp = player->get_part_hp_cur( part ); + const bool limb_is_broken = player->is_limb_broken( part ); + const bool limb_is_mending = player->worn_with_flag( flag_SPLINT, part ); + + if( limb_is_mending ) { + detail += string_format( _( "[ %s ]" ), colorize( _( "SPLINTED" ), c_yellow ) ); + if( no_feeling ) { + hp_str = colorize( "==%==", c_blue ); + } else { + const auto &eff = player->get_effect( effect_mending, part ); + const int mend_perc = eff.is_null() ? 0.0 : 100 * eff.get_duration() / eff.get_max_duration(); + + const int num = mend_perc / 20; + hp_str = colorize( std::string( num, '#' ) + std::string( 5 - num, '=' ), c_blue ); + } + } else if( limb_is_broken ) { + detail += string_format( _( "[ %s ]" ), colorize( _( "BROKEN" ), c_red ) ); + hp_str = "==%=="; + } else if( no_feeling ) { + if( current_hp < maximal_hp * 0.25 ) { + hp_str = colorize( _( "Very Bad" ), c_red ); + } else if( current_hp < maximal_hp * 0.5 ) { + hp_str = colorize( _( "Bad" ), c_light_red ); + } else if( current_hp < maximal_hp * 0.75 ) { + hp_str = colorize( _( "Okay" ), c_light_green ); + } else { + hp_str = colorize( _( "Good" ), c_green ); + } + } else { + std::pair h_bar = get_hp_bar( current_hp, maximal_hp, false ); + hp_str = colorize( h_bar.first, h_bar.second ) + + colorize( std::string( 5 - utf8_width( h_bar.first ), '.' ), c_white ); + } + const std::string bp_name = uppercase_first_letter( body_part_name( part, 1 ) ); + header += colorize( bp_name, + display::limb_color( *player, + part, true, true, true ) ) + " " + hp_str; + + // BLEEDING block + if( bleeding ) { + const effect bleed_effect = player->get_effect( effect_bleed, part ); + const nc_color bleeding_color = colorize_bleeding_intensity( bleed_intensity ); + detail += string_format( _( "[ %s ]" ), colorize( _( "BLEEDING" ), bleeding_color ) ); + description += string_format( "[ %s ] - %s\n", + colorize( bleed_effect.get_speed_name(), bleeding_color ), + bleed_effect.disp_short_desc() ); + } + + // BITTEN block + if( bitten ) { + const effect bite_effect = player->get_effect( effect_bite, part ); + detail += string_format( _( "[ %s ]" ), colorize( _( "BITTEN" ), c_yellow ) ); + description += string_format( "[ %s ] - %s\n", + colorize( bite_effect.get_speed_name(), c_yellow ), + bite_effect.disp_short_desc() ); + } + + // INFECTED block + if( infected ) { + const effect infected_effect = player->get_effect( effect_infected, part ); + detail += string_format( _( "[ %s ]" ), colorize( _( "INFECTED" ), c_pink ) ); + description += string_format( "[ %s ] - %s\n", + colorize( infected_effect.get_speed_name(), c_pink ), + infected_effect.disp_short_desc() ); + } + + selection_line line; + if( !detail.empty() ) { + line = selection_line( string_format( "[%s] - %s", header, detail ), description, max_width ); + } else { + line = selection_line( string_format( "[%s]", header ), description, max_width ); + } + + const bodypart *bp = player->get_part( part ); + std::string detail_str; + for( const limb_score &sc : limb_score::get_all() ) { + if( !part->has_limb_score( sc.getId() ) ) { + continue; + } + + float injury_score = bp->get_limb_score( sc.getId(), 0, 0, 1 ); + float max_score = part->get_limb_score( sc.getId() ); + + if( injury_score < max_score ) { + const float injury_modifier = 100 * ( max_score - injury_score ) / max_score; + std::pair score_c; + if( injury_score < max_score * 0.4f ) { + score_c.first = string_format( _( "Crippled (-%.f%%)" ), injury_modifier ); + score_c.second = c_red; + } else if( injury_score < max_score * 0.6f ) { + score_c.first = string_format( _( "Impaired (-%.f%%)" ), injury_modifier ); + score_c.second = c_light_red; + } else if( injury_score < max_score * 0.75f ) { + score_c.first = string_format( _( "Weakened (-%.f%%)" ), injury_modifier ); + score_c.second = c_yellow; + } else if( injury_score < max_score * 0.9f ) { + score_c.first = string_format( _( "Weakened (-%.f%%)" ), injury_modifier ); + score_c.second = c_dark_gray; + } else { + score_c.first = string_format( _( "OK (-%.f%%)" ), injury_modifier ); + score_c.second = c_dark_gray; + } + detail_str += string_format( _( "%s: %s\n" ), sc.name().translated(), colorize( score_c.first, + score_c.second ) ); + } else { + detail_str += string_format( _( "%s: %s\n" ), sc.name().translated(), colorize( "OK", c_green ) ); + } + } + + for( const character_modifier &mod : character_modifier::get_all() ) { + for( const auto &sc : mod.use_limb_scores() ) { + if( sc.first.is_null() || !part->has_limb_score( sc.first ) ) { + continue; + } + std::string desc = mod.description().translated(); + float injury_score = bp->get_limb_score( sc.first, 0, 0, 1 ); + float max_score = part->get_limb_score( sc.first ); + nc_color score_c; + + if( injury_score < max_score * 0.4f ) { + score_c = c_red; + } else if( injury_score < max_score * 0.6f ) { + score_c = c_light_red; + } else if( injury_score < max_score * 0.75f ) { + score_c = c_yellow; + } else { + score_c = c_white; + } + + std::string valstr = colorize( string_format( "%.2f", mod.modifier( *player->as_character() ) ), + score_c ); + detail_str += string_format( "%s: %s%s\n", desc, mod.mod_type_str(), valstr ); + } + } + + line.set_detail( string_format( _( "%s STATS" ), to_upper_case( bp_name ) ), detail_str ); + health_column.add_column_line( line ); + } + return health_column; +} + +// Displays a summary list of all visible effects. +static medical_column draw_effects_summary( const int column_count, avatar *player, + const point COLUMN_START, + const std::pair COLUMN_BOUNDS ) +{ + medical_column effects_column = medical_column( column_count, COLUMN_START, COLUMN_BOUNDS ); + const int max_width = effects_column.max_width(); + + for( const effect &eff : player->get_effects() ) { + const std::string name = eff.disp_name(); + if( name.empty() ) { + continue; + } + effects_column.add_column_line( selection_line( name, eff.disp_desc(), max_width ) ); + } + + const float bmi = player->get_bmi(); + + if( bmi < character_weight_category::underweight ) { + std::string starvation_name; + std::string starvation_text; + + if( bmi < character_weight_category::emaciated ) { + starvation_name = _( "Severely Malnourished" ); + starvation_text = + _( "Your body is severely weakened by starvation. You might die if you don't start eating regular meals!\n\n" ); + } else { + starvation_name = _( "Malnourished" ); + starvation_text = + _( "Your body is weakened by starvation. Only time and regular meals will help you recover.\n\n" ); + } + + if( bmi < character_weight_category::underweight ) { + const float str_penalty = 1.0f - ( ( bmi - 13.0f ) / 3.0f ); + starvation_text += std::string( _( "Strength" ) ) + " -" + string_format( "%2.0f%%\n", + str_penalty * 100.0f ); + starvation_text += std::string( _( "Dexterity" ) ) + " -" + string_format( "%2.0f%%\n", + str_penalty * 50.0f ); + starvation_text += std::string( _( "Intelligence" ) ) + " -" + string_format( "%2.0f%%", + str_penalty * 50.0f ); + } + + effects_column.add_column_line( selection_line( starvation_name, starvation_text, max_width ) ); + } + + if( player->has_trait( trait_TROGLO ) && g->is_in_sunlight( player->pos() ) && + get_weather().weather_id->sun_intensity >= sun_intensity_type::high ) { + effects_column.add_column_line( selection_line( "In Sunlight", "The sunlight irritates you.\n", + max_width ) ); + } else if( player->has_trait( trait_TROGLO2 ) && g->is_in_sunlight( player->pos() ) ) { + effects_column.add_column_line( selection_line( "In Sunlight", + "The sunlight irritates you badly.\n", max_width ) ); + } else if( player->has_trait( trait_TROGLO3 ) && g->is_in_sunlight( player->pos() ) ) { + effects_column.add_column_line( selection_line( "In Sunlight", + "The sunlight irritates you terribly.\n", max_width ) ); + } + + for( auto &elem : player->addictions ) { + if( elem.sated < 0_turns && elem.intensity >= MIN_ADDICTION_LEVEL ) { + effects_column.add_column_line( selection_line( addiction_name( elem ), addiction_text( elem ), + max_width ) ); + } + } + + if( effects_column.empty() ) { + effects_column.add_column_line( selection_line( colorize( "None", c_dark_gray ), "", max_width ) ); + } + + return effects_column; +} + +// Displays a summary list of the player's statistics. +static medical_column draw_stats_summary( const int column_count, avatar *player, + const point COLUMN_START, + const std::pair COLUMN_BOUNDS ) +{ + medical_column stats_column = medical_column( column_count, COLUMN_START, COLUMN_BOUNDS ); + const int max_width = stats_column.max_width(); + + std::string speed_detail_str; + int runcost = player->run_cost( 100 ); + int newmoves = player->get_speed(); + + std::string coloured_str = colorize( string_format( _( "%d" ), runcost ), + ( runcost <= 100 ? c_green : c_red ) ); + selection_line runcost_line = selection_line( string_format( _( "Base Move Cost: %s" ), + coloured_str ), + colorize( _( "Base move cost is the final modified movement cost taken to traverse flat ground." ), + c_light_blue ), + max_width ); + + coloured_str = colorize( string_format( _( "%d" ), newmoves ), + ( newmoves >= 100 ? c_green : c_red ) ); + selection_line movecost_line = selection_line( string_format( _( "Current Speed: %s" ), + coloured_str ), + colorize( _( "Speed determines the amount of actions or movement points you can perform in a turn." ), + c_light_blue ), + max_width ); + + const int speed_modifier = player->get_enchantment_speed_bonus(); + + std::string pge_str; + if( speed_modifier != 0 ) { + pge_str = pgettext( "speed bonus", "Bio/Mut/Effects " ); + speed_detail_str += colorize( string_format( _( "%s -%2d%%\n" ), pge_str, speed_modifier ), + c_green ); + } + + int pen = 0; + + if( player->weight_carried() > player->weight_capacity() ) { + pen = 25 * ( player->weight_carried() - player->weight_capacity() ) / player->weight_capacity(); + pge_str = pgettext( "speed penalty", "Overburdened " ); + speed_detail_str += colorize( string_format( _( "%s -%2d%%\n" ), pge_str, pen ), c_red ); + } + + pen = player->get_pain_penalty().speed; + if( pen >= 1 ) { + pge_str = pgettext( "speed penalty", "Pain " ); + speed_detail_str += colorize( string_format( _( "%s -%2d%%\n" ), pge_str, pen ), c_red ); + } + if( player->get_thirst() > 40 ) { + pen = std::abs( Character::thirst_speed_penalty( player->get_thirst() ) ); + pge_str = pgettext( "speed penalty", "Thirst " ); + speed_detail_str += colorize( string_format( _( "%s -%2d%%\n" ), pge_str, pen ), c_red ); + } + if( player->kcal_speed_penalty() < 0 ) { + pen = std::abs( player->kcal_speed_penalty() ); + pge_str = pgettext( "speed penalty", player->get_bmi() < character_weight_category::underweight ? + "Starving" : "Underfed" ); + speed_detail_str += colorize( string_format( _( "%s -%2d%%\n" ), pge_str, pen ), c_red ); + } + if( player->has_trait( trait_SUNLIGHT_DEPENDENT ) && !g->is_in_sunlight( player->pos() ) ) { + pen = ( g->light_level( player->posz() ) >= 12 ? 5 : 10 ); + pge_str = pgettext( "speed penalty", "Out of Sunlight " ); + speed_detail_str += colorize( string_format( _( "%s -%2d%%\n" ), pge_str, pen ), c_red ); + } + + const float temperature_speed_modifier = player->mutation_value( "temperature_speed_modifier" ); + if( temperature_speed_modifier != 0 ) { + nc_color pen_color; + std::string pen_sign; + const int player_local_temp = get_weather().get_temperature( player->pos() ); + if( player->has_trait( trait_COLDBLOOD4 ) && player_local_temp > 65 ) { + pen_color = c_green; + pen_sign = "+"; + } else if( player_local_temp < 65 ) { + pen_color = c_red; + pen_sign = "-"; + } + if( !pen_sign.empty() ) { + pen = ( player_local_temp - 65 ) * temperature_speed_modifier; + pge_str = pgettext( "speed modifier", "Cold-Blooded " ); + speed_detail_str += colorize( string_format( _( "%s %s%2d%%\n" ), pge_str, pen_sign, + std::abs( pen ) ), c_red ); + } + } + + std::map speed_effects; + for( effect &elem : player->get_effects() ) { + bool reduced = player->resists_effect( elem ); + int move_adjust = elem.get_mod( "SPEED", reduced ); + if( move_adjust != 0 ) { + const std::string dis_text = elem.get_speed_name(); + speed_effects[dis_text] += move_adjust; + } + } + + for( const std::pair &speed_effect : speed_effects ) { + nc_color col = ( speed_effect.second > 0 ? c_green : c_red ); + speed_detail_str += colorize( string_format( _( "%s %s%d%%\n" ), speed_effect.first, + ( speed_effect.second > 0 ? "+" : "-" ), + std::abs( speed_effect.second ) ), col ); + } + + runcost_line.set_detail( _( "SPEED" ), speed_detail_str ); + movecost_line.set_detail( _( "SPEED" ), speed_detail_str ); + + stats_column.add_column_line( runcost_line ); + stats_column.add_column_line( movecost_line ); + + std::string strength_str = coloured_stat_display( player->get_str(), player->get_str_base() ); + stats_column.add_column_line( + selection_line( string_format( _( "Strength: %s" ), strength_str ), + _( "Strength affects your melee damage, the amount of weight you can carry, your total HP, " + "your resistance to many diseases, and the effectiveness of actions which require brute force." ), + max_width ) ); + + std::string dexterity_str = coloured_stat_display( player->get_dex(), player->get_dex_base() ); + stats_column.add_column_line( + selection_line( string_format( _( "Dexterity: %s" ), dexterity_str ), + _( "Dexterity affects your chance to hit in melee combat, helps you steady your " + "gun for ranged combat, and enhances many actions that require finesse." ), + max_width ) ); + + std::string intelligence_str = coloured_stat_display( player->get_int(), player->get_int_base() ); + stats_column.add_column_line( + selection_line( string_format( _( "Intelligence: %s" ), intelligence_str ), + _( "Intelligence is less important in most situations, but it is vital for more complex tasks like " + "electronics crafting. It also affects how much skill you can pick up from reading a book." ), + max_width ) ); + + std::string perception_str = coloured_stat_display( player->get_per(), player->get_per_base() ); + stats_column.add_column_line( + selection_line( string_format( _( "Perception: %s" ), perception_str ), + _( "Perception is the most important stat for ranged combat. It's also used for " + "detecting traps and other things of interest." ), + max_width ) ); + + return stats_column; +} + +void avatar::disp_medical() +{ + // Windows + catacurses::window w_title; // Title Bar - Tabs, Pain Indicator & Blood Indicator + catacurses::window wMedical; // Primary Window + catacurses::window w_description; // Bottom Detail Bar + + // Window Definitions + const int TITLE_W_HEIGHT = 3; + const int DESC_W_HEIGHT = 6; // Consistent with Player Info (@) Menu + const int HEADER_Y = TITLE_W_HEIGHT; + const int TEXT_START_Y = HEADER_Y + 2; + const int INFO_START_Y = HEADER_Y + 8; + int DESC_W_BEGIN; + int HEIGHT; + int WIDTH; + + // Column Definitions + int second_column_x = 0; + int third_column_x = 0; + + // Scrolling + int SCROLL_POINT; // The number of printed rows at which to enable scrolling + int scroll_position = 0; + int INFO_SCROLL_POINT; + int info_scroll_position = 0; + + int info_lines = 0; + + // Cursor + int cursor_bounds[3]; // Number of selectable rows in each column + point cursor; + + ui_adaptor ui; + ui.on_screen_resize( [&]( ui_adaptor & ui ) { + const int WIDTH_OFFSET = ( TERMX - FULL_SCREEN_WIDTH ) / 4; + HEIGHT = std::min( TERMY, FULL_SCREEN_HEIGHT ); + WIDTH = FULL_SCREEN_WIDTH + WIDTH_OFFSET; + + const point win( ( TERMX - WIDTH ) / 2, ( TERMY - HEIGHT ) / 2 ); + + wMedical = catacurses::newwin( HEIGHT, WIDTH, win ); + + w_title = catacurses::newwin( TITLE_W_HEIGHT, WIDTH, win ); + + DESC_W_BEGIN = HEIGHT - DESC_W_HEIGHT - 1; + w_description = catacurses::newwin( DESC_W_HEIGHT, WIDTH - 2, + win + point( 1, DESC_W_BEGIN ) ); + + //40% - 30% - 30% + second_column_x = WIDTH / 2.5f; + third_column_x = second_column_x + WIDTH / 3.3f; + + ui.position_from_window( wMedical ); + } ); + + ui.mark_resize(); + + ui.on_redraw( [&]( const ui_adaptor & ) { + werase( wMedical ); + + draw_border( wMedical, BORDER_COLOR, _( " MEDICAL " ) ); + mvwputch( wMedical, point( getmaxx( wMedical ) - 1, 2 ), BORDER_COLOR, LINE_XOXX ); // -| + + wnoutrefresh( wMedical ); + + draw_medical_titlebar( w_title, this, WIDTH ); + mvwputch( w_title, point( second_column_x, HEADER_Y - 1 ), BORDER_COLOR, LINE_OXXX ); // ^|^ + mvwputch( w_title, point( third_column_x, HEADER_Y - 1 ), BORDER_COLOR, LINE_OXXX ); // ^|^ + wnoutrefresh( w_title ); + + SCROLL_POINT = HEIGHT - TEXT_START_Y - DESC_W_HEIGHT - 3; + + // Columns + + int column_id = 0; + + // Health Summary + fold_and_print( wMedical, point( 2, HEADER_Y ), WIDTH - 2, c_light_blue, _( "HEALTH" ) ); + medical_column health_column = draw_health_summary( column_id++, this, point( 0, TEXT_START_Y ), + std::pair( second_column_x, HEIGHT ) ); + + // Effects Summary + mvwprintz( wMedical, point( second_column_x + 2, HEADER_Y ), c_light_blue, _( "EFFECTS" ) ); + medical_column effects_column = draw_effects_summary( column_id++, this, point( second_column_x, + TEXT_START_Y ), std::pair( third_column_x, HEIGHT ) ); + + // Stats Summary + mvwprintz( wMedical, point( third_column_x + 2, HEADER_Y ), c_light_blue, _( "STATS" ) ); + medical_column stats_column = draw_stats_summary( column_id++, this, point( third_column_x, + TEXT_START_Y ), std::pair( WIDTH - 2, 5 ) ); + + // Description Text + std::string desc_str; + std::pair detail_str; + switch( cursor.x ) { + case 0: + desc_str = health_column.set_highlight( cursor.y ).description(); + detail_str = health_column.detail_str( cursor.y ); + break; + case 1: + desc_str = effects_column.set_highlight( cursor.y ).description(); + detail_str = effects_column.detail_str( cursor.y ); + break; + case 2: + desc_str = stats_column.set_highlight( cursor.y ).description(); + detail_str = stats_column.detail_str( cursor.y ); + break; + default: + break; + } + + // Description Bar + werase( w_description ); + + int DESCRIPTION_WIN_OFFSET; // Y Position of the start of the description bar (wMedical) + + if( !desc_str.empty() ) { + // Number of display rows required by the highlighted line. + std::vector textformatted = foldstring( desc_str, WIDTH - 2, ' ' ); + + // Beginning row of description text [0-6] (w_description) + const int DESCRIPTION_TEXT_Y = DESC_W_HEIGHT - std::min( DESC_W_HEIGHT, + static_cast( textformatted.size() ) ); + + DESCRIPTION_WIN_OFFSET = DESC_W_BEGIN + DESCRIPTION_TEXT_Y; + mvwputch( wMedical, point( 0, DESCRIPTION_WIN_OFFSET - 1 ), BORDER_COLOR, LINE_XXXO ); + mvwhline( wMedical, point( 1, DESCRIPTION_WIN_OFFSET - 1 ), LINE_OXOX, getmaxx( wMedical ) - 2 ); + mvwputch( wMedical, point( getmaxx( wMedical ) - 1, DESCRIPTION_WIN_OFFSET - 1 ), BORDER_COLOR, + LINE_XOXX ); + fold_and_print( w_description, point( 1, DESCRIPTION_TEXT_Y ), WIDTH - 2, c_light_gray, + desc_str ); + } else { + DESCRIPTION_WIN_OFFSET = getmaxy( wMedical ); + } + + wnoutrefresh( w_description ); + + // Info Menu + + INFO_SCROLL_POINT = HEIGHT - INFO_START_Y - DESC_W_HEIGHT - 3; + const int INFO_CUTOFF = DESCRIPTION_WIN_OFFSET - 1 - ( INFO_START_Y + 3 ); + + if( !detail_str.first.empty() ) { + mvwprintz( wMedical, point( third_column_x + 2, INFO_START_Y + 1 ), c_light_blue, + detail_str.first ); + + mvwputch( wMedical, point( third_column_x, INFO_START_Y ), BORDER_COLOR, LINE_XXXO ); // |- + mvwhline( wMedical, point( third_column_x + 1, INFO_START_Y ), LINE_OXOX, + getmaxx( wMedical ) - 2 ); // - + mvwputch( wMedical, point( getmaxx( wMedical ) - 1, INFO_START_Y ), BORDER_COLOR, // -| + LINE_XOXX ); + + const int info_width = WIDTH - third_column_x - 3; + std::vector textformatted = foldstring( detail_str.second, info_width, + ' ' ); + info_lines = textformatted.size(); + int i = 0; + for( std::string &line : textformatted ) { + if( i - info_scroll_position >= INFO_CUTOFF ) { + break; + } + if( i++ >= info_scroll_position ) { + trim_and_print( wMedical, point( third_column_x + 2, INFO_START_Y + 2 + i - info_scroll_position ), + info_width, c_light_gray, line ); + } + } + } else { + info_lines = 0; + } + + // Draw Selection Lines For Each Column + + const int MAX_COLUMN_HEIGHT = DESCRIPTION_WIN_OFFSET - 1; + + health_column.print_column( wMedical, + health_column.current_column( cursor.x ) ? scroll_position : 0, MAX_COLUMN_HEIGHT ); + effects_column.print_column( wMedical, + effects_column.current_column( cursor.x ) ? scroll_position : 0, MAX_COLUMN_HEIGHT ); + stats_column.print_column( wMedical, stats_column.current_column( cursor.x ) ? scroll_position : 0, + MAX_COLUMN_HEIGHT ); + + // Update Cursor Boundaries to number of selectable rows + + cursor_bounds[0] = health_column.selection_count(); + cursor_bounds[1] = effects_column.selection_count(); + cursor_bounds[2] = stats_column.selection_count(); + + // Draw Column Borders + + effects_column.draw_column( wMedical, HEADER_Y, DESCRIPTION_WIN_OFFSET ); + stats_column.draw_column( wMedical, HEADER_Y, DESCRIPTION_WIN_OFFSET ); + + // Draw Scrollbars + + const int content_size = ( cursor_bounds[cursor.x] - 1 ) * 2; + + scrollbar() + .offset_x( 0 ) + .offset_y( HEADER_Y ) + .content_size( content_size ) + .viewport_pos( cursor.y * 2 ) + .viewport_size( DESC_W_BEGIN - 3 ) + .scroll_to_last( true ) + .apply( wMedical ); + + scrollbar() + .offset_x( third_column_x ) + .offset_y( INFO_START_Y + 1 ) + .content_size( info_lines + INFO_SCROLL_POINT ) + .viewport_pos( info_scroll_position ) + .viewport_size( INFO_SCROLL_POINT + 1 ) + .scroll_to_last( true ) + .apply( wMedical ); + + wnoutrefresh( wMedical ); + } ); + + input_context ctxt( "MEDICAL" ); + ctxt.register_action( "UP" ); + ctxt.register_action( "DOWN" ); + ctxt.register_action( "LEFT" ); + ctxt.register_action( "RIGHT" ); + ctxt.register_action( "APPLY" ); + ctxt.register_action( "HELP_KEYBINDINGS" ); + ctxt.register_action( "SCROLL_INFOBOX_UP", to_translation( "Scroll up" ) ); + ctxt.register_action( "SCROLL_INFOBOX_DOWN", to_translation( "Scroll down" ) ); + ctxt.register_action( "QUIT" ); + + for( ;; ) { + ui_manager::redraw(); + const std::string action = ctxt.handle_input(); + + if( action == "DOWN" || action == "UP" ) { + const int step = cursor.y + ( action == "DOWN" ? 1 : -1 ); + const int limit = cursor_bounds[cursor.x] - 1; + + if( step == -1 ) { + cursor.y = limit; + } else if( step > limit ) { + cursor.y = 0; + } else { + cursor.y = step; + } + + const int scroll_overflow = limit - SCROLL_POINT; + if( scroll_overflow > 0 ) { + const int half_list = SCROLL_POINT / 2; + scroll_position = std::max( 0, std::min( scroll_overflow, cursor.y - half_list ) ); + } + info_scroll_position = 0; + } else if( action == "RIGHT" || action == "LEFT" ) { + const int step = cursor.x + ( action == "RIGHT" ? 1 : -1 ); + const int limit = 2; + if( step == -1 ) { + cursor.x = limit; + } else if( step > limit ) { + cursor.x = 0; + } else { + cursor.x = step; + } + + // Match the cursor to the nearest row on the next column. + const int y_limit = cursor_bounds[cursor.x] - 1; + cursor.y = std::min( cursor.y - scroll_position, y_limit ); + + const int scroll_overflow = y_limit - SCROLL_POINT; + if( scroll_overflow > 0 ) { + const int half_list = SCROLL_POINT / 2; + scroll_position = std::max( 0, std::min( scroll_overflow, cursor.y - half_list ) ); + } else { + scroll_position = 0; + } + info_scroll_position = 0; + } else if( action == "APPLY" ) { + avatar_action::use_item( *this ); + } else if( action == "SCROLL_INFOBOX_UP" || action == "SCROLL_INFOBOX_DOWN" ) { + const int scroll_overflow = info_lines - INFO_SCROLL_POINT - 1; + if( scroll_overflow > 0 ) { + const int step = info_scroll_position + ( action == "SCROLL_INFOBOX_DOWN" ? 1 : -1 ); + + if( step == -1 ) { + info_scroll_position = scroll_overflow; + } else if( step > scroll_overflow ) { + info_scroll_position = 0; + } else { + info_scroll_position = step; + } + } else { + info_scroll_position = 0; + } + } else if( action == "QUIT" ) { + break; + } + } +} diff --git a/src/player_display.cpp b/src/player_display.cpp index 87b55b216f18d..c3a30baa97a78 100644 --- a/src/player_display.cpp +++ b/src/player_display.cpp @@ -14,6 +14,7 @@ #include "calendar.h" #include "cata_utility.h" #include "catacharset.h" +#include "character.h" #include "character_modifier.h" #include "color.h" #include "cursesdef.h" @@ -1150,6 +1151,8 @@ static bool handle_player_display_action( Character &you, unsigned int &line, } else if( action == "SCROLL_INFOBOX_DOWN" ) { ++info_line; ui_info.invalidate_ui(); + } else if( action == "MEDICAL_MENU" ) { + you.as_avatar()->disp_medical(); } return done; } @@ -1327,6 +1330,7 @@ void Character::disp_info( bool customize_character ) ctxt.register_action( "SCROLL_INFOBOX_UP", to_translation( "Scroll information box up" ) ); ctxt.register_action( "SCROLL_INFOBOX_DOWN", to_translation( "Scroll information box down" ) ); ctxt.register_action( "HELP_KEYBINDINGS" ); + ctxt.register_action( "MEDICAL_MENU" ); std::map speed_effects; for( auto &elem : *effects ) {