From 2d6a48a9f4963b15d0ebd8fdf648c48183391191 Mon Sep 17 00:00:00 2001 From: Mark Langsdorf Date: Tue, 14 Apr 2020 10:31:19 -0500 Subject: [PATCH] item: improve the effective dps display Don't calculate dps for monsters that aren't going to be displayed. Adjust the unit tests based on relative values (epsilon) instead of fixed values (margin). --- src/item.cpp | 137 ++++++++++++++++++++++------------- src/item.h | 12 ++- tests/effective_dps_test.cpp | 5 +- tests/iteminfo_test.cpp | 33 ++++++--- 4 files changed, 123 insertions(+), 64 deletions(-) diff --git a/src/item.cpp b/src/item.cpp index 65639e8495d4f..8b87a709995e6 100644 --- a/src/item.cpp +++ b/src/item.cpp @@ -1265,20 +1265,20 @@ static void insert_separation_line( std::vector &info ) } /* - * 0 based lookup table of accuracy - monster defense converted into number of hits per 1000 + * 0 based lookup table of accuracy - monster defense converted into number of hits per 10000 * attacks * data painstakingly looked up at http://onlinestatbook.com/2/calculators/normal_dist.html */ static const double hits_by_accuracy[41] = { - 0.0, 0.1, 0.2, 0.3, 0.7, // -20 to -16 - 1.3, 2.6, 4.7, 8.2, 13.9, // -15 to -11 - 22.8, 35.9, 54.8, 80.8, 115.1, // -10 to -6 - 158.7, 211.9, 274.3, 344.6, 420.7, // -5 to -1 - 500, // 0 - 579.3, 655.4, 725.7, 788.1, 841.3, // 1 to 5 - 884.9, 919.2, 945.2, 964.1, 977.2, // 6 to 10 - 986.1, 991.8, 995.3, 997.4, 998.7, // 11 to 15 - 999.3, 999.7, 999.8, 999.9, 1000.0 // 16 to 20 + 0, 1, 2, 3, 7, // -20 to -16 + 13, 26, 47, 82, 139, // -15 to -11 + 228, 359, 548, 808, 1151, // -10 to -6 + 1587, 2119, 2743, 3446, 4207, // -5 to -1 + 5000, // 0 + 5793, 6554, 7257, 7881, 8413, // 1 to 5 + 8849, 9192, 9452, 9641, 9772, // 6 to 10 + 9861, 9918, 9953, 9974, 9987, // 11 to 15 + 9993, 9997, 9998, 9999, 10000 // 16 to 20 }; double item::effective_dps( const player &guy, monster &mon ) const @@ -1287,62 +1287,87 @@ double item::effective_dps( const player &guy, monster &mon ) const float base_hit = guy.get_dex() / 4.0f + guy.get_hit_weapon( *this ); base_hit *= std::max( 0.25f, 1.0f - guy.encumb( bp_torso ) / 100.0f ); float mon_defense = mon_dodge + mon.size_melee_penalty() / 5.0; - constexpr double hit_trials = 1000.0; - // a hit occurs when a normal distribution with a mean value of base_hit * 5 and a - // standard deviation of 25, minus 5 * monster defense, is greater than 0 - // to shorten the lookup table, divide everything by 5 and simplify - // hit occurs when normal( acc - monster_defense, 5 ) > 0 + constexpr double hit_trials = 10000.0; const int rng_mean = std::max( std::min( static_cast( base_hit - mon_defense ), 20 ), -20 ) + 20; - double num_hits = hits_by_accuracy[ rng_mean ]; + double num_all_hits = hits_by_accuracy[ rng_mean ]; + /* critical hits have two chances to occur: triple critical hits happen much less frequently, + * and double critical hits can only occur if a hit roll is more than 1.5 * monster dodge. + * Not the hit roll used to determine the attack, another one. + * the way the math works, some percentage of the total hits are eligible to be double + * critical hits, and the rest are eligible to be triple critical hits, but in each case, + * only some small percent of them actually become critical hits. + */ + const int rng_high_mean = std::max( std::min( static_cast( base_hit - 1.5 * mon_dodge ), + 20 ), -20 ) + 20; + double num_high_hits = hits_by_accuracy[ rng_high_mean ] * num_all_hits / hit_trials; + double double_crit_chance = guy.crit_chance( 4, 0, *this ); + double crit_chance = guy.crit_chance( 0, 0, *this ); + double num_low_hits = std::max( 0.0, num_all_hits - num_high_hits ); + double moves_per_attack = guy.attack_speed( *this ); // attacks that miss do no damage but take time - double total_moves = ( hit_trials - num_hits ) * moves_per_attack; + double total_moves = ( hit_trials - num_all_hits ) * moves_per_attack; double total_damage = 0.0; - double num_crits = std::min( hit_trials * guy.crit_chance( base_hit * 5.0, - mon_dodge * 5.0, *this ), num_hits ); + double num_crits = std::min( num_low_hits * crit_chance + num_high_hits * double_crit_chance, + num_all_hits ); // critical hits are counted separately - num_hits -= num_crits; - + double num_hits = num_all_hits - num_crits; // sum average damage past armor and return the number of moves required to achieve // that damage - const auto calc_effective_damage = [ &, moves_per_attack]( int num_hits, bool crit, - const player & guy, monster & mon ) { + const auto calc_effective_damage = [ &, moves_per_attack]( const double num_strikes, + const bool crit, const player & guy, monster & mon ) { + monster temp_mon = mon; + double subtotal_damage = 0; damage_instance base_damage; guy.roll_all_damage( crit, base_damage, true, *this ); damage_instance dealt_damage = base_damage; - mon.absorb_hit( bp_torso, dealt_damage ); - double damage_per_hit = 0; + temp_mon.absorb_hit( bp_torso, dealt_damage ); + dealt_damage_instance dealt_dams; for( const damage_unit &dmg_unit : dealt_damage.damage_units ) { - damage_per_hit += dmg_unit.amount + dmg_unit.damage_multiplier; + int cur_damage = 0; + int total_pain = 0; + temp_mon.deal_damage_handle_type( dmg_unit, bp_torso, cur_damage, total_pain ); + if( cur_damage > 0 ) { + dealt_dams.dealt_dams[ dmg_unit.type ] += cur_damage; + } } - double subtotal_damage = damage_per_hit * num_hits; - double subtotal_moves = moves_per_attack * num_hits; + double damage_per_hit = dealt_dams.total_damage(); + subtotal_damage = damage_per_hit * num_strikes; + double subtotal_moves = moves_per_attack * num_strikes; if( has_technique( rapid_strike ) ) { - damage_instance dealt_rs_damage = base_damage; + monster temp_rs_mon = mon; + damage_instance rs_base_damage; + guy.roll_all_damage( crit, rs_base_damage, true, *this ); + damage_instance dealt_rs_damage = rs_base_damage; for( damage_unit &dmg_unit : dealt_rs_damage.damage_units ) { dmg_unit.damage_multiplier *= 0.66; } - mon.absorb_hit( bp_torso, dealt_rs_damage ); - double rs_damage_per_hit = 0; + temp_rs_mon.absorb_hit( bp_torso, dealt_rs_damage ); + dealt_damage_instance rs_dealt_dams; for( const damage_unit &dmg_unit : dealt_rs_damage.damage_units ) { - rs_damage_per_hit += dmg_unit.amount + dmg_unit.damage_multiplier; + int cur_damage = 0; + int total_pain = 0; + temp_rs_mon.deal_damage_handle_type( dmg_unit, bp_torso, cur_damage, total_pain ); + if( cur_damage > 0 ) { + rs_dealt_dams.dealt_dams[ dmg_unit.type ] += cur_damage; + } } - // assume half of hits turn into rapid strikes + double rs_damage_per_hit = rs_dealt_dams.total_damage(); subtotal_moves *= 0.5; subtotal_damage *= 0.5; - subtotal_moves += moves_per_attack * num_hits * 0.33; - subtotal_damage += rs_damage_per_hit * num_hits * 0.5; + subtotal_moves += moves_per_attack * num_strikes * 0.33; + subtotal_damage += rs_damage_per_hit * num_strikes * 0.5; } return std::make_pair( subtotal_moves, subtotal_damage ); }; + std::pair crit_summary = calc_effective_damage( num_crits, true, guy, mon ); + total_moves += crit_summary.first; + total_damage += crit_summary.second; std::pair summary = calc_effective_damage( num_hits, false, guy, mon ); total_moves += summary.first; total_damage += summary.second; - summary = calc_effective_damage( num_crits, true, guy, mon ); - total_moves += summary.first; - total_damage += summary.second; return total_damage * to_moves( 1_seconds ) / total_moves; } @@ -1359,19 +1384,34 @@ static const std::vector> dps_comp_monster { to_translation( "Vs. Mixed" ), { mtype_id( "mon_zombie_survivor" ), false, true } }, }; -std::map item::dps( const player &guy ) const +std::map item::dps( const bool for_display, const bool for_calc, + const player &guy ) const { std::map results; for( const std::pair &comp_mon : dps_comp_monsters ) { + if( ( comp_mon.second.display != for_display ) && + ( comp_mon.second.evaluate != for_calc ) ) { + continue; + } monster test_mon = monster( comp_mon.second.mon_id ); results[ comp_mon.first.translated() ] = effective_dps( guy, test_mon ); } return results; } -std::map item::dps() const +std::map item::dps( const bool for_display, const bool for_calc ) const +{ + return dps( for_display, for_calc, g->u ); +} + +double item::average_dps( const player &guy ) const { - return dps( g->u ); + double dmg_count = 0.0; + const std::map &dps_data = dps( false, true, guy ); + for( const std::pair &dps_entry : dps_data ) { + dmg_count += dps_entry.second; + } + return dmg_count / dps_data.size(); } void item::basic_info( std::vector &info, const iteminfo_query *parts, int batch, @@ -3167,16 +3207,15 @@ void item::combat_info( std::vector &info, const iteminfo_query *parts info.push_back( iteminfo( "BASE", _( "Moves per attack: " ), "", iteminfo::lower_is_better, attack_time() ) ); info.emplace_back( "BASE", _( "Typical damage per second:" ), "" ); - const std::map &dps_data = dps(); - for( const std::pair &ref_data : dps_comp_monsters ) { - const auto dps_entry = dps_data.find( ref_data.first.translated() ); - if( ( dps_entry == dps_data.end() ) || !ref_data.second.display ) { - continue; - } - info.emplace_back( "BASE", space + dps_entry->first + ": ", "", + const std::map &dps_data = dps( true, false ); + std::string sep; + for( const std::pair &dps_entry : dps_data ) { + info.emplace_back( "BASE", sep + dps_entry.first + ": ", "", iteminfo::no_newline | iteminfo::is_decimal, - dps_entry->second ); + dps_entry.second ); + sep = space; } + info.emplace_back( "BASE", "" ); } } diff --git a/src/item.h b/src/item.h index f11d800e1e04c..a460df3ca6a20 100644 --- a/src/item.h +++ b/src/item.h @@ -568,9 +568,15 @@ class item : public visitable /** * calculate effective dps against a stock set of monsters. by default, assume g->u * is wielding - */ - std::map dps( const player &guy ) const; - std::map dps() const; + * for_display - include monsters intended for display purposes + * for_calc - include monsters intended for evaluation purposes + * for_display and for_calc are inclusive + */ + std::map dps( bool for_display, bool for_calc, const player &guy ) const; + std::map dps( bool for_display, bool for_calc ) const; + /** return the average dps of the weapon against evaluation monsters */ + double average_dps( const player &guy ) const; + /** * Whether the character needs both hands to wield this item. */ diff --git a/tests/effective_dps_test.cpp b/tests/effective_dps_test.cpp index d40ec79a84fab..9d05096d406ef 100644 --- a/tests/effective_dps_test.cpp +++ b/tests/effective_dps_test.cpp @@ -134,9 +134,9 @@ TEST_CASE( "effective damage per second", "[effective][dps]" ) CHECK( good_sword.effective_dps( dummy, mummy ) == Approx( 33.0f ).epsilon( 0.15f ) ); } - SECTION( "STR 8, DEX 8" ) { + SECTION( "STR 8, DEX 10" ) { dummy.str_max = 8; - dummy.dex_max = 8; + dummy.dex_max = 10; CHECK( clumsy_sword.effective_dps( dummy, mummy ) == Approx( 25.0f ).epsilon( 0.15f ) ); CHECK( normal_sword.effective_dps( dummy, mummy ) == Approx( 32.0f ).epsilon( 0.15f ) ); @@ -159,7 +159,6 @@ TEST_CASE( "effective vs actual damage per second", "[actual][dps][!mayfail]" ) avatar &dummy = g->u; clear_character( dummy ); - monster mummy( mtype_id( "debug_mon" ) ); monster soldier( mtype_id( "mon_zombie_soldier" ) ); monster smoker( mtype_id( "mon_zombie_smoker" ) ); monster survivor( mtype_id( "mon_zombie_survivor" ) ); diff --git a/tests/iteminfo_test.cpp b/tests/iteminfo_test.cpp index 09fd28c8a9612..e6aa5144ce8d4 100644 --- a/tests/iteminfo_test.cpp +++ b/tests/iteminfo_test.cpp @@ -156,9 +156,9 @@ TEST_CASE( "weapon attack ratings and moves", "[item][iteminfo][weapon]" ) " To-hit bonus: -2\n" "Moves per attack: 79\n" "Typical damage per second:\n" - " Best: 8.84" - " Vs. Agile: 4.62" - " Vs. Armored: 1.35" ); + "Best: 4.92" + " Vs. Agile: 2.05" + " Vs. Armored: 0.15\n" ); } SECTION( "bash and cut damage" ) { @@ -170,9 +170,9 @@ TEST_CASE( "weapon attack ratings and moves", "[item][iteminfo][weapon]" ) " To-hit bonus: +2\n" "Moves per attack: 145\n" "Typical damage per second:\n" - " Best: 11.64" - " Vs. Agile: 6.08" - " Vs. Armored: 4.45" ); + "Best: 9.38" + " Vs. Agile: 5.74" + " Vs. Armored: 2.84\n" ); } SECTION( "bash and pierce damage" ) { @@ -184,9 +184,24 @@ TEST_CASE( "weapon attack ratings and moves", "[item][iteminfo][weapon]" ) " To-hit bonus: +1\n" "Moves per attack: 100\n" "Typical damage per second:\n" - " Best: 11.33" - " Vs. Agile: 5.93" - " Vs. Armored: 1.24" ); + "Best: 8.01" + " Vs. Agile: 4.54" + " Vs. Armored: 0.12\n" ); + } + + + SECTION( "melee and ranged damaged" ) { + test_info_equals( + item( "arrow_wood" ), q, + "--\n" + "Melee damage: Bash: 2" + " Cut: 1" + " To-hit bonus: +0\n" + "Moves per attack: 65\n" + "Typical damage per second:\n" + "Best: 4.90" + " Vs. Agile: 2.46" + " Vs. Armored: 0.00\n" ); } SECTION( "no damage" ) {