Skip to content

Commit

Permalink
item: improve the calculation of DPS
Browse files Browse the repository at this point in the history
Calculate and display typical damage per second values based on the
avatar's actual stats and encumbrance, factoring in misses, critical
hits, and the effects of armor.  Display typical DPS in the best
case scenario (against an debug monster with no armor or dodge),
against agile targets (a smoker zombie with no armor but Dodge 7),
and against armored targets (a soldier zombie with bash resistance
20 and cut resistance 25).

Update the help file to reflect these changes.
  • Loading branch information
mlangsdorf committed Apr 8, 2020
1 parent be7ab20 commit 7e6c874
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 13 deletions.
2 changes: 1 addition & 1 deletion data/help/texts.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@
"messages": [
"There is a wide variety of items available for your use. You may find them lying on the ground; if so, simply press <press_pickup> to pick up items on the same square. Some items are found inside a container, drawn as a { with a blue background. Pressing <press_examine>, then a direction key, will allow you to examine these containers and loot their contents.",
"Pressing <press_compare> opens a comparison menu, where you can see two items side-by-side with their attributes highlighted to indicate which is superior. You can also access the item comparison menu by pressing C after <press_listitems> to view nearby items menu is open and an item is selected.",
"All items may be used as a melee weapon, though some are better than others. You can check the melee attributes of an item you're carrying by hitting <press_inventory> to enter your inventory, then pressing the letter of the item. There are 3 melee values: bashing, cutting, and to-hit bonus (or penalty). Bashing damage is universally effective, but is capped by low strength. Cutting damage is a guaranteed increase in damage, but it may be reduced by a monster's natural armor.",
"Almost all items may be used as a melee weapon, though some are better than others. You can check the melee attributes of an item you're carrying by hitting <press_inventory> to enter your inventory, then pressing the letter of the item. There are 5 melee values: to-hit bonus (or penalty), moves per attack, and bash, cut, and pierce damage. The to-hit bonus increases the chance of an attack connecting with a monster and the chance of a successful attack becoming a critical hit for more damage. Moves per attack are how many moves it takes to attack with the weapon, and 100 moves passing every second. Bash damage can stun a monster, preventing it from counter-attacking, but is capped by low strength. Cut damage is usually an increase in damage over bash damage, but many monsters have natural armor against it. Pierce damage usually penetrates armor better than cut damage, but does less damage overall, especially if you do not have a lot of skill in piercing weapons. it may be reduced by a monster's natural armor. The typical damage per second values are for your survivor and account for moves per attack, encumbrance, missed strikes, weapon skill, critical hits, and target armor. The 'Best' value is against an unarmored target with no Dodge skill. The 'Vs. Agile' value is against an unarmored target with a high Dodge skill. The 'Vs. Armored' value is against a target with more than 15 Bash and 20 Cut resistance but no Dodge skill. These are typical values to let you assess the effectiveness of weapons, and your actual damage in play will vary depending on the situation.",
"To wield an item as a weapon, press <press_wield> then the proper letter. Wielding the item you are currently wielding will unwield it, leaving your hands empty. A wielded weapon will not contribute to your volume carried, so holding a large item in your hands may be a good option for travel. When unwielding your weapon, it will go back in your inventory, or may be dropped on the ground if there is no space.",
"To wear a piece of clothing, press <press_wear> then the proper letter. Armor reduces damage and helps you resist things like smoke. To take off an item, press <press_take_off> then the proper letter. Clothing and armor are worn on layers, and provide different coverage, protection and warmth. Each piece has its own encumbrance, and wearing too much on each layer can significantly hamper your movement and other abilities, especially during combat. You can view and sort worn items by pressing <press_sort_armor>.",
"Your clothing can sit in one of five layers on your body: next-to-skin, standard, waist, over, and belted. You can wear one item from each layer on a body part without incurring an encumbrance penalty for too many worn items. Any items beyond the first on each layer add the encumbrance of the additional article(s) of clothing to the body part's encumbrance. The layering penalty applies a minimum of 2 and a maximum of 10 encumbrance per article of clothing.",
Expand Down
8 changes: 4 additions & 4 deletions src/creature.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -453,9 +453,9 @@ Creature *Creature::auto_find_hostile_target( int range, int &boo_hoo, int area
* Damage-related functions
*/

static int size_melee_penalty( m_size target_size )
int Creature::size_melee_penalty() const
{
switch( target_size ) {
switch( get_size() ) {
case MS_TINY:
return 30;
case MS_SMALL:
Expand All @@ -468,13 +468,13 @@ static int size_melee_penalty( m_size target_size )
return -20;
}

debugmsg( "Invalid target size %d", target_size );
debugmsg( "Invalid target size %d", get_size() );
return 0;
}

int Creature::deal_melee_attack( Creature *source, int hitroll )
{
int hit_spread = hitroll - dodge_roll() - size_melee_penalty( get_size() );
int hit_spread = hitroll - dodge_roll() - size_melee_penalty();

// If attacker missed call targets on_dodge event
if( hit_spread <= 0 && source != nullptr && !source->is_hallucination() ) {
Expand Down
1 change: 1 addition & 0 deletions src/creature.h
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ class Creature
void knock_back_from( const tripoint &p );
virtual void knock_back_to( const tripoint &to ) = 0;

int size_melee_penalty() const;
// begins a melee attack against the creature
// returns hit - dodge (>=0 = hit, <0 = miss)
virtual int deal_melee_attack( Creature *source, int hitroll );
Expand Down
113 changes: 105 additions & 8 deletions src/item.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,8 @@ static const std::string flag_WATER_EXTINGUISH( "WATER_EXTINGUISH" );
static const std::string flag_WET( "WET" );
static const std::string flag_WIND_EXTINGUISH( "WIND_EXTINGUISH" );

static const matec_id rapid_strike( "RAPID" );

class npc_class;

using npc_class_id = string_id<npc_class>;
Expand Down Expand Up @@ -1260,6 +1262,98 @@ static void insert_separation_line( std::vector<iteminfo> &info )
}
}

double item::effective_dps( const player &guy, monster &mon ) const
{
const float mon_dodge = mon.get_dodge() * 5.0f;
float base_hit = guy.get_hit_base() + 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();
double num_hits = 0;
constexpr double hit_trials = 1000.0;
const float accuracy = base_hit * 0.5f;
// monte carlo determination of the number of hits because calculating odds against normal
// rolls is hard
for( int i = 0; i < hit_trials; i++ ) {
num_hits += ( normal_roll( accuracy, 25.0f ) > mon_defense ? 1.0 : 0.0 );
}
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_damage = 0.0;
double num_crits = hit_trials * guy.crit_chance( base_hit, mon_dodge, *this );
// critical hits are counted separately
num_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 ) {
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;
for( const damage_unit &dmg_unit : dealt_damage.damage_units ) {
damage_per_hit += dmg_unit.amount + dmg_unit.damage_multiplier;
}
double subtotal_damage = damage_per_hit * num_hits;
double subtotal_moves = moves_per_attack * num_hits;

if( has_technique( rapid_strike ) ) {
damage_instance dealt_rs_damage = 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;
for( const damage_unit &dmg_unit : dealt_rs_damage.damage_units ) {
rs_damage_per_hit += dmg_unit.amount + dmg_unit.damage_multiplier;
}
// assume half of hits turn into rapid strikes
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;
}
return std::make_pair( subtotal_moves, subtotal_damage );
};
std::pair<double, double> 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<double>( 1_seconds ) / total_moves;
}

struct dps_comp_data {
mtype_id mon_id;
bool display;
bool evaluate;
};

static const std::map<std::string, dps_comp_data> dps_comp_monsters = {
{ _( "Vs. Armored" ), { mtype_id( "mon_zombie_soldier" ), true, true } },
{ _( "Best" ), { mtype_id( "debug_mon" ), true, false } },
{ _( "Vs. Mixed" ), { mtype_id( "mon_zombie_survivor" ), false, true } },
{ _( "Vs. Agile" ), { mtype_id( "mon_zombie_smoker" ), true, true } }
};

std::map<std::string, double> item::dps( const player &guy ) const
{
std::map<std::string, double> results;
for( const std::pair<std::string, dps_comp_data> &comp_mon : dps_comp_monsters ) {
monster test_mon = monster( comp_mon.second.mon_id );
results[ comp_mon.first ] = effective_dps( guy, test_mon );
}
return results;
}

std::map<std::string, double> item::dps() const
{
return dps( g->u );
}

void item::basic_info( std::vector<iteminfo> &info, const iteminfo_query *parts, int batch,
bool debug /* debug */ ) const
{
Expand Down Expand Up @@ -3043,14 +3137,17 @@ void item::combat_info( std::vector<iteminfo> &info, const iteminfo_query *parts
if( parts->test( iteminfo_parts::BASE_MOVES ) ) {
info.push_back( iteminfo( "BASE", _( "Moves per attack: " ), "",
iteminfo::lower_is_better, attack_time() ) );
double dps = ( dmg_bash + dmg_cut + dmg_stab ) * to_moves<int>( 1_seconds ) /
static_cast<double>( attack_time() );
static const matec_id rapid_strike( "RAPID" );
if( has_technique( rapid_strike ) ) {
dps *= 100.0 / 66;
}
info.push_back( iteminfo( "BASE", _( "Damage per second: " ), "",
iteminfo::is_decimal, dps ) );
info.emplace_back( "BASE", _( "Typical damage per second: " ), "" );
const std::map<std::string, double> &dps_data = dps();
for( const std::pair<std::string, double> &dps_entry : dps_data ) {
const auto &ref_data = dps_comp_monsters.find( dps_entry.first );
if( ( ref_data == dps_comp_monsters.end() ) || !ref_data->second.display ) {
continue;
}
info.emplace_back( "BASE", space + dps_entry.first + ": ", "",
iteminfo::no_newline | iteminfo::is_decimal,
dps_entry.second );
}
}
}

Expand Down
11 changes: 11 additions & 0 deletions src/item.h
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,17 @@ class item : public visitable<item>
/** All damage types this item deals when thrown (no skill modifiers etc. applied). */
damage_instance base_damage_thrown() const;

/**
* Calculate the item's effective damage per second past armor when wielded by a
* character against a monster.
*/
double effective_dps( const player &guy, monster &mon ) const;
/**
* calculate effective dps against a stock set of monsters. by default, assume g->u
* is wielding
*/
std::map<std::string, double> dps( const player &guy ) const;
std::map<std::string, double> dps() const;
/**
* Whether the character needs both hands to wield this item.
*/
Expand Down

0 comments on commit 7e6c874

Please sign in to comment.