From 962ffff0ba4df3941949ba726328fb1604ac407d Mon Sep 17 00:00:00 2001 From: Dillon Matchett Date: Tue, 4 Apr 2023 07:53:16 -0300 Subject: [PATCH] Revamped Free Start (#64697) * Add difficulty info to character creation screen * Remove some difficulty impact refs * Working needs to be refactored Basic UI Changes and freeform upgrades * Refactored and working, might need balance * Added option for debug * Last few bits of cleanup * Fixes * more fixes * more fixing without compiling * more cleanup of unused * FIX CLANG * spacing Consolidated descriptors, added 1 additional band, Lifestyle Co-Authored-By: pfj <33199288+pfj@users.noreply.github.com> Co-Authored-By: Terrorforge <5107449+Terrorforge@users.noreply.github.com> Co-Authored-By: PatrikLundell <22739822+PatrikLundell@users.noreply.github.com> --------- Co-authored-by: David Seguin Co-authored-by: pfj <33199288+pfj@users.noreply.github.com> Co-authored-by: Terrorforge <5107449+Terrorforge@users.noreply.github.com> Co-authored-by: PatrikLundell <22739822+PatrikLundell@users.noreply.github.com> --- src/avatar.h | 2 + src/character.h | 4 + src/newcharacter.cpp | 416 ++++++++++++++++++++------------------ src/npc.cpp | 109 +++++----- src/npc.h | 1 + src/options.cpp | 11 +- src/player_difficulty.cpp | 374 ++++++++++++++++++++++++++++++++++ src/player_difficulty.h | 63 ++++++ 8 files changed, 735 insertions(+), 245 deletions(-) create mode 100644 src/player_difficulty.cpp create mode 100644 src/player_difficulty.h diff --git a/src/avatar.h b/src/avatar.h index a7184fe7b5b87..c2873ecb65cf8 100644 --- a/src/avatar.h +++ b/src/avatar.h @@ -98,6 +98,8 @@ class avatar : public Character // newcharacter.cpp bool create( character_type type, const std::string &tempname = "" ); + // initialize avatar and avatar mocks + void initialize( character_type type ); void add_profession_items(); void randomize( bool random_scenario, bool play_now = false ); void randomize_cosmetics(); diff --git a/src/character.h b/src/character.h index 4d69831b6e9c8..bdb201c5d8663 100644 --- a/src/character.h +++ b/src/character.h @@ -412,6 +412,10 @@ class Character : public Creature, public visitable Character &operator=( const Character & ) = delete; ~Character() override; + // initialize avatar and avatar mocks + void initialize(); + + Character *as_character() override { return this; } diff --git a/src/newcharacter.cpp b/src/newcharacter.cpp index e2b6e8ec174bd..0599e8cdca623 100644 --- a/src/newcharacter.cpp +++ b/src/newcharacter.cpp @@ -48,6 +48,7 @@ #include "overmap_ui.h" #include "path_info.h" #include "pimpl.h" +#include "player_difficulty.h" #include "profession.h" #include "proficiency.h" #include "recipe.h" @@ -112,18 +113,8 @@ static bool isWide = false; #define COL_HEADER c_white // Captions, like "Profession items" #define COL_NOTE_MINOR c_light_gray // Just regular note -// The point after which stats cost double -static constexpr int HIGH_STAT = 12; - static int skill_increment_cost( const Character &u, const skill_id &skill ); -enum class pool_type { - FREEFORM = 0, - ONE_POOL, - MULTI_POOL, - TRANSFER, -}; - class tab_manager { std::vector &tab_names; @@ -153,10 +144,10 @@ void tab_manager::draw( const catacurses::window &w ) draw_border_below_tabs( w ); for( int i = 1; i < TERMX - 1; i++ ) { - mvwputch( w, point( i, 4 ), BORDER_COLOR, LINE_OXOX ); + mvwputch( w, point( i, 5 ), BORDER_COLOR, LINE_OXOX ); } - mvwputch( w, point( 0, 4 ), BORDER_COLOR, LINE_XXXO ); // |- - mvwputch( w, point( TERMX - 1, 4 ), BORDER_COLOR, LINE_XOXX ); // -| + mvwputch( w, point( 0, 5 ), BORDER_COLOR, LINE_XXXO ); // |- + mvwputch( w, point( TERMX - 1, 5 ), BORDER_COLOR, LINE_XOXX ); // -| } bool tab_manager::handle_input( const std::string &action, const input_context &ctxt ) @@ -348,7 +339,7 @@ static std::string pools_to_string( const avatar &u, pool_type pool ) case pool_type::TRANSFER: return _( "Character Transfer: No changes can be made." ); case pool_type::FREEFORM: - return _( "Freeform" ); + return _( "Survivor" ); } return "If you see this, this is a bug"; } @@ -681,7 +672,12 @@ bool avatar::create( character_type type, const std::string &tempname ) }; tab_manager tabs( character_tabs ); - pool_type pool = pool_type::MULTI_POOL; + const std::string point_pool = get_option( "CHARACTER_POINT_POOLS" ); + pool_type pool = pool_type::FREEFORM; + if( point_pool == "multi_pool" ) { + // if using legacy multipool only set it to that + pool = pool_type::MULTI_POOL; + } switch( type ) { case character_type::CUSTOM: @@ -777,6 +773,13 @@ bool avatar::create( character_type type, const std::string &tempname ) save_template( _( "Last Character" ), pool ); + initialize( type ); + + return true; +} + +void Character::initialize() +{ recalc_hp(); if( has_trait( trait_SMELLY ) ) { @@ -831,15 +834,6 @@ bool avatar::create( character_type type, const std::string &tempname ) learn_recipe( &r ); } } - for( const mtype_id &elem : prof->pets() ) { - starting_pets.push_back( elem ); - } - - if( get_scenario()->vehicle() != vproto_id::NULL_ID() ) { - starting_vehicle = get_scenario()->vehicle(); - } else { - starting_vehicle = prof->vehicle(); - } std::vector prof_addictions = prof->addictions(); for( const addiction &iter : prof_addictions ) { @@ -874,6 +868,25 @@ bool avatar::create( character_type type, const std::string &tempname ) } } + // Activate some mutations right from the start. + for( const trait_id &mut : get_mutations() ) { + const mutation_branch &branch = mut.obj(); + if( branch.starts_active ) { + my_mutations[mut].powered = true; + } + } + + // Ensure that persistent morale effects (e.g. Optimist) are present at the start. + apply_persistent_morale(); + + // Restart cardio accumulator + reset_cardio_acc(); +} + +void avatar::initialize( character_type type ) +{ + this->as_character()->initialize(); + for( const trait_id &t : get_base_traits() ) { std::vector styles; for( const matype_id &s : t->initial_ma_styles ) { @@ -888,23 +901,17 @@ bool avatar::create( character_type type, const std::string &tempname ) } } - // Activate some mutations right from the start. - for( const trait_id &mut : get_mutations() ) { - const mutation_branch &branch = mut.obj(); - if( branch.starts_active ) { - my_mutations[mut].powered = true; - } + for( const mtype_id &elem : prof->pets() ) { + starting_pets.push_back( elem ); } - prof->learn_spells( *this ); - - // Ensure that persistent morale effects (e.g. Optimist) are present at the start. - apply_persistent_morale(); - - // Restart cardio accumulator - reset_cardio_acc(); + if( get_scenario()->vehicle() != vproto_id::NULL_ID() ) { + starting_vehicle = get_scenario()->vehicle(); + } else { + starting_vehicle = prof->vehicle(); + } - return true; + prof->learn_spells( *this ); } static void draw_points( const catacurses::window &w, pool_type pool, const avatar &u, @@ -912,15 +919,20 @@ static void draw_points( const catacurses::window &w, pool_type pool, const avat { // Clear line (except borders) mvwprintz( w, point( 2, 3 ), c_black, std::string( getmaxx( w ) - 3, ' ' ) ); + mvwprintz( w, point( 2, 4 ), c_black, std::string( getmaxx( w ) - 3, ' ' ) ); std::string points_msg = pools_to_string( u, pool ); int pMsg_length = utf8_width( remove_color_tags( points_msg ), true ); nc_color color = c_light_gray; print_colored_text( w, point( 2, 3 ), color, c_light_gray, points_msg ); - if( netPointCost > 0 ) { - mvwprintz( w, point( pMsg_length + 2, 3 ), c_red, " (-%d)", std::abs( netPointCost ) ); - } else if( netPointCost < 0 ) { - mvwprintz( w, point( pMsg_length + 2, 3 ), c_green, " (+%d)", std::abs( netPointCost ) ); + if( pool != pool_type::FREEFORM ) { + if( netPointCost > 0 ) { + mvwprintz( w, point( pMsg_length + 2, 3 ), c_red, " (-%d)", std::abs( netPointCost ) ); + } else if( netPointCost < 0 ) { + mvwprintz( w, point( pMsg_length + 2, 3 ), c_green, " (+%d)", std::abs( netPointCost ) ); + } } + print_colored_text( w, point( 2, 4 ), color, c_light_gray, + player_difficulty::getInstance().difficulty_to_string( u ) ); } template @@ -956,7 +968,7 @@ void set_points( tab_manager &tabs, avatar &u, pool_type &pool ) catacurses::window w_description; const auto init_windows = [&]( ui_adaptor & ui ) { w = catacurses::newwin( TERMY, TERMX, point_zero ); - w_description = catacurses::newwin( TERMY - 10, TERMX - 35, point( 31, 5 ) ); + w_description = catacurses::newwin( TERMY - 11, TERMX - 35, point( 31, 6 ) ); ui.position_from_window( w ); }; init_windows( ui ); @@ -974,23 +986,25 @@ void set_points( tab_manager &tabs, avatar &u, pool_type &pool ) std::vector opts; const point_limit_tuple multi_pool = std::make_tuple( pool_type::MULTI_POOL, - _( "Multiple pools" ), + _( "Legacy: Multiple pools" ), _( "Stats, traits and skills have separate point pools.\n" "Putting stat points into traits and skills is allowed and putting trait points into skills is allowed.\n" - "Scenarios and professions affect skill points." ) ); + "Scenarios and professions affect skill points.\n\n" + "This is a legacy mode. Point totals are no longer balanced." ) ); - const point_limit_tuple one_pool = std::make_tuple( pool_type::ONE_POOL, _( "Single pool" ), - _( "Stats, traits and skills share a single point pool." ) ); + const point_limit_tuple one_pool = std::make_tuple( pool_type::ONE_POOL, _( "Legacy: Single pool" ), + _( "Stats, traits and skills share a single point pool.\n\n" + "This is a legacy mode. Point totals are no longer balanced." ) ); - const point_limit_tuple freeform = std::make_tuple( pool_type::FREEFORM, _( "Freeform" ), - _( "No point limits are enforced." ) ); + const point_limit_tuple freeform = std::make_tuple( pool_type::FREEFORM, _( "Survivor" ), + _( "No point limits are enforced, create a character with the intention of telling a story or challenging yourself." ) ); if( point_pool == "multi_pool" ) { opts = { { multi_pool } }; - } else if( point_pool == "no_freeform" ) { - opts = { { multi_pool, one_pool } }; + } else if( point_pool == "story_teller" ) { + opts = { { freeform } }; } else { - opts = { { multi_pool, one_pool, freeform } }; + opts = { { freeform, multi_pool, one_pool } }; } int highlighted = 0; @@ -1016,7 +1030,7 @@ void set_points( tab_manager &tabs, avatar &u, pool_type &pool ) } else { color = highlighted == i ? COL_SELECT : c_light_gray; } - mvwprintz( w, point( 2, 5 + i ), color, std::get<1>( opts[i] ) ); + mvwprintz( w, point( 2, 6 + i ), color, std::get<1>( opts[i] ) ); } fold_and_print( w_description, point_zero, getmaxx( w_description ), @@ -1073,7 +1087,7 @@ void set_stats( tab_manager &tabs, avatar &u, pool_type pool ) const auto init_windows = [&]( ui_adaptor & ui ) { w = catacurses::newwin( TERMY, TERMX, point_zero ); w_description = catacurses::newwin( 8, TERMX - iSecondColumn - 1, - point( iSecondColumn, 5 ) ); + point( iSecondColumn, 6 ) ); ui.position_from_window( w ); }; init_windows( ui ); @@ -1099,20 +1113,21 @@ void set_stats( tab_manager &tabs, avatar &u, pool_type pool ) // This is description line, meaning its length excludes first column and border const std::string clear_line( getmaxx( w ) - iSecondColumn - 1, ' ' ); mvwprintz( w, point( iSecondColumn, 3 ), c_black, clear_line ); - for( int i = 6; i < 13; i++ ) { + mvwprintz( w, point( iSecondColumn, 4 ), c_black, clear_line ); + for( int i = 7; i < 14; i++ ) { mvwprintz( w, point( iSecondColumn, i ), c_black, clear_line ); } draw_points( w, pool, u ); - mvwprintz( w, point( 2, 5 ), c_light_gray, _( "Strength:" ) ); - mvwprintz( w, point( 16, 5 ), c_light_gray, "%2d", u.str_max ); - mvwprintz( w, point( 2, 6 ), c_light_gray, _( "Dexterity:" ) ); - mvwprintz( w, point( 16, 6 ), c_light_gray, "%2d", u.dex_max ); - mvwprintz( w, point( 2, 7 ), c_light_gray, _( "Intelligence:" ) ); - mvwprintz( w, point( 16, 7 ), c_light_gray, "%2d", u.int_max ); - mvwprintz( w, point( 2, 8 ), c_light_gray, _( "Perception:" ) ); - mvwprintz( w, point( 16, 8 ), c_light_gray, "%2d", u.per_max ); + mvwprintz( w, point( 2, 6 ), c_light_gray, _( "Strength:" ) ); + mvwprintz( w, point( 16, 6 ), c_light_gray, "%2d", u.str_max ); + mvwprintz( w, point( 2, 7 ), c_light_gray, _( "Dexterity:" ) ); + mvwprintz( w, point( 16, 7 ), c_light_gray, "%2d", u.dex_max ); + mvwprintz( w, point( 2, 8 ), c_light_gray, _( "Intelligence:" ) ); + mvwprintz( w, point( 16, 8 ), c_light_gray, "%2d", u.int_max ); + mvwprintz( w, point( 2, 9 ), c_light_gray, _( "Perception:" ) ); + mvwprintz( w, point( 16, 9 ), c_light_gray, "%2d", u.per_max ); werase( w_description ); u.reset_stats(); @@ -1120,9 +1135,9 @@ void set_stats( tab_manager &tabs, avatar &u, pool_type pool ) u.reset_bonuses(); // Removes pollution of stats by modifications appearing inside reset_stats(). Is reset_stats() even necessary in this context? switch( sel ) { case 1: - mvwprintz( w, point( 2, 5 ), COL_SELECT, _( "Strength:" ) ); - mvwprintz( w, point( 16, 5 ), c_light_gray, "%2d", u.str_max ); - if( u.str_max >= HIGH_STAT ) { + mvwprintz( w, point( 2, 6 ), COL_SELECT, _( "Strength:" ) ); + mvwprintz( w, point( 16, 6 ), c_light_gray, "%2d", u.str_max ); + if( u.str_max >= HIGH_STAT && pool != pool_type::FREEFORM ) { mvwprintz( w, point( iSecondColumn, 3 ), c_light_red, _( "Increasing Str further costs 2 points" ) ); } @@ -1140,9 +1155,9 @@ void set_stats( tab_manager &tabs, avatar &u, pool_type pool ) break; case 2: - mvwprintz( w, point( 2, 6 ), COL_SELECT, _( "Dexterity:" ) ); - mvwprintz( w, point( 16, 6 ), c_light_gray, "%2d", u.dex_max ); - if( u.dex_max >= HIGH_STAT ) { + mvwprintz( w, point( 2, 7 ), COL_SELECT, _( "Dexterity:" ) ); + mvwprintz( w, point( 16, 7 ), c_light_gray, "%2d", u.dex_max ); + if( u.dex_max >= HIGH_STAT && pool != pool_type::FREEFORM ) { mvwprintz( w, point( iSecondColumn, 3 ), c_light_red, _( "Increasing Dex further costs 2 points" ) ); } @@ -1161,9 +1176,9 @@ void set_stats( tab_manager &tabs, avatar &u, pool_type pool ) break; case 3: { - mvwprintz( w, point( 2, 7 ), COL_SELECT, _( "Intelligence:" ) ); - mvwprintz( w, point( 16, 7 ), c_light_gray, "%2d", u.int_max ); - if( u.int_max >= HIGH_STAT ) { + mvwprintz( w, point( 2, 8 ), COL_SELECT, _( "Intelligence:" ) ); + mvwprintz( w, point( 16, 8 ), c_light_gray, "%2d", u.int_max ); + if( u.int_max >= HIGH_STAT && pool != pool_type::FREEFORM ) { mvwprintz( w, point( iSecondColumn, 3 ), c_light_red, _( "Increasing Int further costs 2 points" ) ); } @@ -1180,9 +1195,9 @@ void set_stats( tab_manager &tabs, avatar &u, pool_type pool ) break; case 4: - mvwprintz( w, point( 2, 8 ), COL_SELECT, _( "Perception:" ) ); - mvwprintz( w, point( 16, 8 ), c_light_gray, "%2d", u.per_max ); - if( u.per_max >= HIGH_STAT ) { + mvwprintz( w, point( 2, 9 ), COL_SELECT, _( "Perception:" ) ); + mvwprintz( w, point( 16, 9 ), c_light_gray, "%2d", u.per_max ); + if( u.per_max >= HIGH_STAT && pool != pool_type::FREEFORM ) { mvwprintz( w, point( iSecondColumn, 3 ), c_light_red, _( "Increasing Per further costs 2 points" ) ); } @@ -1393,7 +1408,7 @@ void set_traits( tab_manager &tabs, avatar &u, pool_type pool ) w_description = catacurses::newwin( 3, TERMX - 2, point( 1, TERMY - 4 ) ); ui.position_from_window( w ); page_width = std::min( ( TERMX - 4 ) / used_pages, 38 ); - iContentHeight = TERMY - 9; + iContentHeight = TERMY - 10; pos_calc(); }; @@ -1479,14 +1494,17 @@ void set_traits( tab_manager &tabs, avatar &u, pool_type pool ) if( negativeTrait ) { points *= -1; } - mvwprintz( w, point( full_string_length + 3, 3 ), col_tr, - n_gettext( "%s %s %d point", "%s %s %d points", points ), - cursor.name(), - negativeTrait ? _( "earns" ) : _( "costs" ), - points ); + if( pool != pool_type::FREEFORM ) { + mvwprintz( w, point( full_string_length + 3, 3 ), col_tr, + n_gettext( "%s %s %d point", "%s %s %d points", points ), + cursor.name(), + negativeTrait ? _( "earns" ) : _( "costs" ), + points ); + } fold_and_print( w_description, point_zero, TERMX - 2, col_tr, cursor.desc() ); + } nc_color cLine = col_off_pas; @@ -1523,14 +1541,14 @@ void set_traits( tab_manager &tabs, avatar &u, pool_type pool ) cLine = c_light_gray; } - int cur_line_y = 5 + i - start; + int cur_line_y = 6 + i - start; int cur_line_x = 2 + iCurrentPage * page_width; mvwprintz( w, point( cur_line_x, cur_line_y ), cLine, utf8_truncate( cursor.name(), page_width - 2 ) ); } trait_sbs[iCurrentPage].offset_x( page_width * iCurrentPage ) - .offset_y( 5 ) + .offset_y( 6 ) .content_size( traits_size[iCurrentPage] ) .viewport_pos( start ) .viewport_size( iContentHeight ) @@ -1967,7 +1985,7 @@ void set_profession( tab_manager &tabs, avatar &u, pool_type pool ) catacurses::window w_details_pane; scrolling_text_view details( w_details_pane ); bool details_recalc = true; - const int iHeaderHeight = 5; + const int iHeaderHeight = 6; scrollbar list_sb; const auto init_windows = [&]( ui_adaptor & ui ) { iContentHeight = TERMY - iHeaderHeight - 1; @@ -2024,22 +2042,24 @@ void set_profession( tab_manager &tabs, avatar &u, pool_type pool ) // Draw header. draw_points( w, pool, u, netPointCost ); - const char *prof_msg_temp; - if( negativeProf ) { - //~ 1s - profession name, 2d - current character points. - prof_msg_temp = n_gettext( "Profession %1$s earns %2$d point", - "Profession %1$s earns %2$d points", - pointsForProf ); - } else { - //~ 1s - profession name, 2d - current character points. - prof_msg_temp = n_gettext( "Profession %1$s costs %2$d point", - "Profession %1$s costs %2$d points", - pointsForProf ); - } + if( pool != pool_type::FREEFORM ) { + const char *prof_msg_temp; + if( negativeProf ) { + //~ 1s - profession name, 2d - current character points. + prof_msg_temp = n_gettext( "Profession %1$s earns %2$d point", + "Profession %1$s earns %2$d points", + pointsForProf ); + } else { + //~ 1s - profession name, 2d - current character points. + prof_msg_temp = n_gettext( "Profession %1$s costs %2$d point", + "Profession %1$s costs %2$d points", + pointsForProf ); + } - int pMsg_length = utf8_width( remove_color_tags( pools_to_string( u, pool ) ) ); - mvwprintz( w, point( pMsg_length + 9, 3 ), can_afford.success() ? c_green : c_light_red, - prof_msg_temp, sorted_profs[cur_id]->gender_appropriate_name( u.male ), pointsForProf ); + int pMsg_length = utf8_width( remove_color_tags( pools_to_string( u, pool ) ) ); + mvwprintz( w, point( pMsg_length + 9, 3 ), can_afford.success() ? c_green : c_light_red, + prof_msg_temp, sorted_profs[cur_id]->gender_appropriate_name( u.male ), pointsForProf ); + } } //Draw options @@ -2063,12 +2083,12 @@ void set_profession( tab_manager &tabs, avatar &u, pool_type pool ) col = ( cur_id_is_valid && sorted_profs[i] == sorted_profs[cur_id] ? hilite( c_light_green ) : COL_SKILL_USED ); } - mvwprintz( w, point( 2, 5 + i - iStartPos ), col, + mvwprintz( w, point( 2, 6 + i - iStartPos ), col, sorted_profs[i]->gender_appropriate_name( u.male ) ); } list_sb.offset_x( 0 ) - .offset_y( 5 ) + .offset_y( 6 ) .content_size( profs_length ) .viewport_pos( iStartPos ) .viewport_size( iContentHeight ) @@ -2271,7 +2291,7 @@ void set_hobbies( tab_manager &tabs, avatar &u, pool_type pool ) catacurses::window w_details_pane; scrolling_text_view details( w_details_pane ); bool details_recalc = true; - const int iHeaderHeight = 5; + const int iHeaderHeight = 6; scrollbar list_sb; const auto init_windows = [&]( ui_adaptor & ui ) { @@ -2324,24 +2344,27 @@ void set_hobbies( tab_manager &tabs, avatar &u, pool_type pool ) if( negativeProf ) { pointsForProf *= -1; } + // Draw header. draw_points( w, pool, u, netPointCost ); - const char *prof_msg_temp; - if( negativeProf ) { - //~ 1s - profession name, 2d - current character points. - prof_msg_temp = n_gettext( "Background %1$s earns %2$d point", - "Background %1$s earns %2$d points", - pointsForProf ); - } else { - //~ 1s - profession name, 2d - current character points. - prof_msg_temp = n_gettext( "Background %1$s costs %2$d point", - "Background %1$s costs %2$d points", - pointsForProf ); - } + if( pool != pool_type::FREEFORM ) { + const char *prof_msg_temp; + if( negativeProf ) { + //~ 1s - profession name, 2d - current character points. + prof_msg_temp = n_gettext( "Background %1$s earns %2$d point", + "Background %1$s earns %2$d points", + pointsForProf ); + } else { + //~ 1s - profession name, 2d - current character points. + prof_msg_temp = n_gettext( "Background %1$s costs %2$d point", + "Background %1$s costs %2$d points", + pointsForProf ); + } - int pMsg_length = utf8_width( remove_color_tags( pools_to_string( u, pool ) ) ); - mvwprintz( w, point( pMsg_length + 9, 3 ), can_pick.success() ? c_green : c_light_red, - prof_msg_temp, sorted_hobbies[cur_id]->gender_appropriate_name( u.male ), pointsForProf ); + int pMsg_length = utf8_width( remove_color_tags( pools_to_string( u, pool ) ) ); + mvwprintz( w, point( pMsg_length + 9, 3 ), can_pick.success() ? c_green : c_light_red, + prof_msg_temp, sorted_hobbies[cur_id]->gender_appropriate_name( u.male ), pointsForProf ); + } } //Draw options @@ -2363,7 +2386,7 @@ void set_hobbies( tab_manager &tabs, avatar &u, pool_type pool ) } list_sb.offset_x( 0 ) - .offset_y( 5 ) + .offset_y( 6 ) .content_size( hobbies_length ) .viewport_pos( iStartPos ) .viewport_size( iContentHeight ) @@ -2569,7 +2592,7 @@ void set_skills( tab_manager &tabs, avatar &u, pool_type pool ) catacurses::window w_keybindings; std::vector keybinding_hint; int iContentHeight = 0; - const int iHeaderHeight = 5; + const int iHeaderHeight = 6; scrollbar list_sb; input_context ctxt( "NEW_CHAR_SKILLS" ); details.set_up_navigation( ctxt, scrolling_key_scheme::angle_bracket_scroll ); @@ -2598,7 +2621,7 @@ void set_skills( tab_manager &tabs, avatar &u, pool_type pool ) iContentHeight = TERMY - static_cast( keybinding_hint.size() ) - iHeaderHeight - 1; w = catacurses::newwin( TERMY, TERMX, point_zero ); w_list = catacurses::newwin( iContentHeight, 35, point( 1, iHeaderHeight ) ); - w_details_pane = catacurses::newwin( iContentHeight, TERMX - 35, point( 31, 5 ) ); + w_details_pane = catacurses::newwin( iContentHeight, TERMX - 35, point( 31, 6 ) ); details_recalc = true; w_keybindings = catacurses::newwin( static_cast( keybinding_hint.size() ), TERMX - 2, point( 1, iHeaderHeight + iContentHeight ) ); @@ -2707,56 +2730,56 @@ void set_skills( tab_manager &tabs, avatar &u, pool_type pool ) // Write the hint as to upgrade costs const int cost = skill_increment_cost( u, currentSkill->ident() ); const int level = u.get_skill_level( currentSkill->ident() ); - const int upgrade_levels = level == 0 ? 2 : 1; - // We have two different strings to pluralize, so we have to use two translation calls. - const std::string upgrade_levels_s = string_format( - //~ levels here are skill levels at character creation time - n_gettext( "%d level", "%d levels", upgrade_levels ), upgrade_levels ); - const nc_color color = skill_points_left( u, pool ) >= cost ? COL_SKILL_USED : c_light_red; - mvwprintz( w, point( remaining_points_length + 9, 3 ), color, - //~ Second string is e.g. "1 level" or "2 levels" - n_gettext( "Upgrading %s by %s costs %d point", - "Upgrading %s by %s costs %d points", cost ), - currentSkill->name(), upgrade_levels_s, cost ); + if( pool != pool_type::FREEFORM ) { + // in pool the first level of a skill gives 2 + const int upgrade_levels = level == 0 ? 2 : 1; + // We have two different strings to pluralize, so we have to use two translation calls. + const std::string upgrade_levels_s = string_format( + //~ levels here are skill levels at character creation time + n_gettext( "%d level", "%d levels", upgrade_levels ), upgrade_levels ); + const nc_color color = skill_points_left( u, pool ) >= cost ? COL_SKILL_USED : c_light_red; + mvwprintz( w, point( remaining_points_length + 9, 3 ), color, + //~ Second string is e.g. "1 level" or "2 levels" + n_gettext( "Upgrading %s by %s costs %d point", + "Upgrading %s by %s costs %d points", cost ), + currentSkill->name(), upgrade_levels_s, cost ); + } calcStartPos( cur_offset, cur_pos, iContentHeight, num_skills ); for( int i = cur_offset; i < num_skills && i - cur_offset < iContentHeight; ++i ) { const int y = i - cur_offset; const skill_displayType_id &display_type = skill_list[i].first; const Skill *thisSkill = skill_list[i].second; + int prof_skill_level = 0; + if( !!thisSkill ) { + for( auto &prof_skill : u.prof->skills() ) { + if( prof_skill.first == thisSkill->ident() ) { + prof_skill_level += prof_skill.second; + break; + } + } + } if( !thisSkill ) { mvwprintz( w_list, point( 1, y ), c_yellow, display_type->display_string() ); - } else if( u.get_skill_level( thisSkill->ident() ) == 0 ) { + } else if( u.get_skill_level( thisSkill->ident() ) + prof_skill_level == 0 ) { mvwprintz( w_list, point( 1, y ), ( i == cur_pos ? COL_SELECT : c_light_gray ), thisSkill->name() ); } else { mvwprintz( w_list, point( 1, y ), ( i == cur_pos ? hilite( COL_SKILL_USED ) : COL_SKILL_USED ), thisSkill->name() ); - wprintz( w_list, ( i == cur_pos ? hilite( COL_SKILL_USED ) : COL_SKILL_USED ), - " ( %d )", u.get_skill_level( thisSkill->ident() ) ); - } - - int skill_level = 0; - - // Grab skills from profession - if( !!thisSkill ) { - for( auto &prof_skill : u.prof->skills() ) { - if( prof_skill.first == thisSkill->ident() ) { - skill_level += prof_skill.second; - break; - } + if( prof_skill_level > 0 ) { + wprintz( w_list, ( i == cur_pos ? hilite( COL_SKILL_USED ) : COL_SKILL_USED ), + " ( %d + %d )", prof_skill_level, u.get_skill_level( thisSkill->ident() ) ); + } else { + wprintz( w_list, ( i == cur_pos ? hilite( COL_SKILL_USED ) : COL_SKILL_USED ), + " ( %d )", u.get_skill_level( thisSkill->ident() ) ); } } - - // Only show bonus if we are above 0 - if( skill_level > 0 ) { - wprintz( w, ( i == cur_pos ? h_white : c_white ), " (+%d)", skill_level ); - } } list_sb.offset_x( 0 ) - .offset_y( 5 ) + .offset_y( 6 ) .content_size( num_skills ) .viewport_pos( cur_offset ) .viewport_size( iContentHeight ) @@ -2811,7 +2834,8 @@ void set_skills( tab_manager &tabs, avatar &u, pool_type pool ) if( level > 0 ) { // For balance reasons, increasing a skill from level 0 gives 1 extra level for free, but // decreasing it from level 2 forfeits the free extra level (thus changes it to 0) - u.mod_skill_level( skill_id, level == 2 ? -2 : -1 ); + // this only matters in legacy character creation modes + u.mod_skill_level( skill_id, level == 2 && pool != pool_type::FREEFORM ? -2 : -1 ); u.set_knowledge_level( skill_id, u.get_skill_level( skill_id ) ); } details_recalc = true; @@ -2820,7 +2844,8 @@ void set_skills( tab_manager &tabs, avatar &u, pool_type pool ) const int level = u.get_skill_level( skill_id ); if( level < MAX_SKILL ) { // For balance reasons, increasing a skill from level 0 gives 1 extra level for free - u.mod_skill_level( skill_id, level == 0 ? +2 : +1 ); + // this only matters in legacy character creation modes + u.mod_skill_level( skill_id, level == 0 && pool != pool_type::FREEFORM ? +2 : +1 ); u.set_knowledge_level( skill_id, u.get_skill_level( skill_id ) ); } details_recalc = true; @@ -2981,7 +3006,7 @@ void set_scenario( tab_manager &tabs, avatar &u, pool_type pool ) catacurses::window w_details_pane; scrolling_text_view details( w_details_pane ); bool details_recalc = true; - const int iHeaderHeight = 5; + const int iHeaderHeight = 6; scrollbar list_sb; const auto init_windows = [&]( ui_adaptor & ui ) { @@ -3039,22 +3064,25 @@ void set_scenario( tab_manager &tabs, avatar &u, pool_type pool ) if( negativeScen ) { pointsForScen *= -1; } - // Draw header. draw_points( w, pool, u, netPointCost ); + if( pool != pool_type::FREEFORM ) { - const char *scen_msg_temp; - if( negativeScen ) { - scen_msg_temp = n_gettext( "Scenario earns %2$d point", - "Scenario earns %2$d points", pointsForScen ); - } else { - scen_msg_temp = n_gettext( "Scenario costs %2$d point", - "Scenario costs %2$d points", pointsForScen ); - } + const char *scen_msg_temp; - int pMsg_length = utf8_width( remove_color_tags( pools_to_string( u, pool ) ) ); - mvwprintz( w, point( pMsg_length + 9, 3 ), can_afford.success() ? c_green : c_light_red, - scen_msg_temp, sorted_scens[cur_id]->gender_appropriate_name( u.male ), pointsForScen ); + if( negativeScen ) { + scen_msg_temp = n_gettext( "Scenario earns %2$d point", + "Scenario earns %2$d points", pointsForScen ); + } else { + scen_msg_temp = n_gettext( "Scenario costs %2$d point", + "Scenario costs %2$d points", pointsForScen ); + } + + + int pMsg_length = utf8_width( remove_color_tags( pools_to_string( u, pool ) ) ); + mvwprintz( w, point( pMsg_length + 9, 3 ), can_afford.success() ? c_green : c_light_red, + scen_msg_temp, sorted_scens[cur_id]->gender_appropriate_name( u.male ), pointsForScen ); + } } //Draw options @@ -3079,13 +3107,13 @@ void set_scenario( tab_manager &tabs, avatar &u, pool_type pool ) col = ( cur_id_is_valid && sorted_scens[i] == sorted_scens[cur_id] ? hilite( c_light_green ) : COL_SKILL_USED ); } - mvwprintz( w, point( 2, 5 + i - iStartPos ), col, + mvwprintz( w, point( 2, 6 + i - iStartPos ), col, sorted_scens[i]->gender_appropriate_name( u.male ) ); } list_sb.offset_x( 0 ) - .offset_y( 5 ) + .offset_y( 6 ) .content_size( scens_length ) .viewport_pos( iStartPos ) .viewport_size( iContentHeight ) @@ -3289,38 +3317,38 @@ void set_description( tab_manager &tabs, avatar &you, const bool allow_reroll, const int begin_sncol = TERMX / 2; if( isWide ) { w = catacurses::newwin( TERMY, TERMX, point_zero ); - w_name = catacurses::newwin( 2, ncol2 + 2, point( 2, 5 ) ); - w_gender = catacurses::newwin( 1, ncol2 + 2, point( 2, 7 ) ); - w_location = catacurses::newwin( 1, ncol3, point( beginx3, 5 ) ); - w_vehicle = catacurses::newwin( 1, ncol3, point( beginx3, 6 ) ); - w_addictions = catacurses::newwin( 1, ncol3, point( beginx3, 7 ) ); - w_stats = catacurses::newwin( 6, 20, point( 2, 9 ) ); - w_traits = catacurses::newwin( TERMY - 10, ncol2, point( beginx2, 9 ) ); - w_bionics = catacurses::newwin( TERMY - 10, ncol3, point( beginx3, 9 ) ); - w_proficiencies = catacurses::newwin( TERMY - 20, 19, point( 2, 15 ) ); + w_name = catacurses::newwin( 2, ncol2 + 2, point( 2, 6 ) ); + w_gender = catacurses::newwin( 1, ncol2 + 2, point( 2, 8 ) ); + w_location = catacurses::newwin( 1, ncol3, point( beginx3, 6 ) ); + w_vehicle = catacurses::newwin( 1, ncol3, point( beginx3, 7 ) ); + w_addictions = catacurses::newwin( 1, ncol3, point( beginx3, 8 ) ); + w_stats = catacurses::newwin( 6, 20, point( 2, 10 ) ); + w_traits = catacurses::newwin( TERMY - 11, ncol2, point( beginx2, 10 ) ); + w_bionics = catacurses::newwin( TERMY - 11, ncol3, point( beginx3, 10 ) ); + w_proficiencies = catacurses::newwin( TERMY - 21, 19, point( 2, 16 ) ); // Extra - 11 to avoid overlap with long text in w_guide. - w_hobbies = catacurses::newwin( TERMY - 10 - 11, ncol4, point( beginx4, 9 ) ); + w_hobbies = catacurses::newwin( TERMY - 11 - 11, ncol4, point( beginx4, 10 ) ); w_scenario = catacurses::newwin( 1, ncol2, point( beginx2, 3 ) ); w_profession = catacurses::newwin( 1, ncol3, point( beginx3, 3 ) ); - w_skills = catacurses::newwin( TERMY - 10, 23, point( 22, 9 ) ); + w_skills = catacurses::newwin( TERMY - 11, 23, point( 22, 10 ) ); w_guide = catacurses::newwin( 9, TERMX - 3, point( 2, TERMY - 10 ) ); - w_height = catacurses::newwin( 1, ncol2, point( beginx2, 5 ) ); - w_age = catacurses::newwin( 1, ncol2, point( beginx2, 6 ) ); - w_blood = catacurses::newwin( 1, ncol2, point( beginx2, 7 ) ); + w_height = catacurses::newwin( 1, ncol2, point( beginx2, 6 ) ); + w_age = catacurses::newwin( 1, ncol2, point( beginx2, 7 ) ); + w_blood = catacurses::newwin( 1, ncol2, point( beginx2, 8 ) ); ui.position_from_window( w ); } else { w = catacurses::newwin( TERMY, TERMX, point_zero ); - w_name = catacurses::newwin( 1, ncol_small, point( 2, 5 ) ); - w_gender = catacurses::newwin( 1, ncol_small, point( 2, 6 ) ); - w_height = catacurses::newwin( 1, ncol_small, point( 2, 7 ) ); - w_age = catacurses::newwin( 1, ncol_small, point( begin_sncol, 5 ) ); - w_blood = catacurses::newwin( 1, ncol_small, point( begin_sncol, 6 ) ); - w_location = catacurses::newwin( 1, ncol_small, point( begin_sncol, 7 ) ); - w_stats = catacurses::newwin( 6, ncol_small, point( 2, 9 ) ); - w_scenario = catacurses::newwin( 1, ncol_small, point( begin_sncol, 9 ) ); - w_profession = catacurses::newwin( 1, ncol_small, point( begin_sncol, 10 ) ); - w_vehicle = catacurses::newwin( 2, ncol_small, point( begin_sncol, 12 ) ); - w_addictions = catacurses::newwin( 2, ncol_small, point( begin_sncol, 14 ) ); + w_name = catacurses::newwin( 1, ncol_small, point( 2, 6 ) ); + w_gender = catacurses::newwin( 1, ncol_small, point( 2, 7 ) ); + w_height = catacurses::newwin( 1, ncol_small, point( 2, 8 ) ); + w_age = catacurses::newwin( 1, ncol_small, point( begin_sncol, 6 ) ); + w_blood = catacurses::newwin( 1, ncol_small, point( begin_sncol, 7 ) ); + w_location = catacurses::newwin( 1, ncol_small, point( begin_sncol, 8 ) ); + w_stats = catacurses::newwin( 6, ncol_small, point( 2, 10 ) ); + w_scenario = catacurses::newwin( 1, ncol_small, point( begin_sncol, 10 ) ); + w_profession = catacurses::newwin( 1, ncol_small, point( begin_sncol, 11 ) ); + w_vehicle = catacurses::newwin( 2, ncol_small, point( begin_sncol, 13 ) ); + w_addictions = catacurses::newwin( 2, ncol_small, point( begin_sncol, 15 ) ); w_guide = catacurses::newwin( 2, TERMX - 3, point( 2, TERMY - 3 ) ); ui.position_from_window( w ); } @@ -3398,7 +3426,7 @@ void set_description( tab_manager &tabs, avatar &you, const bool allow_reroll, //Draw the line between editable and non-editable stuff. for( int i = 0; i < getmaxx( w ); ++i ) { if( i == 0 ) { - mvwputch( w, point( i, 8 ), BORDER_COLOR, LINE_XXXO ); + mvwputch( w, point( i, 9 ), BORDER_COLOR, LINE_XXXO ); } else if( i == getmaxx( w ) - 1 ) { wputch( w, BORDER_COLOR, LINE_XOXX ); } else { diff --git a/src/npc.cpp b/src/npc.cpp index e89850210e2b0..89a4e9b4aed58 100644 --- a/src/npc.cpp +++ b/src/npc.cpp @@ -1560,31 +1560,63 @@ void npc::invalidate_range_cache() void npc::form_opinion( const Character &you ) { + op_of_u = get_opinion_values( you ); + + decide_needs(); + for( const npc_need &i : needs ) { + if( i == need_food || i == need_drink ) { + op_of_u.value += 2; + } + } + + if( op_of_u.fear < personality.bravery + 10 && + op_of_u.fear - personality.aggression > -10 && op_of_u.trust > -8 ) { + set_attitude( NPCATT_TALK ); + } else if( op_of_u.fear - 2 * personality.aggression - personality.bravery < -30 ) { + set_attitude( NPCATT_KILL ); + } else if( my_fac && my_fac->likes_u < -10 ) { + if( is_player_ally() ) { + mutiny(); + } + set_attitude( NPCATT_KILL ); + } else { + set_attitude( NPCATT_FLEE_TEMP ); + } + + add_msg_debug( debugmode::DF_NPC, "%s formed an opinion of you: %s", get_name(), + npc_attitude_id( attitude ) ); +} + +npc_opinion npc::get_opinion_values( const Character &you ) const +{ + npc_opinion npc_values = op_of_u; + + const item_location weapon = you.get_wielded_item(); // FEAR if( !you.is_armed() ) { // Unarmed, but actually unarmed ("unarmed weapons" are not unarmed) - op_of_u.fear -= 3; + npc_values.fear -= 3; } else if( weapon->is_gun() ) { // TODO: Make bows not guns if( weapon->has_flag( flag_PRIMITIVE_RANGED_WEAPON ) ) { - op_of_u.fear += 2; + npc_values.fear += 2; } else { - op_of_u.fear += 6; + npc_values.fear += 6; } } else if( you.weapon_value( *weapon ) > 20 ) { - op_of_u.fear += 2; + npc_values.fear += 2; } ///\EFFECT_STR increases NPC fear of the player if( you.str_max >= 16 ) { - op_of_u.fear += 2; + npc_values.fear += 2; } else if( you.str_max >= 12 ) { - op_of_u.fear += 1; + npc_values.fear += 1; } else if( you.str_max <= 3 ) { - op_of_u.fear -= 3; + npc_values.fear -= 3; } else if( you.str_max <= 5 ) { - op_of_u.fear -= 1; + npc_values.fear -= 1; } // is your health low @@ -1592,7 +1624,7 @@ void npc::form_opinion( const Character &you ) const int hp_max = elem.second.get_hp_max(); const int hp_cur = elem.second.get_hp_cur(); if( hp_cur <= hp_max / 2 ) { - op_of_u.fear--; + npc_values.fear--; } } @@ -1601,15 +1633,15 @@ void npc::form_opinion( const Character &you ) const int hp_max = elem.second.get_hp_max(); const int hp_cur = elem.second.get_hp_cur(); if( hp_cur <= hp_max / 2 ) { - op_of_u.fear++; + npc_values.fear++; } } if( you.has_trait( trait_SAPIOVORE ) ) { - op_of_u.fear += 10; // Sapiovores = Scary + npc_values.fear += 10; // Sapiovores = Scary } if( you.has_trait( trait_TERRIFYING ) ) { - op_of_u.fear += 6; + npc_values.fear += 6; } int u_ugly = 0; @@ -1623,79 +1655,58 @@ void npc::form_opinion( const Character &you ) u_ugly += bp->ugliness_mandatory; u_ugly += bp->ugliness - ( bp->ugliness * worn.get_coverage( bp ) / 100 ); } - op_of_u.fear += u_ugly / 2; - op_of_u.trust -= u_ugly / 3; + npc_values.fear += u_ugly / 2; + npc_values.trust -= u_ugly / 3; if( you.get_stim() > 20 ) { - op_of_u.fear++; + npc_values.fear++; } if( you.has_effect( effect_drunk ) ) { - op_of_u.fear -= 2; + npc_values.fear -= 2; } // TRUST if( op_of_u.fear > 0 ) { - op_of_u.trust -= 3; + npc_values.trust -= 3; } else { - op_of_u.trust += 1; + npc_values.trust += 1; } if( weapon && weapon->is_gun() ) { - op_of_u.trust -= 2; + npc_values.trust -= 2; } else if( !you.is_armed() ) { - op_of_u.trust += 2; + npc_values.trust += 2; } // TODO: More effects if( you.has_effect( effect_high ) ) { - op_of_u.trust -= 1; + npc_values.trust -= 1; } if( you.has_effect( effect_drunk ) ) { - op_of_u.trust -= 2; + npc_values.trust -= 2; } if( you.get_stim() > 20 || you.get_stim() < -20 ) { - op_of_u.trust -= 1; + npc_values.trust -= 1; } if( you.get_painkiller() > 30 ) { - op_of_u.trust -= 1; + npc_values.trust -= 1; } if( op_of_u.trust > 0 ) { // Trust is worth a lot right now - op_of_u.trust /= 2; + npc_values.trust /= 2; } // VALUE - op_of_u.value = 0; + npc_values.value = 0; for( const std::pair &elem : get_body() ) { if( elem.second.get_hp_cur() < elem.second.get_hp_max() * 0.8f ) { - op_of_u.value++; - } - } - decide_needs(); - for( const npc_need &i : needs ) { - if( i == need_food || i == need_drink ) { - op_of_u.value += 2; + npc_values.value++; } } - if( op_of_u.fear < personality.bravery + 10 && - op_of_u.fear - personality.aggression > -10 && op_of_u.trust > -8 ) { - set_attitude( NPCATT_TALK ); - } else if( op_of_u.fear - 2 * personality.aggression - personality.bravery < -30 ) { - set_attitude( NPCATT_KILL ); - } else if( my_fac && my_fac->likes_u < -10 ) { - if( is_player_ally() ) { - mutiny(); - } - set_attitude( NPCATT_KILL ); - } else { - set_attitude( NPCATT_FLEE_TEMP ); - } - - add_msg_debug( debugmode::DF_NPC, "%s formed an opinion of you: %s", get_name(), - npc_attitude_id( attitude ) ); + return npc_values; } void npc::mutiny() diff --git a/src/npc.h b/src/npc.h index 9c697710e46bd..a2788640ceeca 100644 --- a/src/npc.h +++ b/src/npc.h @@ -818,6 +818,7 @@ class npc : public Character // Interaction with the player void form_opinion( const Character &you ); + npc_opinion get_opinion_values( const Character &you ) const; std::string pick_talk_topic( const Character &u ); std::string const &get_specified_talk_topic( std::string const &topic_id ); float character_danger( const Character &u ) const; diff --git a/src/options.cpp b/src/options.cpp index ffd849035018a..eb0af9a14a424 100644 --- a/src/options.cpp +++ b/src/options.cpp @@ -2633,8 +2633,8 @@ void options_manager::add_options_world_default() add( "CHARACTER_POINT_POOLS", "world_default", to_translation( "Character point pools" ), to_translation( "Allowed point pools for character generation." ), - { { "any", to_translation( "Any" ) }, { "multi_pool", to_translation( "Multi-pool only" ) }, { "no_freeform", to_translation( "No freeform" ) } }, - "any" + { { "any", to_translation( "Any" ) }, { "multi_pool", to_translation( "Legacy Multipool" ) }, { "story_teller", to_translation( "Survivor" ) } }, + "story_teller" ); add_empty_line(); @@ -2680,6 +2680,13 @@ void options_manager::add_options_debug() add_empty_line(); + add( "DEBUG_DIFFICULTIES", "debug", to_translation( "Show values for character creation" ), + to_translation( "In character creation will show the underlying value that is used to determine difficulty." ), + false + ); + + add_empty_line(); + add( "SKILL_TRAINING_SPEED", "debug", to_translation( "Skill training speed" ), to_translation( "Scales experience gained from practicing skills and reading books. 0.5 is half as fast as default, 2.0 is twice as fast, 0.0 disables skill training except for NPC training." ), 0.0, 100.0, 1.0, 0.1 diff --git a/src/player_difficulty.cpp b/src/player_difficulty.cpp new file mode 100644 index 0000000000000..1948a332971c0 --- /dev/null +++ b/src/player_difficulty.cpp @@ -0,0 +1,374 @@ +#include "avatar.h" +#include "character_martial_arts.h" +#include "martialarts.h" +#include "mutation.h" +#include "options.h" +#include "player_difficulty.h" +#include "profession.h" +#include "skill.h" + + +player_difficulty::player_difficulty() +{ + // set up an average NPC + average = npc(); + reset_npc( average ); +} + +// creates an npc with similar stats to an avatar for testing +void player_difficulty::npc_from_avatar( const avatar &u, npc &dummy ) +{ + // set stats + dummy.str_max = u.str_max; + dummy.dex_max = u.dex_max; + dummy.int_max = u.int_max; + dummy.per_max = u.per_max; + dummy.reset_stats(); + + + // set skills + for( const auto &t : u.get_all_skills() ) { + dummy.set_skill_level( t.first, t.second.level() ); + } + + // set profession and hobbies + dummy.prof = u.prof; + dummy.hobbies = u.hobbies; + + // set mutations + for( const trait_id &t : u.get_mutations( true ) ) { + dummy.set_mutation( t ); + } + + dummy.initialize(); +} + +void player_difficulty::reset_npc( Character &dummy ) +{ + dummy.set_body(); + dummy.normalize(); // In particular this clears martial arts style + + // delete all worn items. + dummy.worn.clear(); + dummy.calc_encumbrance(); + dummy.inv->clear(); + dummy.remove_weapon(); + dummy.clear_mutations(); + + // Clear stomach and then eat a nutritious meal to normalize stomach + // contents (needs to happen before clear_morale). + dummy.stomach.empty(); + dummy.guts.empty(); + dummy.clear_vitamins(); + + // This sets HP to max, clears addictions and morale, + // and sets hunger, thirst, fatigue and such to zero + dummy.environmental_revert_effect(); + // However, the above does not set stored kcal + dummy.set_stored_kcal( dummy.get_healthy_kcal() ); + + dummy.empty_skills(); + dummy.martial_arts_data->clear_styles(); + dummy.clear_morale(); + dummy.clear_bionics(); + dummy.activity.set_to_null(); + dummy.reset_chargen_attributes(); + dummy.set_pain( 0 ); + dummy.reset_bonuses(); + dummy.set_speed_base( 100 ); + dummy.set_speed_bonus( 0 ); + dummy.set_sleep_deprivation( 0 ); + for( const proficiency_id &prof : dummy.known_proficiencies() ) { + dummy.lose_proficiency( prof, true ); + } + + // Reset cardio_acc to baseline + dummy.reset_cardio_acc(); + // Restore all stamina and go to walk mode + dummy.set_stamina( dummy.get_stamina_max() ); + dummy.reset_activity_level(); + + // Make sure we don't carry around weird effects. + dummy.clear_effects(); + + // Make stats nominal. + dummy.str_max = 8; + dummy.dex_max = 8; + dummy.int_max = 8; + dummy.per_max = 8; + dummy.set_str_bonus( 0 ); + dummy.set_dex_bonus( 0 ); + dummy.set_int_bonus( 0 ); + dummy.set_per_bonus( 0 ); +} + +double player_difficulty::calc_armor_value( const Character &u, bodypart_id bp ) +{ + // a low damage value to be concerned about early + // only concerned with bash damage since it is most + // prevalent early game + + + + float armor_val = 0.0f; + + // check any other items the character has on them + if( u.prof ) { + for( const item &i : u.prof->items( true, std::vector() ) ) { + armor_val += i.resist( damage_type::BASH, false, bp ); + } + } + + return armor_val; +} + +std::string player_difficulty::get_defense_difficulty( const Character &u ) const +{ + // figure out how survivable the character is. + + // the percent margin between result bands + const float percent_band = 0.5f; + + // guess at the ammount of armor an average clothed person would have + float base_armor = 2.0f; + + // how much each portion is valued compared to the deviation of others + const float standard_movement = 1.0f; + const float difficult_movement = 2.0f; + const float dodge = 1.0f; + const float head_protection = 0.2f; + const float torso_protection = 0.4f; + const float arms_protection = 0.2f; + const float legs_protection = 0.2f; + + // calculate move cost on simple ground + int player_run_cost = u.run_cost( 100 ); + int average_run_cost = average.run_cost( 100 ); + float per = standard_movement * ( player_run_cost - average_run_cost ) / average_run_cost; + + // calculate move cost on simple ground + player_run_cost = u.run_cost( 400 ); + average_run_cost = average.run_cost( 400 ); + per += difficult_movement * ( player_run_cost - average_run_cost ) / average_run_cost; + + // calculate head armor + per += head_protection * ( calc_armor_value( u, bodypart_id( "head" ) ) - base_armor ) / base_armor; + // calculate torso armor + per += torso_protection * ( calc_armor_value( u, + bodypart_id( "torso" ) ) - base_armor ) / base_armor; + // calculate arm armor + per += arms_protection * ( calc_armor_value( u, + bodypart_id( "arm_r" ) ) - base_armor ) / base_armor; + // calculate leg armor + per += legs_protection * ( calc_armor_value( u, + bodypart_id( "leg_r" ) ) - base_armor ) / base_armor; + // calculate dodge + per += dodge * ( u.get_dodge() - average.get_dodge() ) / average.get_dodge(); + + return format_output( percent_band, per ); +} + +double player_difficulty::calc_dps_value( const Character &u ) +{ + // check against the big three + // efficient early weapons you can easily get access to + item early_piercing = item( "knife_combat" ); + item early_cutting = item( "machete" ); + item early_bashing = item( "bat" ); + + double baseline = std::max( u.weapon_value( early_piercing ), + u.weapon_value( early_cutting ) ); + baseline = std::max( baseline, u.weapon_value( early_bashing ) ); + + + + // check any other items the character has on them + if( u.prof ) { + for( const item &i : u.prof->items( true, std::vector() ) ) { + baseline = std::max( baseline, u.weapon_value( i ) ); + } + } + + return baseline; +} + +std::string player_difficulty::get_combat_difficulty( const Character &u ) const +{ + // figure out how good the player is at combat. + // get the best dps of a basic civilian with 3 scavengable weapons + // compare to the dps of this character with 1 of those 3 or their best weapon they spawn with + + // the percent margin between result bands + const float percent_band = 0.5f; + + double npc_dps = calc_dps_value( average ); + double player_dps = calc_dps_value( u ); + + float per = ( player_dps - npc_dps ) / npc_dps; + + return format_output( percent_band, per ); +} + +std::string player_difficulty::get_genetics_difficulty( const Character &u ) const +{ + // figure out how genetically advantaged the character is + + // how many attributes the average character has + const int average_stats = 38; + // how much stats above HIGH_STAT penalize the player + const int high_stat_penalty = 4; + // how much traits are valued at + const int trait_value = 1; + // the percent margin between result bands + const float percent_band = 0.1f; + + + int genetics_total = u.str_max + u.dex_max + u.per_max + u.int_max; + genetics_total += std::max( 0, u.str_max - HIGH_STAT ) * high_stat_penalty; + genetics_total += std::max( 0, u.dex_max - HIGH_STAT ) * high_stat_penalty; + genetics_total += std::max( 0, u.per_max - HIGH_STAT ) * high_stat_penalty; + genetics_total += std::max( 0, u.int_max - HIGH_STAT ) * high_stat_penalty; + + // each trait effects genetics slightly as well + for( const trait_id &trait : u.get_mutations( true ) ) { + if( trait->points > 0 ) { + genetics_total += trait_value; + } else if( trait->points < 0 ) { + genetics_total += -1 * trait_value; + } + } + + float per = static_cast( genetics_total - average_stats ) / static_cast + ( average_stats ); + + return format_output( percent_band, per ); +} + +std::string player_difficulty::get_expertise_difficulty( const Character &u ) const +{ + // a bit extra over multipool since you get 2 points per in multi + const int average_skill_ranks = 4; + const float percent_band = 0.6f; + + // how much each proficiency is valued compared to a skill point + const int proficiency_value = 2; + + // how much each portion is valued compared to the deviation of others + const float skill_weighting = 1.0f; + const float reading_weighting = 2.0f; + const float learn_weighting = 2.0f; + const float focus_weighting = 2.0f; + + + // sum player skills and proficiencies + int player_skills = 0; + // every skill point is worth 1 point of value + for( const auto &t : u.get_all_skills() ) { + // combat skills will be handled in offence + if( !t.first->is_combat_skill() ) { + player_skills += t.second.level(); + } + } + // every proficiency is worth about the value of 2 skill points + player_skills += proficiency_value * u._proficiencies->known_profs().size(); + + // skills and professions + float per = skill_weighting * static_cast( player_skills - average_skill_ranks ) / + static_cast( average_skill_ranks ); + + // focus + per += focus_weighting * static_cast( u.calc_focus_equilibrium( + true ) - average.calc_focus_equilibrium( + true ) ) / static_cast( average.calc_focus_equilibrium( true ) ); + + // reading speed negative is good + per -= reading_weighting * static_cast( u.read_speed() - average.read_speed() ) / + static_cast( average.read_speed() ); + + // how much each point of experience is worth to your character + per += learn_weighting * static_cast( u.adjust_for_focus( 100 ) - + average.adjust_for_focus( 100 ) ) / static_cast( average.adjust_for_focus( 100 ) ); + + + + return format_output( percent_band, per ); +} + + +int player_difficulty::calc_social_value( const Character &u, const npc &compare ) +{ + // weighting skill and underlying values equally + + int social = std::max( u.intimidation(), u.persuade_skill() ); + int lying = u.lie_skill(); + + npc_opinion npc_opinion = compare.get_opinion_values( u ); + + int oppinion = npc_opinion.trust; + oppinion -= npc_opinion.anger; + oppinion += npc_opinion.fear; + oppinion += npc_opinion.value; + + return social + lying + oppinion; +} + +std::string player_difficulty::get_social_difficulty( const Character &u ) const +{ + // compare the characters social value to an average npc + int player_val = calc_social_value( u, average ); + int average_val = calc_social_value( average, average ); + + const float percent_band = 0.6f; + + + float per = static_cast( player_val - average_val ) / static_cast + ( average_val ); + + return format_output( percent_band, per ); +} + +std::string player_difficulty::format_output( float percent_band, float per ) +{ + std::string output; + if( per < -1 * percent_band ) { + output = string_format( "%s", "light_red", _( "underpowered" ) ); + } else if( per < 0.0f ) { + output = string_format( "%s", "light_red", _( "weak" ) ); + } else if( per < percent_band ) { + output = string_format( "%s", "yellow", _( "average" ) ); + } else if( per < 2 * percent_band ) { + output = string_format( "%s", "light_green", _( "strong" ) ); + } else if( per < 3 * percent_band ) { + output = string_format( "%s", "light_green", _( "powerful" ) ); + } else { + output = string_format( "%s", "light_green", _( "overpowered" ) ); + } + + if( get_option( "DEBUG_DIFFICULTIES" ) ) { + output = string_format( "%2f: %s", per, output ); + } + return output; +} + +std::string player_difficulty::difficulty_to_string( const avatar &u ) const +{ + // make a faux avatar that can have the effects of creation applied to it + npc n = npc(); + reset_npc( n ); + npc_from_avatar( u, n ); + + std::string genetics = get_genetics_difficulty( n ); + std::string socials = get_social_difficulty( n ); + std::string expertise = get_expertise_difficulty( n ); + std::string combat = get_combat_difficulty( n ); + std::string defense = get_defense_difficulty( n ); + + + return string_format( "%s | %s: %s %s: %s %s: %s %s: %s %s: %s", + _( "Summary" ), + _( "Lifestyle" ), genetics, + _( "Knowledge" ), expertise, + _( "Offense" ), combat, + _( "Defense" ), defense, + _( "Social" ), socials ); +} diff --git a/src/player_difficulty.h b/src/player_difficulty.h new file mode 100644 index 0000000000000..b06f9ec4b5951 --- /dev/null +++ b/src/player_difficulty.h @@ -0,0 +1,63 @@ +#pragma once +#ifndef CATA_SRC_PLAYER_DIFFICULTY_H +#define CATA_SRC_PLAYER_DIFFICULTY_H + +#include + +// The point after which stats cost double +constexpr int HIGH_STAT = 12; + +enum class pool_type { + FREEFORM = 0, + ONE_POOL, + MULTI_POOL, + TRANSFER, +}; + +class player_difficulty +{ + private: + player_difficulty(); + ~player_difficulty() = default; + + // calculate individual properties + std::string get_defense_difficulty( const Character &u ) const; + std::string get_combat_difficulty( const Character &u ) const; + std::string get_genetics_difficulty( const Character &u ) const; + std::string get_expertise_difficulty( const Character &u ) const; + std::string get_social_difficulty( const Character &u ) const; + + // helpers for the above functions + static double calc_armor_value( const Character &u, bodypart_id bp ); + static double calc_dps_value( const Character &u ); + static int calc_social_value( const Character &u, const npc &compare ); + + // npc helpers + static void reset_npc( Character &dummy ); + static void npc_from_avatar( const avatar &u, npc &dummy ); + + // format the output + // percent band is the range to consider for values + // per is the actual percent as a decimal + // difficulty is true for things going from Very Easy to Very Hard + // difficutly is false for things going from Very Weak to Very Powerful + static std::string format_output( float percent_band, float per ); + + + npc average; + + public: + player_difficulty( const player_difficulty & ) = delete; + player_difficulty &operator= ( const player_difficulty & ) = delete; + + static player_difficulty &getInstance() { + static player_difficulty instance; + return instance; + } + + // call to get the details out + std::string difficulty_to_string( const avatar &u ) const; +}; + + +#endif // CATA_SRC_PLAYER_DIFFICULTY_H