From ea8b949349cacac219923776526376f3f7dcff14 Mon Sep 17 00:00:00 2001 From: Dave Hendler Date: Mon, 19 Oct 2026 20:39:48 -0700 Subject: [PATCH 1/7] [profiles] Add level 11 hunter --- profiles/CI.simc | 1 + profiles/LowLevel/hunter-11.simc | 109 +++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 profiles/LowLevel/hunter-11.simc diff --git a/profiles/CI.simc b/profiles/CI.simc index 53d4363d07..299d2a42fc 100644 --- a/profiles/CI.simc +++ b/profiles/CI.simc @@ -5,5 +5,6 @@ single_actor_batch=1 profiles/LowLevel/shaman-20.simc profiles/LowLevel/warrior-11.simc profiles/LowLevel/warlock-10.simc +profiles/LowLevel/hunter-11.simc iterations=100 diff --git a/profiles/LowLevel/hunter-11.simc b/profiles/LowLevel/hunter-11.simc new file mode 100644 index 0000000000..6fe355e65e --- /dev/null +++ b/profiles/LowLevel/hunter-11.simc @@ -0,0 +1,109 @@ +# Fallochli - 2026-10-19 16:10 - /Classic Beta PvE 2 +# SimC Addon 12.1.5-02 +# WoW 1.60.1.70450, TOC 16001 +# Requires SimulationCraft 1000-01 or newer + +hunter="Fallochli" +level=11 +race=dwarf +region= +server=classic_beta_pve_2 +role= +professions=skinning=56 +spec=unknown + +talents=C0cBZcHs6hiHBl7Csp3hvR+3RCAwDMAAAAAA + +# Primary Talents +# talents=C0cBZcHs6hiHBl7Csp3hvR+3RCAwDMAAAAAA + +legacy_talents=136864:5/137371:5/136855:5/136866:1 + +# Goat Fur Cloak (10) +back=,id=2905 +# Ragged Leather Vest (5) +chest=,id=1364 +# Rugged Trapper's Shirt (1) +shirt=,id=148 +# Frayed Bracers (5) +wrist=,id=3365 +# Driving Gloves (9) +hands=,id=3152 +# Worn Leather Belt (7) +waist=,id=1418 +# Dwarven Leather Pants (5) +legs=,id=61 +# Worn Leather Boots (8) +feet=,id=1419 +# Avala's Binding (8) +finger1=,id=286737 +# Sharp Axe (8) +main_hand=,id=1011 +# Light Shot +ammo=,id=2516 +# ammo_count=956 +# Old Blunderbuss (2) +ranged=,id=2508 + +bag1=,id=2102 +bag2=,id=277977 + +### Gear from Bags +# +# Patchwork Cloak (6) +# back=,id=1429 +# +# Patchwork Cloak (6) +# back=,id=1429 +# +# Bear Shawl (5) +# back=,id=6185 +# +# Patchwork Armor (7) +# chest=,id=1433 +# +# Patchwork Gloves (7) +# hands=,id=1430 +# +# Worn Leather Pants (7) +# legs=,id=1423 +# +# Priest's Mace of Stamina (12) +# main_hand=,id=2075,bonus_id=12731 +# +# Skinning Knife (4) +# main_hand=,id=7005 + +### Additional Character Info +# +# character_stats=health:246/power_type:0/mana:315/rage:0/focus:0/energy:0/strength:26/agility:33/stamina:31/intellect:24/spirit:30 +# character_stats_base=strength:26/agility:33/stamina:31/intellect:24/spirit:27 +# character_stats_bonus=strength:0/agility:0/stamina:0/intellect:0/spirit:3 +# character_stats_gear=damage_per_second:4.79/frost_resistance:1/armor:154/resistance4:1 +# attack_power=melee:61:0:0/ranged:68:20:0 +# spell_power=holy:0/fire:0/nature:0/frost:0/shadow:0/arcane:0 +# spell_stats=healing:0/penetration:0 +# regen=health:3.91:0/health_from_spirit:5.21:0/mana:7.50:0.00/mana_from_spirit:7.50:0/mana_power:7.50:0.00/rage_power:-12.50:0.00/focus_power:10.00:10.00/energy_power:10.00:10.00 +# crit=melee:4.10/ranged:4.42/spell:4.94 +# hit=melee_rating_bonus:0/melee_modifier:0/ranged_rating_bonus:0/ranged_modifier:0/spell_rating_bonus:0/spell_modifier:0 +# haste=melee:0/ranged:0/ranged_ammo:10.00/spell:0 +# combat_modifiers=expertise:0/expertise_offhand:0/expertise_ranged:0/armor_penetration:0/mastery:0 +# combat_ratings= +# defense=armor_base:60/armor_effective:280/armor:280/armor_bonus:220/defense_skill:48/defense_modifier:0/dodge:8.64/parry:4.72/block:0/block_value:1 +# resistances=holy:0/fire:0/nature:0/frost:1/shadow:0/arcane:0 +# weapon_damage=mainhand:17.50:22.55:2.60/pos_buff:0/neg_buff:0/percent:1.01/ranged:17.62:21.65:2.09/ranged_pos_buff:0/ranged_neg_buff:0/ranged_percent:1.01 +# pet_info=npc_id:165189/level:9/name:bear/family:boar/type:beast/talent_tree:default/loyalty:submissive/happiness:3/damage_percent:125/loyalty_rate:20/training_points:18:5 +# pet_stats=health:289/power_type:2/mana:0/rage:0/focus:100/energy:0/strength:0/agility:0/stamina:0/intellect:0/spirit:0 +# pet_stats_base=strength:0/agility:0/stamina:0/intellect:0/spirit:0 +# pet_stats_bonus=strength:0/agility:0/stamina:0/intellect:0/spirit:0 +# pet_attack_power=melee:29:8:0/ranged:0:0:0 +# pet_modifiers=spell_power:0/haste_melee:0/hit_melee_modifier:0/hit_spell_modifier:0 +# pet_defense=armor_base:533/armor_effective:600/armor:600/armor_bonus:67/defense_skill:45/defense_modifier:0 +# pet_resistances=holy:0/fire:0/nature:0/frost:0/shadow:0/arcane:0 +# pet_weapon_damage=mainhand:14.92:19.32:2/pos_buff:0/neg_buff:0/percent:1 +# +# skills=44:46:0:55/46:54:0:55/50:1:0:1/51:1:0:1/95:48:0:55/98:300:0:300/111:300:0:300/162:1:0:55/163:1:0:1/173:1:0:55/185:2:0:75/393:56:0:75/414:1:0:1/415:1:0:1/2939:2:0:75/2947:56:0:75 +# +# buffs=8091:0/8112:0/13165:0 + +# Checksum: d5c9b9ef From 566512d499076698d59453cd722fdc5b1f08a177 Mon Sep 17 00:00:00 2001 From: Dave Hendler Date: Mon, 19 Oct 2026 20:40:25 -0700 Subject: [PATCH 2/7] [hunter] Hunter basics and auto shot --- engine/class_modules/apl/hunter.cpp | 12 +++ engine/class_modules/apl/hunter.hpp | 1 + engine/class_modules/sc_hunter.cpp | 120 +++++++++++++++++++++++++++- 3 files changed, 132 insertions(+), 1 deletion(-) diff --git a/engine/class_modules/apl/hunter.cpp b/engine/class_modules/apl/hunter.cpp index 2ae7b78044..f87dd7d6d3 100644 --- a/engine/class_modules/apl/hunter.cpp +++ b/engine/class_modules/apl/hunter.cpp @@ -29,4 +29,16 @@ std::string temporary_enchant( const player_t* ) { return "disabled"; } + +//hunter_apl_start +void hunter( player_t* p ) +{ + action_priority_list_t* precombat = p->get_action_priority_list( "precombat" ); + action_priority_list_t* default_ = p->get_action_priority_list( "default" ); + + //precombat->add_action( "summon_pet" ); + + default_->add_action( "auto_shot" ); +} +//hunter_apl_end } // namespace hunter_apl diff --git a/engine/class_modules/apl/hunter.hpp b/engine/class_modules/apl/hunter.hpp index 51eb11141f..9e1aa4539b 100644 --- a/engine/class_modules/apl/hunter.hpp +++ b/engine/class_modules/apl/hunter.hpp @@ -11,4 +11,5 @@ std::string flask( const player_t* ); std::string food( const player_t* ); std::string rune( const player_t* ); std::string temporary_enchant( const player_t* ); +void hunter( player_t* ); } // namespace hunter_apl diff --git a/engine/class_modules/sc_hunter.cpp b/engine/class_modules/sc_hunter.cpp index 96a5c5631f..686b1655cf 100644 --- a/engine/class_modules/sc_hunter.cpp +++ b/engine/class_modules/sc_hunter.cpp @@ -4,15 +4,133 @@ // ========================================================================== #include "simulationcraft.hpp" +#include "action/parse_effects.hpp" +#include "class_modules/apl/hunter.hpp" namespace -{ // UNNAMED NAMESPACE +{ +// ========================================================================= +// Hunter +// ========================================================================= + struct hunter_t final : public player_t { hunter_t( sim_t* sim, util::string_view name, race_e r = RACE_NONE ) : player_t( sim, HUNTER, name, r ) {} + + resource_e primary_resource() const override { return RESOURCE_MANA; } + + void init_base_stats() override; + + action_t* create_action( util::string_view name, util::string_view options_str ) override; + void init_action_list() override; +}; + +template +struct hunter_action_t : public parse_action_effects_t +{ + private: + using ab = parse_action_effects_t; + + public: + using base_t = hunter_action_t; + + template + hunter_action_t( Args&&... args ) : ab( std::forward( args )... ) {} + + hunter_t* p() + { return debug_cast( this->player ); } + + const hunter_t* p() const + { return debug_cast( this->player ); } +}; + +using hunter_melee_t = hunter_action_t; +using hunter_ranged_t = hunter_action_t; +using hunter_spell_t = hunter_action_t; + +// ================================================================================ +// Hunter Ranged +// ================================================================================ + +struct hunter_ranged_attack_t : public hunter_ranged_t +{ + hunter_ranged_attack_t( util::string_view name, player_t* player ) + : hunter_ranged_t( name, player, spell_data_t::nil() ) + { + weapon = &player->equipped_weapons[ SLOT_RANGED ]; + weapon_multiplier = 1.0; + base_execute_time = weapon->swing_time; + school = SCHOOL_PHYSICAL; + trigger_gcd = 0_ms; + + background = true; + repeating = true; + may_glance = false; + may_crit = true; + may_dodge = false; + may_parry = false; + special = false; + } +}; + +struct hunter_auto_shot_t : public hunter_ranged_t +{ + hunter_auto_shot_t( hunter_t* p, util::string_view options_str ) + : hunter_ranged_t( "auto_shot", p, spell_data_t::nil() ) + { + parse_options( options_str ); + + trigger_gcd = 0_ms; + ignore_false_positive = true; + + assert( p->equipped_weapons[ SLOT_RANGED ].type != WEAPON_NONE ); + + p->ranged_attack = new hunter_ranged_attack_t( "auto_shot", p ); + } + + void execute() override + { + player->ranged_attack->schedule_execute(); + } + + bool ready() override + { + if ( player->is_moving() ) + { + return false; + } + + return player->ranged_attack->execute_event == nullptr; + } }; +void hunter_t::init_base_stats() +{ + player_t::init_base_stats(); +} + +action_t* hunter_t::create_action( util::string_view name, util::string_view options_str ) +{ + if ( name == "auto_shot" ) return new hunter_auto_shot_t( this, options_str ); + + return player_t::create_action( name, options_str ); +} + +void hunter_t::init_action_list() +{ + if ( !action_list_str.empty() ) + { + player_t::init_action_list(); + return; + } + + clear_action_priority_lists(); + hunter_apl::hunter( this ); + use_default_action_list = true; + player_t::init_action_list(); +} + // HUNTER MODULE INTERFACE ================================================== struct hunter_module_t: public module_t From 6d8e40806826554bf7454560719330e5c68b9d24 Mon Sep 17 00:00:00 2001 From: Dave Hendler Date: Mon, 19 Oct 2026 20:42:12 -0700 Subject: [PATCH 3/7] ranged= option and untangling ranged from mainhand --- engine/action/attack.hpp | 5 ++++- engine/item/item.cpp | 16 ++++++++++++++-- engine/player/player.cpp | 10 +++++++--- engine/player/player.hpp | 1 + engine/util/util.cpp | 8 +++++--- 5 files changed, 31 insertions(+), 9 deletions(-) diff --git a/engine/action/attack.hpp b/engine/action/attack.hpp index 19758579ef..b7aad592ce 100644 --- a/engine/action/attack.hpp +++ b/engine/action/attack.hpp @@ -98,6 +98,8 @@ struct ranged_attack_t : public attack_t // Ranged Attack Overrides double composite_target_multiplier( player_t* ) const override; void schedule_execute( action_state_t* execute_state = nullptr ) override; + // TODO: Think this will be needed to separate melee/ranged haste + //double execute_time_pct_multiplier() const override; proc_types proc_type() const override; }; @@ -136,7 +138,8 @@ struct auto_attack_t : public action_t parse_options( options_str ); for ( auto& [ slot, weapon ] : p->equipped_weapons ) - create_melee_attack( weapon ); + if ( slot != SLOT_RANGED ) + create_melee_attack( weapon ); } // TODO (FOREVER): handle different weapon types range behaviours diff --git a/engine/item/item.cpp b/engine/item/item.cpp index 4400be16b5..b948c4b0ef 100644 --- a/engine/item/item.cpp +++ b/engine/item/item.cpp @@ -682,6 +682,7 @@ inventory_type item_t::dbc_inventory_type() const case WEAPON_STAFF: case WEAPON_POLEARM: return INVTYPE_2HWEAPON; + // TODO: Can we remove this branch in Forever since thrown go into the ranged slot? case WEAPON_THROWN: return INVTYPE_THROWN; default: @@ -1441,8 +1442,6 @@ bool item_t::verify_slot() case INVTYPE_2HWEAPON: case INVTYPE_WEAPON: case INVTYPE_WEAPONMAINHAND: - case INVTYPE_RANGED: - case INVTYPE_RANGEDRIGHT: return true; default: return false; @@ -1462,6 +1461,19 @@ bool item_t::verify_slot() return false; } } + case SLOT_RANGED: + { + switch ( it ) + { + case INVTYPE_RANGED: + case INVTYPE_RANGEDRIGHT: + case INVTYPE_THROWN: + case INVTYPE_RELIC: + return true; + default: + return false; + } + } case SLOT_FINGER_1: case SLOT_FINGER_2: return it == INVTYPE_FINGER; diff --git a/engine/player/player.cpp b/engine/player/player.cpp index fb601420ff..1b58abc6a1 100644 --- a/engine/player/player.cpp +++ b/engine/player/player.cpp @@ -1033,6 +1033,7 @@ player_t::player_t( sim_t* s, player_e t, util::string_view n, race_e r ) // Attacks main_hand_attack( nullptr ), off_hand_attack( nullptr ), + ranged_attack( nullptr ), current_auto_attack_speed( 1.0 ), // Resources resources(), @@ -6522,6 +6523,8 @@ void player_t::halt() main_hand_attack->cancel(); if ( off_hand_attack ) off_hand_attack->cancel(); + if ( ranged_attack ) + ranged_attack->cancel(); } void player_t::stun() @@ -12296,6 +12299,7 @@ void player_t::create_options() add_option( opt_string( "back", items[ SLOT_BACK ].options_str ) ); 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( "tabard", items[ SLOT_TABARD ].options_str ) ); // Set Bonus @@ -13502,10 +13506,10 @@ void player_t::delay_ranged_auto_attacks( timespan_t delay, proc_t* proc ) bool delayed = false; - if ( main_hand_attack && main_hand_attack->execute_event && dynamic_cast(main_hand_attack) ) + if ( ranged_attack && ranged_attack->execute_event && dynamic_cast(ranged_attack) ) { - sim->print_debug( "Delaying ranged auto attack swing timer by {} to {}", delay, main_hand_attack->execute_event->remains() + delay ); - main_hand_attack->execute_event->reschedule( main_hand_attack->execute_event->remains() + delay ); + sim->print_debug( "Delaying ranged auto attack swing timer by {} to {}", delay, ranged_attack->execute_event->remains() + delay ); + ranged_attack->execute_event->reschedule( ranged_attack->execute_event->remains() + delay ); delayed = true; } diff --git a/engine/player/player.hpp b/engine/player/player.hpp index 0600c13fe5..1b548d5e90 100644 --- a/engine/player/player.hpp +++ b/engine/player/player.hpp @@ -266,6 +266,7 @@ struct player_t : public actor_t std::map melee_attacks; attack_t* main_hand_attack; attack_t* off_hand_attack; + attack_t* ranged_attack; // Current attack speed (needed for dynamic attack speed adjustments) double current_auto_attack_speed; diff --git a/engine/util/util.cpp b/engine/util/util.cpp index ab9b7a8019..c95dee0474 100644 --- a/engine/util/util.cpp +++ b/engine/util/util.cpp @@ -1119,6 +1119,7 @@ const char* util::slot_type_string( slot_e slot ) case SLOT_BACK: return "back"; case SLOT_MAIN_HAND: return "main_hand"; case SLOT_OFF_HAND: return "off_hand"; + case SLOT_RANGED: return "ranged"; case SLOT_TABARD: return "tabard"; default: return "unknown"; } @@ -2285,15 +2286,16 @@ slot_e util::translate_invtype( inventory_type inv_type ) case INVTYPE_2HWEAPON: case INVTYPE_WEAPON: case INVTYPE_WEAPONMAINHAND: - case INVTYPE_RANGED: - case INVTYPE_RANGEDRIGHT: return SLOT_MAIN_HAND; case INVTYPE_WEAPONOFFHAND: case INVTYPE_SHIELD: case INVTYPE_HOLDABLE: return SLOT_OFF_HAND; case INVTYPE_THROWN: - return SLOT_MAIN_HAND; + case INVTYPE_RANGED: + case INVTYPE_RANGEDRIGHT: + case INVTYPE_RELIC: + return SLOT_RANGED; case INVTYPE_CHEST: case INVTYPE_ROBE: return SLOT_CHEST; From 7fb4ead9bc2311d8cf80fa18decd027d5929472b Mon Sep 17 00:00:00 2001 From: Dave Hendler Date: Tue, 20 Oct 2026 16:25:07 -0700 Subject: [PATCH 4/7] Hook up Ranged Attack Power stat and AP type, get auto shot closer to observed --- engine/action/action.cpp | 3 + engine/action/attack.cpp | 7 +- engine/buff/buff.cpp | 9 ++- engine/class_modules/sc_hunter.cpp | 21 ++++- engine/dbc/sc_extra_data.inc | 2 +- engine/item/special_effect.cpp | 4 +- engine/player/gear_stats.hpp | 6 +- engine/player/player.cpp | 63 +++++++++++++++ engine/player/player.hpp | 6 +- engine/player/player_stat_cache.cpp | 13 +++ engine/player/player_stat_cache.hpp | 3 + engine/sc_enums.hpp | 7 ++ engine/sim/gear_stats.cpp | 10 ++- engine/util/util.cpp | 121 +++++++++++++++------------- 14 files changed, 204 insertions(+), 71 deletions(-) diff --git a/engine/action/action.cpp b/engine/action/action.cpp index f564665dc8..3c6f7d8097 100644 --- a/engine/action/action.cpp +++ b/engine/action/action.cpp @@ -1406,6 +1406,8 @@ 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? + // 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 dmg = capm * ( sim->averaged_range( weapon->min_dmg, weapon->max_dmg ) + weapon->bonus_dmg ); @@ -2795,6 +2797,7 @@ void action_t::init() // this only defines the ap type, the ability may not necessarily use attack power at all, however // that is not possible to know at init time with 100% accuracy. // Only overwrite this if the default attack_power_type::AP_NONE value is still set, since modules may set manually. + // TODO Forever - can a bunch of this go since we're mostly back in classic era weapons? if ( ap_type == attack_power_type::NONE ) { // Weapon multiplier is set. The power calculation for the ability uses base ap only, as the diff --git a/engine/action/attack.cpp b/engine/action/attack.cpp index ca1eabb0f7..73391b02f1 100644 --- a/engine/action/attack.cpp +++ b/engine/action/attack.cpp @@ -474,8 +474,11 @@ ranged_attack_t::ranged_attack_t( util::string_view token, player_t* p ) ranged_attack_t::ranged_attack_t( util::string_view token, player_t* p, const spell_data_t* s ) : attack_t( token, p, s ) { - // ranged attacks cannot be blocked or parried - may_block = may_parry = false; + // ranged attacks cannot be blocked, dodged, or parried + // TODO Forever: can they be glanced? + may_block = may_parry = may_dodge = false; + + ap_type = attack_power_type::RANGED; } // Ranged attacks are identical to melee attacks, but cannot be parried or dodged. diff --git a/engine/buff/buff.cpp b/engine/buff/buff.cpp index ff7558b8b4..4502889f37 100644 --- a/engine/buff/buff.cpp +++ b/engine/buff/buff.cpp @@ -3335,8 +3335,6 @@ stat_buff_t::stat_buff_t( actor_pair_t q, util::string_view name, const spell_da stat_gain( player->get_gain( fmt::format( "{}_buff", name ) ) ), // append _buff for now to check usage manual_stats_added( false ) { - bool has_ap = false; - for ( size_t i = 1; i <= data().effect_count(); i++ ) { stat_e s = STAT_NONE; @@ -3380,10 +3378,13 @@ stat_buff_t::stat_buff_t( actor_pair_t q, util::string_view name, const spell_da { s = STAT_BONUS_ARMOR; } - else if ( !has_ap && ( effect.subtype() == A_MOD_ATTACK_POWER || effect.subtype() == A_MOD_RANGED_ATTACK_POWER ) ) + else if ( effect.subtype() == A_MOD_ATTACK_POWER ) { s = STAT_ATTACK_POWER; - has_ap = true; + } + else if ( effect.subtype() == A_MOD_RANGED_ATTACK_POWER ) + { + s = STAT_RANGED_ATTACK_POWER; } else if ( effect.subtype() == A_MOD_INCREASE_HEALTH_2 || effect.subtype() == A_MOD_INCREASE_HEALTH ) { diff --git a/engine/class_modules/sc_hunter.cpp b/engine/class_modules/sc_hunter.cpp index 686b1655cf..8e94246d9d 100644 --- a/engine/class_modules/sc_hunter.cpp +++ b/engine/class_modules/sc_hunter.cpp @@ -7,6 +7,10 @@ #include "action/parse_effects.hpp" #include "class_modules/apl/hunter.hpp" +// TODO: Default to max distance +// TODO: ItemDamageRanged for ranged weapon damage multipliers +// TODO: Does "plain" +Attack Power contribute to RAP? + namespace { // ========================================================================= @@ -66,10 +70,7 @@ struct hunter_ranged_attack_t : public hunter_ranged_t background = true; repeating = true; - may_glance = false; may_crit = true; - may_dodge = false; - may_parry = false; special = false; } }; @@ -77,7 +78,7 @@ struct hunter_ranged_attack_t : public hunter_ranged_t struct hunter_auto_shot_t : public hunter_ranged_t { hunter_auto_shot_t( hunter_t* p, util::string_view options_str ) - : hunter_ranged_t( "auto_shot", p, spell_data_t::nil() ) + : hunter_ranged_t( "auto_shot", p, p->find_spell( 75 ) ) { parse_options( options_str ); @@ -89,6 +90,8 @@ struct hunter_auto_shot_t : public hunter_ranged_t p->ranged_attack = new hunter_ranged_attack_t( "auto_shot", p ); } + // TODO: Copy midnight's immediate first shot + void execute() override { player->ranged_attack->schedule_execute(); @@ -107,7 +110,17 @@ struct hunter_auto_shot_t : public hunter_ranged_t void hunter_t::init_base_stats() { + base.attack_power_per_strength = 1.0; + base.attack_power_per_agility = 1.0; + + base.ranged_attack_power_per_agility = 2.0; + player_t::init_base_stats(); + + base.attack_crit_chance = 0; + + base.stats.attack_power = 2.0 * level() - 20.0; + base.stats.ranged_attack_power = 2.0 * level() - 20.0; } action_t* hunter_t::create_action( util::string_view name, util::string_view options_str ) diff --git a/engine/dbc/sc_extra_data.inc b/engine/dbc/sc_extra_data.inc index fbc46180d2..c987e307fc 100644 --- a/engine/dbc/sc_extra_data.inc +++ b/engine/dbc/sc_extra_data.inc @@ -403,7 +403,7 @@ static constexpr stat_data_t __gt_class_stats_by_level[][120] = { 23, 31, 27, 23, 25 }, { 23, 32, 28, 24, 26 }, { 24, 34, 29, 25, 26 }, // 10 - { 22, 36, 584, 33, 0 }, + { 24, 35, 30, 25, 27 }, { 24, 39, 614, 34, 0 }, { 26, 43, 643, 36, 0 }, { 28, 46, 672, 38, 0 }, diff --git a/engine/item/special_effect.cpp b/engine/item/special_effect.cpp index d4fb99e1e4..344642186a 100644 --- a/engine/item/special_effect.cpp +++ b/engine/item/special_effect.cpp @@ -227,9 +227,11 @@ stat_e special_effect_t::stat_buff_type( const spelleffect_data_t& effect ) cons stat_ = STAT_BONUS_ARMOR; break; case A_MOD_ATTACK_POWER: - case A_MOD_RANGED_ATTACK_POWER: stat_ = STAT_ATTACK_POWER; break; + case A_MOD_RANGED_ATTACK_POWER: + stat_ = STAT_RANGED_ATTACK_POWER; + break; case A_MOD_DAMAGE_DONE: if ( effect.misc_value1() & 0x7E ) stat_ = STAT_SPELL_POWER; diff --git a/engine/player/gear_stats.hpp b/engine/player/gear_stats.hpp index cf2ccbd429..469b151d23 100644 --- a/engine/player/gear_stats.hpp +++ b/engine/player/gear_stats.hpp @@ -22,6 +22,7 @@ struct gear_stats_t std::array resource; double spell_power; double attack_power; + double ranged_attack_power; double expertise_rating; double expertise_rating2; double hit_rating; @@ -51,6 +52,7 @@ struct gear_stats_t resource(), spell_power( 0.0 ), attack_power( 0.0 ), + ranged_attack_power( 0.0 ), expertise_rating( 0.0 ), expertise_rating2( 0.0 ), hit_rating( 0.0 ), @@ -84,6 +86,7 @@ struct gear_stats_t spell_power = initializer; attack_power = initializer; + ranged_attack_power = initializer; expertise_rating = initializer; expertise_rating2 = initializer; hit_rating = initializer; @@ -119,6 +122,7 @@ struct gear_stats_t { spell_power += right.spell_power; attack_power += right.attack_power; + ranged_attack_power += right.ranged_attack_power; expertise_rating += right.expertise_rating; expertise_rating2 += right.expertise_rating2; hit_rating += right.hit_rating; @@ -152,4 +156,4 @@ struct gear_stats_t static double stat_mod( stat_e stat ); friend void sc_format_to( const gear_stats_t&, fmt::format_context::iterator ); -}; \ No newline at end of file +}; diff --git a/engine/player/player.cpp b/engine/player/player.cpp index 1b58abc6a1..1b8da6f811 100644 --- a/engine/player/player.cpp +++ b/engine/player/player.cpp @@ -1248,6 +1248,7 @@ player_t::base_initial_current_t::base_initial_current_t() : attack_power_per_strength( 0 ), attack_power_per_agility( 0 ), attack_power_per_spell_power( 0 ), + ranged_attack_power_per_agility( 0 ), dodge_per_agility( 0 ), parry_per_strength( 0 ), parry_rating_per_crit_rating( 0 ), @@ -1286,6 +1287,7 @@ player_t::base_initial_current_t::base_initial_current_t() : rating_multiplier(), spell_power_multiplier( 1.0 ), attack_power_multiplier( 1.0 ), + ranged_attack_power_multiplier( 1.0 ), base_armor_multiplier( 1.0 ), armor_multiplier( 1.0 ), crit_damage_multiplier(), @@ -1319,6 +1321,7 @@ void sc_format_to( const player_t::base_initial_current_t& s, fmt::format_contex fmt::format_to( out, " attack_power_per_strength={:.6g}", s.attack_power_per_strength ); fmt::format_to( out, " attack_power_per_agility={:.6g}", s.attack_power_per_agility ); fmt::format_to( out, " attack_power_per_spell_power={:.6g}", s.attack_power_per_spell_power ); + fmt::format_to( out, " ranged_attack_power_per_agility={:.6g}", s.ranged_attack_power_per_agility ); fmt::format_to( out, " dodge_per_agility={:.6g}", s.dodge_per_agility ); fmt::format_to( out, " parry_per_strength={:.6g}", s.parry_per_strength ); fmt::format_to( out, " parry_rating_per_crit_rating={:.6g}", s.parry_rating_per_crit_rating ); @@ -1356,6 +1359,7 @@ void sc_format_to( const player_t::base_initial_current_t& s, fmt::format_contex fmt::format_to( out, " spell_power_multiplier={:.6g}", s.spell_power_multiplier ); fmt::format_to( out, " attack_power_multiplier={:.6g}", s.attack_power_multiplier ); + fmt::format_to( out, " ranged_attack_power_multiplier={:.6g}", s.ranged_attack_power_multiplier ); fmt::format_to( out, " base_armor_multiplier={:.6g}", s.base_armor_multiplier ); fmt::format_to( out, " armor_multiplier={:.6g}", s.armor_multiplier ); for ( auto school = SCHOOL_NONE; school < SCHOOL_MAX_PRIMARY; ++school ) @@ -1543,6 +1547,7 @@ void player_t::init_base_stats() base.attack_speed_multiplier /= get_passive_player_value( attack_speed_multiplier, "attack_speed" ); base.attack_speed_multiplier *= get_passive_player_value( attack_speed_multiplier, "attack_speed_modifier" ); base.attack_power_multiplier = get_passive_player_value( base.attack_power_multiplier, "attack_power_multiplier" ); + base.ranged_attack_power_multiplier = get_passive_player_value( base.ranged_attack_power_multiplier, "ranged_attack_power_multiplier" ); base.absorb_multiplier = get_passive_player_value( base.absorb_multiplier, "absorb_multiplier" ); base.healing_multiplier = get_passive_player_value( base.healing_multiplier, "healing_multiplier" ); @@ -3426,6 +3431,8 @@ void player_t::init_scaling() scaling->set( STAT_SPELL_POWER, spell ); scaling->set( STAT_ATTACK_POWER, attack ); + // TODO Forever: Limit this to hunters + scaling->set( STAT_RANGED_ATTACK_POWER, attack ); scaling->enable( STAT_CRIT_RATING ); scaling->enable( STAT_HASTE_RATING ); @@ -3474,6 +3481,10 @@ void player_t::init_scaling() add_stat( initial.stats.attack_power, v, 0 ); break; + case STAT_RANGED_ATTACK_POWER: + add_stat( initial.stats.ranged_attack_power, v, 0 ); + break; + case STAT_CRIT_RATING: add_stat( initial.stats.crit_rating, v, 0 ); break; @@ -4425,6 +4436,15 @@ double player_t::composite_melee_attack_power() const return ap; } +double player_t::composite_ranged_attack_power() const +{ + double ap = current.stats.ranged_attack_power; + + ap += current.ranged_attack_power_per_agility * cache.agility(); + + return ap; +} + double player_t::composite_weapon_attack_power_by_type( attack_power_type ap_type ) const { double wdps = 0; @@ -4500,6 +4520,11 @@ double player_t::composite_total_attack_power_by_type( attack_power_type ap_type return std::round( current.attack_power_per_spell_power * sp ); } + if ( ap_type == attack_power_type::RANGED ) + { + return std::round( cache.ranged_attack_power() * composite_ranged_attack_power_multiplier() ); + } + auto mul = composite_attack_power_multiplier(); // total attack power is rounded to integer @@ -4521,6 +4546,22 @@ double player_t::composite_attack_power_multiplier() const return std::round( m * 1000 ) * 0.001; } +double player_t::composite_ranged_attack_power_multiplier() const +{ + if ( is_pet() || is_enemy() || type == HEALING_ENEMY || current.attack_power_per_spell_power > 0 ) + { + return 1.0; + } + + double m = current.ranged_attack_power_multiplier; + + // TODO Forever: Trueshot aura? + // TODO Forever: Hunter's mark? + + // multiplier is rounded to 3 digits + return std::round( m * 1000 ) * 0.001; +} + double player_t::composite_melee_crit_chance() const { double ac = current.attack_crit_chance; @@ -5205,6 +5246,8 @@ void player_t::invalidate_cache( cache_e c ) case CACHE_AGILITY: if ( current.attack_power_per_agility > 0 ) invalidate_cache( CACHE_ATTACK_POWER ); + if ( current.ranged_attack_power_per_agility > 0 ) + invalidate_cache( CACHE_RANGED_ATTACK_POWER ); if ( current.dodge_per_agility > 0 ) invalidate_cache( CACHE_DODGE ); if ( current.attack_crit_per_agility > 0 ) @@ -5245,6 +5288,8 @@ void player_t::invalidate_cache( cache_e c ) invalidate_cache( CACHE_PARRY ); break; + // TODO Forever: Do we need cases for AP and RAP? + default: break; } @@ -6654,6 +6699,8 @@ double player_t::get_stat_value(stat_e stat) return cache.spell_power( SCHOOL_NONE ); case STAT_ATTACK_POWER: return cache.attack_power(); + case STAT_RANGED_ATTACK_POWER: + return cache.ranged_attack_power(); case STAT_CRIT_RATING: return composite_melee_crit_rating(); case STAT_HASTE_RATING: @@ -7005,6 +7052,7 @@ void player_t::stat_gain( stat_e stat, double amount, gain_t* gain, action_t* ac case STAT_SPIRIT: case STAT_SPELL_POWER: case STAT_ATTACK_POWER: + case STAT_RANGED_ATTACK_POWER: case STAT_EXPERTISE_RATING: case STAT_HIT_RATING: case STAT_CRIT_RATING: @@ -7164,6 +7212,7 @@ void player_t::stat_loss( stat_e stat, double amount, gain_t* gain, action_t* ac case STAT_SPIRIT: case STAT_SPELL_POWER: case STAT_ATTACK_POWER: + case STAT_RANGED_ATTACK_POWER: case STAT_EXPERTISE_RATING: case STAT_HIT_RATING: case STAT_CRIT_RATING: @@ -11202,6 +11251,13 @@ std::unique_ptr player_t::create_expression( util::string_view expressio } ); } + case STAT_RANGED_ATTACK_POWER: + { + return make_fn_expr( expression_str, [ this ] { + return static_cast( cache.ranged_attack_power() * composite_ranged_attack_power_multiplier() ); + } ); + } + case STAT_EXPERTISE_RATING: return make_mem_fn_expr( expression_str, *this, &player_t::composite_expertise_rating ); case STAT_HIT_RATING: @@ -12093,6 +12149,9 @@ std::string player_t::create_profile( save_e stype ) if ( enchant.attack_power != 0 ) profile_str += "enchant_attack_power=" + util::to_string( enchant.attack_power ) + term; + if ( enchant.ranged_attack_power != 0 ) + profile_str += "enchant_ranged_attack_power=" + util::to_string( enchant.ranged_attack_power ) + term; + if ( enchant.expertise_rating != 0 ) profile_str += "enchant_expertise_rating=" + util::to_string( enchant.expertise_rating ) + term; @@ -12314,6 +12373,7 @@ void player_t::create_options() add_option( opt_float( "gear_spirit", gear.attribute[ ATTR_SPIRIT ] ) ); add_option( opt_float( "gear_spell_power", gear.spell_power ) ); add_option( opt_float( "gear_attack_power", gear.attack_power ) ); + add_option( opt_float( "gear_ranged_attack_power", gear.ranged_attack_power ) ); add_option( opt_float( "gear_expertise_rating", gear.expertise_rating ) ); add_option( opt_float( "gear_haste_rating", gear.haste_rating ) ); add_option( opt_float( "gear_hit_rating", gear.hit_rating ) ); @@ -12341,6 +12401,7 @@ void player_t::create_options() add_option( opt_float( "enchant_spirit", enchant.attribute[ ATTR_SPIRIT ] ) ); add_option( opt_float( "enchant_spell_power", enchant.spell_power ) ); add_option( opt_float( "enchant_attack_power", enchant.attack_power ) ); + add_option( opt_float( "enchant_ranged_attack_power", enchant.ranged_attack_power ) ); add_option( opt_float( "enchant_expertise_rating", enchant.expertise_rating ) ); add_option( opt_float( "enchant_armor", enchant.armor ) ); add_option( opt_float( "enchant_haste_rating", enchant.haste_rating ) ); @@ -13955,6 +14016,7 @@ static constexpr std::pair field_type_map[] = { { A_MOD_BASE_RESISTANCE_PCT, "base_armor_multiplier" }, // 142 { A_MOD_CRIT_DAMAGE_MULTIPLIER, "crit_damage_multiplier" }, // 163 { A_MOD_ATTACK_POWER_PCT, "attack_power_multiplier", }, // 166 + { A_MOD_RANGED_ATTACK_POWER_PCT, "ranged_attack_power_multiplier", }, // 167 { A_MOD_RECHARGE_TIME_PCT_CATEGORY_MASK, "charge_cooldown" }, // 173 { A_MOD_MAX_MANA_PCT, "resource_multiplier" }, // 178 { A_MOD_ATTACKER_MELEE_CRIT_CHANCE, "crit_avoidance" }, // 187 @@ -14310,6 +14372,7 @@ bool player_t::register_passive_effect( const spelleffect_data_t& modifying_eff, case A_MOD_HEALING_DONE_PERCENT: // 136 case A_MOD_CRIT_DAMAGE_MULTIPLIER: // 163 case A_MOD_ATTACK_POWER_PCT: // 166 + case A_MOD_RANGED_ATTACK_POWER_PCT: // 167 case A_HASTE_ALL: // 193 case A_MOD_MECHANIC_DAMAGE_DONE_PERCENT: // 276 case A_MOD_MELEE_AUTO_ATTACK_SPEED: // 319 diff --git a/engine/player/player.hpp b/engine/player/player.hpp index 1b548d5e90..e309421018 100644 --- a/engine/player/player.hpp +++ b/engine/player/player.hpp @@ -202,6 +202,7 @@ struct player_t : public actor_t double spell_power_per_intellect, spell_power_per_attack_power; double attack_power_per_strength, attack_power_per_agility, attack_power_per_spell_power; + double ranged_attack_power_per_agility; double dodge_per_agility, parry_per_strength, parry_rating_per_crit_rating; double attack_crit_per_agility, spell_crit_per_intellect; double health_per_stamina; @@ -222,7 +223,7 @@ struct player_t : public actor_t std::array attribute_multiplier; std::array matching_armor_multiplier; std::array rating_multiplier; - double spell_power_multiplier, attack_power_multiplier, base_armor_multiplier, armor_multiplier; + double spell_power_multiplier, attack_power_multiplier, ranged_attack_power_multiplier, base_armor_multiplier, armor_multiplier; std::array crit_damage_multiplier; double crit_healing_multiplier; double attack_speed_multiplier; @@ -873,8 +874,10 @@ struct player_t : public actor_t virtual double composite_melee_haste() const; virtual double composite_melee_auto_attack_speed() const; virtual double composite_melee_attack_power() const; + virtual double composite_ranged_attack_power() const; virtual double composite_weapon_attack_power_by_type( attack_power_type ) const; virtual double composite_total_attack_power_by_type( attack_power_type ) const; + // TODO Forever: Do I need weapon ranged attack power composites here? virtual double composite_melee_hit() const; virtual double composite_melee_crit_chance() const; virtual double composite_melee_crit_chance_multiplier() const @@ -905,6 +908,7 @@ struct player_t : public actor_t virtual double composite_block_value( const action_state_t* s ) const; virtual double composite_crit_avoidance() const; virtual double composite_attack_power_multiplier() const; + virtual double composite_ranged_attack_power_multiplier() const; virtual double composite_spell_power_multiplier() const; virtual double matching_gear_multiplier( attribute_e /* attr */ ) const; /// Player-wide school based multipliers diff --git a/engine/player/player_stat_cache.cpp b/engine/player/player_stat_cache.cpp index 461ecb186e..17aa6edffb 100644 --- a/engine/player/player_stat_cache.cpp +++ b/engine/player/player_stat_cache.cpp @@ -169,6 +169,18 @@ double player_stat_cache_t::attack_power() const return _attack_power; } +double player_stat_cache_t::ranged_attack_power() const +{ + if ( !active || !valid[ CACHE_RANGED_ATTACK_POWER ] ) + { + valid[ CACHE_RANGED_ATTACK_POWER ] = true; + _ranged_attack_power = player->composite_ranged_attack_power(); + } + else + assert( _ranged_attack_power == player->composite_ranged_attack_power() ); + return _ranged_attack_power; +} + double player_stat_cache_t::weapon_attack_power( attack_power_type t ) const { auto type = static_cast( t ); @@ -489,6 +501,7 @@ double player_stat_cache_t::intellect() const { return _player->intellect(); } double player_stat_cache_t::spirit() const { return _player->spirit(); } double player_stat_cache_t::spell_power( school_e s ) const { return _player->composite_spell_power( s ); } double player_stat_cache_t::attack_power() const { return _player->composite_melee_attack_power(); } +double player_stat_cache_t::ranged_attack_power() const { return _player->composite_ranged_attack_power(); } double player_stat_cache_t::weapon_attack_power( attack_power_type t ) { const return _player->composite_weapon_attack_power_by_type( t ); } double player_stat_cache_t::attack_expertise() const { return _player->composite_melee_expertise(); } double player_stat_cache_t::attack_hit() const { return _player->composite_melee_hit(); } diff --git a/engine/player/player_stat_cache.hpp b/engine/player/player_stat_cache.hpp index dbb740da0b..8dae9e7a2a 100644 --- a/engine/player/player_stat_cache.hpp +++ b/engine/player/player_stat_cache.hpp @@ -49,6 +49,7 @@ struct player_stat_cache_t mutable double _strength, _agility, _stamina, _intellect, _spirit; mutable std::array _spell_power; mutable double _attack_power; + mutable double _ranged_attack_power; mutable std::array( attack_power_type::NONE )> _weapon_attack_power; mutable double _attack_expertise; mutable double _attack_hit, _spell_hit; @@ -77,6 +78,7 @@ struct player_stat_cache_t double spirit() const; double spell_power( school_e ) const; double attack_power() const; + double ranged_attack_power() const; double weapon_attack_power( attack_power_type ) const; double attack_expertise() const; double attack_hit() const; @@ -111,6 +113,7 @@ struct player_stat_cache_t double spirit() const { return _player->spirit(); } double spell_power( school_e s ) const { return _player->composite_spell_power( s ); } double attack_power() const { return _player->composite_melee_attack_power(); } + double ranged_attack_power() const { return _player->composite_ranged_attack_power(); } double weapon_attack_power( attack_power_type t ) const { return _player->composite_weapon_attack_power_by_type( t ); } double attack_expertise() const { return _player->composite_melee_expertise(); } double attack_hit() const { return _player->composite_melee_hit(); } diff --git a/engine/sc_enums.hpp b/engine/sc_enums.hpp index ccb97fde42..31a30304bd 100644 --- a/engine/sc_enums.hpp +++ b/engine/sc_enums.hpp @@ -44,6 +44,9 @@ enum class attack_power_type : unsigned // Attack power is purely based on player power (main stat) NO_WEAPON, + // Attack power is a composite of RAP and ranged weapon DPS + RANGED, + // Let SimC figure out BfA attack power mode based on information assigned to the action object. NONE, @@ -841,6 +844,7 @@ enum stat_e STAT_MAX_RUNIC, STAT_SPELL_POWER, STAT_ATTACK_POWER, + STAT_RANGED_ATTACK_POWER, STAT_EXPERTISE_RATING, STAT_EXPERTISE_RATING2, STAT_HIT_RATING, @@ -945,6 +949,7 @@ enum cache_e CACHE_STR_AGI_INT, CACHE_SPELL_POWER, CACHE_ATTACK_POWER, + CACHE_RANGED_ATTACK_POWER, CACHE_WEAPON_DPS, CACHE_EXP, CACHE_ATTACK_EXP, @@ -1023,6 +1028,8 @@ inline cache_e cache_from_stat( stat_e st ) return CACHE_SPELL_POWER; case STAT_ATTACK_POWER: return CACHE_ATTACK_POWER; + case STAT_RANGED_ATTACK_POWER: + return CACHE_RANGED_ATTACK_POWER; case STAT_WEAPON_DPS: case STAT_WEAPON_OFFHAND_DPS: return CACHE_WEAPON_DPS; diff --git a/engine/sim/gear_stats.cpp b/engine/sim/gear_stats.cpp index 01c052c1d6..aa97693425 100644 --- a/engine/sim/gear_stats.cpp +++ b/engine/sim/gear_stats.cpp @@ -48,6 +48,7 @@ void gear_stats_t::add_stat( stat_e stat, case STAT_SPELL_POWER: spell_power += value; break; case STAT_ATTACK_POWER: attack_power += value; break; + case STAT_RANGED_ATTACK_POWER: ranged_attack_power += value; break; case STAT_EXPERTISE_RATING: expertise_rating += value; break; case STAT_EXPERTISE_RATING2: expertise_rating2 += value; break; @@ -122,6 +123,7 @@ void gear_stats_t::set_stat( stat_e stat, case STAT_SPELL_POWER: spell_power = value; break; case STAT_ATTACK_POWER: attack_power = value; break; + case STAT_RANGED_ATTACK_POWER: ranged_attack_power = value; break; case STAT_EXPERTISE_RATING: expertise_rating = value; break; case STAT_EXPERTISE_RATING2: expertise_rating2 = value; break; @@ -195,6 +197,7 @@ double gear_stats_t::get_stat( stat_e stat ) const case STAT_SPELL_POWER: return spell_power; case STAT_ATTACK_POWER: return attack_power; + case STAT_RANGED_ATTACK_POWER: return ranged_attack_power; case STAT_EXPERTISE_RATING: return expertise_rating; case STAT_EXPERTISE_RATING2: return expertise_rating2; @@ -236,9 +239,10 @@ double gear_stats_t::stat_mod( stat_e stat ) { switch ( stat ) { - case STAT_ATTACK_POWER: return 1.0; - case STAT_SPELL_POWER: return 1.0; - default: return 1.0; + case STAT_ATTACK_POWER: return 1.0; + case STAT_RANGED_ATTACK_POWER: return 1.0; + case STAT_SPELL_POWER: return 1.0; + default: return 1.0; } } diff --git a/engine/util/util.cpp b/engine/util/util.cpp index c95dee0474..eb944afa52 100644 --- a/engine/util/util.cpp +++ b/engine/util/util.cpp @@ -260,6 +260,10 @@ double util::stat_value( const player_t* p, stat_e stat ) { v -= p->sim->scaling->scale_value * p->composite_attack_power_multiplier(); } + else if ( stat == STAT_RANGED_ATTACK_POWER ) + { + v -= p->sim->scaling->scale_value * p->composite_ranged_attack_power_multiplier(); + } } return v; @@ -1254,6 +1258,7 @@ const char* util::cache_type_string( cache_e c ) case CACHE_SPIRIT: return "spirit"; case CACHE_SPELL_POWER: return "spell_power"; case CACHE_ATTACK_POWER: return "attack_power"; + case CACHE_RANGED_ATTACK_POWER: return "ranged_attack_power"; case CACHE_WEAPON_DPS: return "weapon_dps"; case CACHE_EXP: return "expertise"; case CACHE_ATTACK_EXP: return "attack_expertise"; @@ -1421,6 +1426,7 @@ const char* util::stat_type_string( stat_e stat ) case STAT_SPELL_POWER: return "spell_power"; case STAT_ATTACK_POWER: return "attack_power"; + case STAT_RANGED_ATTACK_POWER: return "ranged_attack_power"; case STAT_EXPERTISE_RATING: return "expertise_rating"; case STAT_EXPERTISE_RATING2: return "inverse_expertise_rating"; @@ -1507,6 +1513,7 @@ const char* util::stat_type_abbrev( stat_e stat ) case STAT_SPELL_POWER: return "SP"; case STAT_ATTACK_POWER: return "AP"; + case STAT_RANGED_ATTACK_POWER: return "RAP"; case STAT_EXPERTISE_RATING: return "Exp"; case STAT_EXPERTISE_RATING2: return "InvExp"; @@ -1569,6 +1576,8 @@ const char* util::stat_type_wowhead( stat_e stat ) case STAT_SPELL_POWER: return "spellPower"; case STAT_ATTACK_POWER: return "attackPower"; + // TODO Forever: Not sure how to verify this + case STAT_RANGED_ATTACK_POWER: return "rangedAttackPower"; case STAT_EXPERTISE_RATING: return "expertiseRating"; case STAT_HIT_RATING: return "hitRating"; @@ -2052,34 +2061,34 @@ stat_e util::translate_item_mod( int item_mod ) { switch ( item_mod ) { - case ITEM_MOD_AGILITY: return STAT_AGILITY; - case ITEM_MOD_STRENGTH: return STAT_STRENGTH; - case ITEM_MOD_INTELLECT: return STAT_INTELLECT; - case ITEM_MOD_SPIRIT: return STAT_SPIRIT; - case ITEM_MOD_STAMINA: return STAT_STAMINA; - case ITEM_MOD_DODGE_RATING: return STAT_DODGE_RATING; - case ITEM_MOD_PARRY_RATING: return STAT_PARRY_RATING; - case ITEM_MOD_BLOCK_RATING: return STAT_BLOCK_RATING; - case ITEM_MOD_HIT_RATING: return STAT_HIT_RATING; - case ITEM_MOD_CRIT_RATING: return STAT_CRIT_RATING; - case ITEM_MOD_HASTE_RATING: return STAT_HASTE_RATING; - case ITEM_MOD_EXPERTISE_RATING: return STAT_EXPERTISE_RATING; - case ITEM_MOD_ATTACK_POWER: return STAT_ATTACK_POWER; - case ITEM_MOD_RANGED_ATTACK_POWER: return STAT_ATTACK_POWER; - case ITEM_MOD_SPELL_POWER: return STAT_SPELL_POWER; - case ITEM_MOD_EXTRA_ARMOR: return STAT_BONUS_ARMOR; - case ITEM_MOD_RESILIENCE_RATING: return STAT_RESILIENCE_RATING; - case ITEM_MOD_PVP_POWER: return STAT_PVP_POWER; + case ITEM_MOD_AGILITY: return STAT_AGILITY; + case ITEM_MOD_STRENGTH: return STAT_STRENGTH; + case ITEM_MOD_INTELLECT: return STAT_INTELLECT; + case ITEM_MOD_SPIRIT: return STAT_SPIRIT; + case ITEM_MOD_STAMINA: return STAT_STAMINA; + case ITEM_MOD_DODGE_RATING: return STAT_DODGE_RATING; + case ITEM_MOD_PARRY_RATING: return STAT_PARRY_RATING; + case ITEM_MOD_BLOCK_RATING: return STAT_BLOCK_RATING; + case ITEM_MOD_HIT_RATING: return STAT_HIT_RATING; + case ITEM_MOD_CRIT_RATING: return STAT_CRIT_RATING; + case ITEM_MOD_HASTE_RATING: return STAT_HASTE_RATING; + case ITEM_MOD_EXPERTISE_RATING: return STAT_EXPERTISE_RATING; + case ITEM_MOD_ATTACK_POWER: return STAT_ATTACK_POWER; + case ITEM_MOD_RANGED_ATTACK_POWER: return STAT_RANGED_ATTACK_POWER; + case ITEM_MOD_SPELL_POWER: return STAT_SPELL_POWER; + case ITEM_MOD_EXTRA_ARMOR: return STAT_BONUS_ARMOR; + case ITEM_MOD_RESILIENCE_RATING: return STAT_RESILIENCE_RATING; + case ITEM_MOD_PVP_POWER: return STAT_PVP_POWER; case ITEM_MOD_STRENGTH_AGILITY_INTELLECT: return STAT_STR_AGI_INT; - case ITEM_MOD_AGILITY_INTELLECT: return STAT_AGI_INT; - case ITEM_MOD_STRENGTH_AGILITY: return STAT_STR_AGI; - case ITEM_MOD_STRENGTH_INTELLECT: return STAT_STR_INT; - case ITEM_MOD_LEECH_RATING: return STAT_LEECH_RATING; - case ITEM_MOD_SPEED_RATING: return STAT_SPEED_RATING; - case ITEM_MOD_AVOIDANCE_RATING: return STAT_AVOIDANCE_RATING; - case ITEM_MOD_CORRUPTION: return STAT_CORRUPTION; - case ITEM_MOD_CORRUPTION_RESISTANCE: return STAT_CORRUPTION_RESISTANCE; - default: return STAT_NONE; + case ITEM_MOD_AGILITY_INTELLECT: return STAT_AGI_INT; + case ITEM_MOD_STRENGTH_AGILITY: return STAT_STR_AGI; + case ITEM_MOD_STRENGTH_INTELLECT: return STAT_STR_INT; + case ITEM_MOD_LEECH_RATING: return STAT_LEECH_RATING; + case ITEM_MOD_SPEED_RATING: return STAT_SPEED_RATING; + case ITEM_MOD_AVOIDANCE_RATING: return STAT_AVOIDANCE_RATING; + case ITEM_MOD_CORRUPTION: return STAT_CORRUPTION; + case ITEM_MOD_CORRUPTION_RESISTANCE: return STAT_CORRUPTION_RESISTANCE; + default: return STAT_NONE; } } @@ -2087,32 +2096,33 @@ int util::translate_stat( stat_e stat ) { switch ( stat ) { - case STAT_AGILITY: return ITEM_MOD_AGILITY; - case STAT_STRENGTH: return ITEM_MOD_STRENGTH; - case STAT_INTELLECT: return ITEM_MOD_INTELLECT; - case STAT_SPIRIT: return ITEM_MOD_SPIRIT; - case STAT_STAMINA: return ITEM_MOD_STAMINA; - case STAT_DODGE_RATING: return ITEM_MOD_DODGE_RATING; - case STAT_PARRY_RATING: return ITEM_MOD_PARRY_RATING; - case STAT_HIT_RATING: return ITEM_MOD_HIT_RATING; - case STAT_CRIT_RATING: return ITEM_MOD_CRIT_RATING; - case STAT_HASTE_RATING: return ITEM_MOD_HASTE_RATING; - case STAT_EXPERTISE_RATING: return ITEM_MOD_EXPERTISE_RATING; - case STAT_ATTACK_POWER: return ITEM_MOD_ATTACK_POWER; - case STAT_SPELL_POWER: return ITEM_MOD_SPELL_POWER; - case STAT_BONUS_ARMOR: return ITEM_MOD_EXTRA_ARMOR; - case STAT_RESILIENCE_RATING: return ITEM_MOD_RESILIENCE_RATING; - case STAT_PVP_POWER: return ITEM_MOD_PVP_POWER; - case STAT_STR_AGI_INT: return ITEM_MOD_STRENGTH_AGILITY_INTELLECT; - case STAT_AGI_INT: return ITEM_MOD_AGILITY_INTELLECT; - case STAT_STR_AGI: return ITEM_MOD_STRENGTH_AGILITY; - case STAT_STR_INT: return ITEM_MOD_STRENGTH_INTELLECT; - case STAT_LEECH_RATING: return ITEM_MOD_LEECH_RATING; - case STAT_SPEED_RATING: return ITEM_MOD_SPEED_RATING; - case STAT_AVOIDANCE_RATING: return ITEM_MOD_AVOIDANCE_RATING; - case STAT_CORRUPTION: return ITEM_MOD_CORRUPTION; + case STAT_AGILITY: return ITEM_MOD_AGILITY; + case STAT_STRENGTH: return ITEM_MOD_STRENGTH; + case STAT_INTELLECT: return ITEM_MOD_INTELLECT; + case STAT_SPIRIT: return ITEM_MOD_SPIRIT; + case STAT_STAMINA: return ITEM_MOD_STAMINA; + case STAT_DODGE_RATING: return ITEM_MOD_DODGE_RATING; + case STAT_PARRY_RATING: return ITEM_MOD_PARRY_RATING; + case STAT_HIT_RATING: return ITEM_MOD_HIT_RATING; + case STAT_CRIT_RATING: return ITEM_MOD_CRIT_RATING; + case STAT_HASTE_RATING: return ITEM_MOD_HASTE_RATING; + case STAT_EXPERTISE_RATING: return ITEM_MOD_EXPERTISE_RATING; + case STAT_ATTACK_POWER: return ITEM_MOD_ATTACK_POWER; + case STAT_RANGED_ATTACK_POWER: return ITEM_MOD_RANGED_ATTACK_POWER; + case STAT_SPELL_POWER: return ITEM_MOD_SPELL_POWER; + case STAT_BONUS_ARMOR: return ITEM_MOD_EXTRA_ARMOR; + case STAT_RESILIENCE_RATING: return ITEM_MOD_RESILIENCE_RATING; + case STAT_PVP_POWER: return ITEM_MOD_PVP_POWER; + case STAT_STR_AGI_INT: return ITEM_MOD_STRENGTH_AGILITY_INTELLECT; + case STAT_AGI_INT: return ITEM_MOD_AGILITY_INTELLECT; + case STAT_STR_AGI: return ITEM_MOD_STRENGTH_AGILITY; + case STAT_STR_INT: return ITEM_MOD_STRENGTH_INTELLECT; + case STAT_LEECH_RATING: return ITEM_MOD_LEECH_RATING; + case STAT_SPEED_RATING: return ITEM_MOD_SPEED_RATING; + case STAT_AVOIDANCE_RATING: return ITEM_MOD_AVOIDANCE_RATING; + case STAT_CORRUPTION: return ITEM_MOD_CORRUPTION; case STAT_CORRUPTION_RESISTANCE: return ITEM_MOD_CORRUPTION_RESISTANCE; - default: return ITEM_MOD_NONE; + default: return ITEM_MOD_NONE; } } @@ -3202,8 +3212,11 @@ void util::fuzzy_stats( std::string& encoding, stat_search( encoding, splits, STAT_SPELL_POWER, "spell power" ); - stat_search( encoding, splits, STAT_ATTACK_POWER, "attack power" ); - stat_search( encoding, splits, STAT_EXPERTISE_RATING, "expertise rating" ); + stat_search( encoding, splits, STAT_ATTACK_POWER, "attack power" ); + // TODO Forever: I think this is very old code that doesn't atually get used but need to be careful + // since "attack power" would string match "ranged attack power" + stat_search( encoding, splits, STAT_RANGED_ATTACK_POWER, "ranged attack power" ); + stat_search( encoding, splits, STAT_EXPERTISE_RATING, "expertise rating" ); stat_search( encoding, splits, STAT_HASTE_RATING, "haste" ); stat_search( encoding, splits, STAT_HASTE_RATING, "haste rating" ); From 0731bb7f3821f8c3416eea68cd5c1c609fd82954 Mon Sep 17 00:00:00 2001 From: Dave Hendler Date: Tue, 27 Oct 2026 15:36:13 -0700 Subject: [PATCH 5/7] [sim] Align text --- engine/sim/gear_stats.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/engine/sim/gear_stats.cpp b/engine/sim/gear_stats.cpp index 56e8c6ea1c..9eea7b83fd 100644 --- a/engine/sim/gear_stats.cpp +++ b/engine/sim/gear_stats.cpp @@ -55,7 +55,7 @@ void gear_stats_t::add_stat( stat_e stat, case STAT_SHADOW_SPELL_POWER: school_spell_power[ SCHOOL_SHADOW ] += value; break; case STAT_ATTACK_POWER: attack_power += value; break; - case STAT_RANGED_ATTACK_POWER: ranged_attack_power += value; break; + case STAT_RANGED_ATTACK_POWER: ranged_attack_power += value; break; case STAT_EXPERTISE_RATING: expertise_rating += value; break; case STAT_EXPERTISE_RATING2: expertise_rating2 += value; break; @@ -137,7 +137,7 @@ void gear_stats_t::set_stat( stat_e stat, case STAT_SHADOW_SPELL_POWER: school_spell_power[ SCHOOL_SHADOW ] = value; break; case STAT_ATTACK_POWER: attack_power = value; break; - case STAT_RANGED_ATTACK_POWER: ranged_attack_power = value; break; + case STAT_RANGED_ATTACK_POWER: ranged_attack_power = value; break; case STAT_EXPERTISE_RATING: expertise_rating = value; break; case STAT_EXPERTISE_RATING2: expertise_rating2 = value; break; From 0fc11f592429f126102e1bdc3a41e270a417fe21 Mon Sep 17 00:00:00 2001 From: Dave Hendler Date: Tue, 27 Oct 2026 15:36:30 -0700 Subject: [PATCH 6/7] [hunter] TODO that auto shot movement needs to be handled --- engine/class_modules/sc_hunter.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/engine/class_modules/sc_hunter.cpp b/engine/class_modules/sc_hunter.cpp index 8e94246d9d..8e335d7c0a 100644 --- a/engine/class_modules/sc_hunter.cpp +++ b/engine/class_modules/sc_hunter.cpp @@ -99,6 +99,9 @@ struct hunter_auto_shot_t : public hunter_ranged_t bool ready() override { + // TODO: Copied from auto attacks and not correct for hunter. + // Figure out the auto shot timers around movement. Roughly seemed like you could move until about 0.5s before + // the next shot to avoid interruption if ( player->is_moving() ) { return false; From ca01e66497276ba43e332fcaf00ca90818b8d590 Mon Sep 17 00:00:00 2001 From: Dave Hendler Date: Tue, 27 Oct 2026 15:57:02 -0700 Subject: [PATCH 7/7] Add a few more tracking TODOs --- engine/action/action.cpp | 1 + engine/action/attack.cpp | 4 ++-- engine/buff/buff.cpp | 4 ++-- engine/class_modules/sc_hunter.cpp | 6 +++++- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/engine/action/action.cpp b/engine/action/action.cpp index 66096d82f3..14e668af27 100644 --- a/engine/action/action.cpp +++ b/engine/action/action.cpp @@ -1410,6 +1410,7 @@ double action_t::calculate_weapon_damage( double attack_power ) const 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 // 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(); diff --git a/engine/action/attack.cpp b/engine/action/attack.cpp index 73391b02f1..c9066ebd34 100644 --- a/engine/action/attack.cpp +++ b/engine/action/attack.cpp @@ -475,8 +475,8 @@ ranged_attack_t::ranged_attack_t( util::string_view token, player_t* p, const sp : attack_t( token, p, s ) { // ranged attacks cannot be blocked, dodged, or parried - // TODO Forever: can they be glanced? - may_block = may_parry = may_dodge = false; + // TODO Forever: Fairly certain ranged attacks can't be glancing but this needs a bit more verification + may_block = may_parry = may_dodge = may_glance = false; ap_type = attack_power_type::RANGED; } diff --git a/engine/buff/buff.cpp b/engine/buff/buff.cpp index 4502889f37..c677fbb532 100644 --- a/engine/buff/buff.cpp +++ b/engine/buff/buff.cpp @@ -3380,11 +3380,11 @@ stat_buff_t::stat_buff_t( actor_pair_t q, util::string_view name, const spell_da } else if ( effect.subtype() == A_MOD_ATTACK_POWER ) { - s = STAT_ATTACK_POWER; + s = STAT_ATTACK_POWER; } else if ( effect.subtype() == A_MOD_RANGED_ATTACK_POWER ) { - s = STAT_RANGED_ATTACK_POWER; + s = STAT_RANGED_ATTACK_POWER; } else if ( effect.subtype() == A_MOD_INCREASE_HEALTH_2 || effect.subtype() == A_MOD_INCREASE_HEALTH ) { diff --git a/engine/class_modules/sc_hunter.cpp b/engine/class_modules/sc_hunter.cpp index 8e335d7c0a..1cf66839eb 100644 --- a/engine/class_modules/sc_hunter.cpp +++ b/engine/class_modules/sc_hunter.cpp @@ -60,7 +60,7 @@ using hunter_spell_t = hunter_action_t; struct hunter_ranged_attack_t : public hunter_ranged_t { hunter_ranged_attack_t( util::string_view name, player_t* player ) - : hunter_ranged_t( name, player, spell_data_t::nil() ) + : hunter_ranged_t( name, player, player->find_spell( 75 ) ) { weapon = &player->equipped_weapons[ SLOT_RANGED ]; weapon_multiplier = 1.0; @@ -121,9 +121,13 @@ void hunter_t::init_base_stats() player_t::init_base_stats(); base.attack_crit_chance = 0; + // TODO Forever: Only checked on low level characters + base.spell_crit_chance = 0.036; base.stats.attack_power = 2.0 * level() - 20.0; base.stats.ranged_attack_power = 2.0 * level() - 20.0; + + // TODO Forever: Dodge, parry chance } action_t* hunter_t::create_action( util::string_view name, util::string_view options_str )