From 6c1a944a50959e422040d9a92ef92b14d50ed3e7 Mon Sep 17 00:00:00 2001 From: Dave Hendler Date: Wed, 28 Oct 2026 14:42:54 -0700 Subject: [PATCH] Implement ammo option and damage to ranged weapons --- engine/action/action.cpp | 17 ++++++++++------- engine/item/item.cpp | 4 ++++ engine/player/player.cpp | 21 +++++++++++++++++++-- engine/player/weapon.hpp | 5 ++++- engine/sc_enums.hpp | 4 +++- engine/util/util.cpp | 3 +++ 6 files changed, 43 insertions(+), 11 deletions(-) diff --git a/engine/action/action.cpp b/engine/action/action.cpp index 14e668af27..963d064b16 100644 --- a/engine/action/action.cpp +++ b/engine/action/action.cpp @@ -1409,21 +1409,24 @@ double action_t::calculate_weapon_damage( double attack_power ) const if ( !weapon || weapon_multiplier <= 0 ) return 0; - // TODO Forever: Do we need special handling for ranged weapons here? - // Currently the weapon roll is multiplied by melee AP so probably need ranged handling + // TODO Forever: Verify melee and ranged attack power are handled separately + // TODO Forever: Wands are probably a 3rd branch here that we'll need to deal with + // TODO Forever: Verify that ammo is not affected by RAP multipliers (Classic ammo was not affected) + // TODO Forever: Verify that ammo is/isn't affected by damage % talents (Focused Fire, Ranged Weapon Specialization) // The weapon damage roll (and its bonus damage) is affected by attak power modifiers just like regular attack power - double capm = player -> composite_attack_power_multiplier(); + double capm = weapon->slot != SLOT_RANGED ? player -> composite_attack_power_multiplier() : player -> composite_ranged_attack_power_multiplier(); double dmg = capm * ( sim->averaged_range( weapon->min_dmg, weapon->max_dmg ) + weapon->bonus_dmg ); + double ammo = sim->averaged_range( weapon->ammo_min_dmg, weapon->ammo_max_dmg ); timespan_t weapon_speed = normalize_weapon_speed ? weapon->get_normalized_speed() : weapon->swing_time; double power_damage = weapon_speed.total_seconds() * weapon_power_mod * attack_power; - double total_dmg = dmg + power_damage; + double total_dmg = dmg + power_damage + ammo; sim->print_debug( - "{} weapon damage for {}: base=({:.7g} to {:.7g}) total={:.6f} weapon_damage={:.7g} bonus_damage={:.7g} " + "{} weapon damage for {}: base=({:.7g} to {:.7g}) ammo=({:.7g} to {:.7g}) total={:.6f} weapon_damage={:.7g} bonus_damage={:.7g} " "multiplier={:.7g} speed={} power_damage={:.7g} ap={:.7g}", - *player, *this, weapon->min_dmg, weapon->max_dmg, total_dmg, dmg, weapon->bonus_dmg, capm, weapon_speed, - power_damage, attack_power ); + *player, *this, weapon->min_dmg, weapon->max_dmg, weapon->ammo_min_dmg, weapon->ammo_max_dmg, total_dmg, + dmg, weapon->bonus_dmg, capm, weapon_speed, power_damage, attack_power ); return total_dmg; } diff --git a/engine/item/item.cpp b/engine/item/item.cpp index b948c4b0ef..38d9a95884 100644 --- a/engine/item/item.cpp +++ b/engine/item/item.cpp @@ -698,6 +698,8 @@ inventory_type item_t::dbc_inventory_type() const } case SLOT_RANGED: return INVTYPE_RANGED; + case SLOT_AMMO: + return INVTYPE_AMMO; case SLOT_TABARD: return INVTYPE_TABARD; default: return INVTYPE_NON_EQUIP; @@ -736,6 +738,8 @@ item_class item_t::dbc_item_class() const case SLOT_OFF_HAND: case SLOT_RANGED: return ITEM_CLASS_WEAPON; + case SLOT_AMMO: + return ITEM_CLASS_PROJECTILE; // No good default value, so just use Miscellaneous default: return ITEM_CLASS_MISC; diff --git a/engine/player/player.cpp b/engine/player/player.cpp index 4fe24ee2a2..3a7f3b2640 100644 --- a/engine/player/player.cpp +++ b/engine/player/player.cpp @@ -2124,6 +2124,21 @@ void player_t::init_weapon( weapon_t& w ) assert( w.type > WEAPON_NONE && w.type < WEAPON_RANGED ); if ( w.slot == SLOT_OFF_HAND ) assert( w.type > WEAPON_NONE && w.type < WEAPON_2H ); + // Store ammo min/max damage on the ranged weapon + // TODO Forever: This all needs to be verified. I was only able to test Light Shot in-game at level 11 + if ( w.slot == SLOT_RANGED && items[ SLOT_AMMO ].parsed.data.id ) + { + const item_t& ammo = items[ SLOT_AMMO ]; + double dps = dbc->item_damage_ammo( ammo.item_level() ).value( ammo.parsed.data.quality ); + // TODO: Verify that ammo damage is speed normalized + double avg = dps * w.swing_time.total_seconds() / w.get_normalized_speed().total_seconds(); + double r = items[ SLOT_RANGED ].parsed.data.dmg_range; + // 60% range fallback if no weapon data is present. This seems to be the default in game data for these items + if ( r <= 0 ) + r = 0.6; + w.ammo_min_dmg = std::floor( avg * ( 1 - r / 2 ) ); + w.ammo_max_dmg = std::floor( avg * ( 1 + r / 2 ) + 0.5 ); + } } void player_t::create_special_effects() @@ -10485,7 +10500,8 @@ double player_t::avg_item_level() const int num_ilvl_items = 0; for ( const auto& item : items ) { - if ( item.slot != SLOT_SHIRT && item.slot != SLOT_TABARD && item.slot != SLOT_RANGED && item.active() ) + // TODO Forever: Removed SLOT_RANGED and added SLOT_AMMO - verify how SLOT_RANGED affects avg item level in-game + if ( item.slot != SLOT_SHIRT && item.slot != SLOT_TABARD && item.slot != SLOT_AMMO && item.active() ) { avg_ilvl += item.item_level(); num_ilvl_items++; @@ -12162,7 +12178,7 @@ std::string player_t::create_profile( save_e stype ) const slot_e SLOT_OUT_ORDER[] = { SLOT_HEAD, SLOT_NECK, SLOT_SHOULDERS, SLOT_BACK, SLOT_CHEST, SLOT_SHIRT, SLOT_TABARD, SLOT_WRISTS, SLOT_HANDS, SLOT_WAIST, SLOT_LEGS, SLOT_FEET, SLOT_FINGER_1, SLOT_FINGER_2, - SLOT_TRINKET_1, SLOT_TRINKET_2, SLOT_MAIN_HAND, SLOT_OFF_HAND, SLOT_RANGED, + SLOT_TRINKET_1, SLOT_TRINKET_2, SLOT_MAIN_HAND, SLOT_OFF_HAND, SLOT_RANGED, SLOT_AMMO, }; for ( auto& slot : SLOT_OUT_ORDER ) @@ -12436,6 +12452,7 @@ void player_t::create_options() add_option( opt_string( "main_hand", items[ SLOT_MAIN_HAND ].options_str ) ); add_option( opt_string( "off_hand", items[ SLOT_OFF_HAND ].options_str ) ); add_option( opt_string( "ranged", items[ SLOT_RANGED ].options_str ) ); + add_option( opt_string( "ammo", items[ SLOT_AMMO ].options_str ) ); add_option( opt_string( "tabard", items[ SLOT_TABARD ].options_str ) ); // Set Bonus diff --git a/engine/player/weapon.hpp b/engine/player/weapon.hpp index 448005ca46..0f77f230ba 100644 --- a/engine/player/weapon.hpp +++ b/engine/player/weapon.hpp @@ -15,6 +15,7 @@ struct weapon_t school_e school; double damage, dps; double min_dmg, max_dmg; + double ammo_min_dmg, ammo_max_dmg; timespan_t swing_time; slot_e slot; int buff_type; @@ -31,6 +32,8 @@ struct weapon_t dps( d / st.total_seconds() ), min_dmg( d ), max_dmg( d ), + ammo_min_dmg( 0 ), + ammo_max_dmg( 0 ), swing_time( st ), slot( SLOT_INVALID ), buff_type( 0 ), @@ -85,4 +88,4 @@ struct weapon_t return proc_chance; } -}; \ No newline at end of file +}; diff --git a/engine/sc_enums.hpp b/engine/sc_enums.hpp index 1f5f620eb8..624fb05b91 100644 --- a/engine/sc_enums.hpp +++ b/engine/sc_enums.hpp @@ -780,7 +780,9 @@ enum slot_e // these enum values match armory settings SLOT_OFF_HAND = 16, SLOT_RANGED = 17, SLOT_TABARD = 18, - SLOT_MAX = 19, + // TODO Forever: No Armory for Forever yet so don't know if/how they will represent ammo slot + SLOT_AMMO = 19, + SLOT_MAX = 20, SLOT_MIN = 0 }; diff --git a/engine/util/util.cpp b/engine/util/util.cpp index 8ba00c4cb4..d600dee6fd 100644 --- a/engine/util/util.cpp +++ b/engine/util/util.cpp @@ -1125,6 +1125,7 @@ const char* util::slot_type_string( slot_e slot ) case SLOT_OFF_HAND: return "off_hand"; case SLOT_RANGED: return "ranged"; case SLOT_TABARD: return "tabard"; + case SLOT_AMMO: return "ammo"; default: return "unknown"; } } @@ -2360,6 +2361,8 @@ slot_e util::translate_invtype( inventory_type inv_type ) return SLOT_WRISTS; case INVTYPE_BODY: return SLOT_SHIRT; + case INVTYPE_AMMO: + return SLOT_AMMO; default: return SLOT_INVALID; }