From 548b961492357b58532c80903ec7365fb341f895 Mon Sep 17 00:00:00 2001 From: dark room danny <115037920+followingthefasciaplane@users.noreply.github.com> Date: Thu, 19 Mar 2026 23:43:00 +1300 Subject: [PATCH 1/3] pvp --- engine/action/action.cpp | 51 ++++++++++- engine/action/action.hpp | 2 + engine/dbc/data_enums.hh | 1 + engine/dbc/sc_item_data.cpp | 21 ++++- engine/player/player.cpp | 82 ++++++++++++++++- engine/player/player.hpp | 5 ++ engine/report/json/report_json.cpp | 37 ++++++++ engine/report/report_html_player.cpp | 64 +++++++++++++ engine/sim/pvp_scaling.cpp | 100 +++++++++++++++++++++ engine/sim/pvp_scaling.hpp | 74 +++++++++++++++ engine/sim/sim.cpp | 74 +++++++++++++++ engine/sim/sim.hpp | 3 + profiles/tests/pvp_warrior_arms.simc | 45 ++++++++++ profiles/tests/pvp_warrior_arms_honor.simc | 44 +++++++++ source_files/cmake_engine.txt | 2 + source_files/engine_make | 1 + 16 files changed, 600 insertions(+), 6 deletions(-) create mode 100644 engine/sim/pvp_scaling.cpp create mode 100644 engine/sim/pvp_scaling.hpp create mode 100644 profiles/tests/pvp_warrior_arms.simc create mode 100644 profiles/tests/pvp_warrior_arms_honor.simc diff --git a/engine/action/action.cpp b/engine/action/action.cpp index db614fc398a..2a257cab0da 100644 --- a/engine/action/action.cpp +++ b/engine/action/action.cpp @@ -1391,8 +1391,8 @@ double action_t::total_crit_bonus( const action_state_t* state ) const double damage_from_crit_multiplier = composite_player_critical_multiplier( state ); double base_bonus = base_crit_bonus; - if ( sim->pvp_mode ) - base_bonus += sim->pvp_rules->effectN( 3 ).percent(); + if ( sim->pvp.enabled ) + base_bonus += ( sim->pvp.crit_damage_mod - 1.0 ); // applies only to bonus from crit double bonus_mult = @@ -1409,6 +1409,43 @@ double action_t::total_crit_bonus( const action_state_t* state ) const return bonus; } +double action_t::get_pvp_coefficient() const +{ + unsigned sid = data().id(); + + // Priority 1: Effect-level overrides + for ( size_t i = 1; i <= data().effect_count(); i++ ) + { + auto it = sim->pvp.effect_overrides.find( data().effectN( i ).id() ); + if ( it != sim->pvp.effect_overrides.end() ) + return it->second; + } + + // Priority 2: Spell-level override + { + auto it = sim->pvp.coefficient_overrides.find( sid ); + if ( it != sim->pvp.coefficient_overrides.end() ) + return it->second; + } + + // Priority 3: Item bonus coefficients + { + auto it = sim->pvp.item_bonus_coefficients.find( sid ); + if ( it != sim->pvp.item_bonus_coefficients.end() ) + return it->second; + } + + // Priority 4: DBC pvp_coeff on the spell's effects + for ( size_t i = 1; i <= data().effect_count(); i++ ) + { + double c = data().effectN( i ).pvp_coeff(); + if ( c != 0.0 && c != 1.0 ) + return c; + } + + return 1.0; +} + double action_t::calculate_weapon_damage( double attack_power ) const { if ( !weapon || weapon_multiplier <= 0 ) @@ -1447,6 +1484,11 @@ double action_t::calculate_tick_amount( action_state_t* state, double dot_multip amount *= state->composite_ta_multiplier(); amount *= state->composite_rolling_ta_multiplier(); + if ( sim->pvp.enabled && sim->pvp.coefficients ) + { + amount *= get_pvp_coefficient(); + } + double init_tick_amount = amount; if ( !sim->average_range ) @@ -1521,6 +1563,11 @@ double action_t::calculate_direct_amount( action_state_t* state ) const amount *= state->composite_da_multiplier(); + if ( sim->pvp.enabled && sim->pvp.coefficients ) + { + amount *= get_pvp_coefficient(); + } + // damage variation in WoD is based on the delta field in the spell data, applied to entire amount double delta_mod = amount_delta_modifier( state ); if ( !sim->average_range && delta_mod > 0 ) diff --git a/engine/action/action.hpp b/engine/action/action.hpp index f6b5e57ffe7..ef0fa8a1d94 100644 --- a/engine/action/action.hpp +++ b/engine/action/action.hpp @@ -824,6 +824,8 @@ struct action_t : private noncopyable virtual double total_crit_bonus( const action_state_t* /* state */ ) const; // Check if we want to move this into the stateless system. + virtual double get_pvp_coefficient() const; + virtual int num_targets() const; virtual size_t available_targets( std::vector< player_t* >& ) const; diff --git a/engine/dbc/data_enums.hh b/engine/dbc/data_enums.hh index c8b37b7c222..6a78445810f 100644 --- a/engine/dbc/data_enums.hh +++ b/engine/dbc/data_enums.hh @@ -91,6 +91,7 @@ enum item_bonus_type ITEM_BONUS_MOD_ITEM_STAT = 25, // Modify item stat to type ITEM_BONUS_ILEVEL_IN_PVP = 36, // Item has a higher level in PvP context ITEM_BONUS_SET_ILEVEL_2 = 42, // Used in some DF (10.0) crafted items + ITEM_BONUS_SET_ILEVEL_PVP = 43, // Set item level to value_1 when in PvP mode (Midnight+) ITEM_BONUS_SQUISH_CURVE = 48, ITEM_BONUS_SCALE_CONFIG = 49, ITEM_BONUS_APPLY_BONUS = 50, diff --git a/engine/dbc/sc_item_data.cpp b/engine/dbc/sc_item_data.cpp index 7a2dcd22e77..4faa55178bb 100644 --- a/engine/dbc/sc_item_data.cpp +++ b/engine/dbc/sc_item_data.cpp @@ -505,10 +505,23 @@ bool item_database::apply_item_bonus( item_t& item, const item_bonus_entry_t& en case ITEM_BONUS_ILEVEL_IN_PVP: if ( item.sim->pvp_mode ) { - // TODO: Should this be disabled if Midnight scaling bonuses are present? item.parsed.data.level += entry.value_1; } break; + // Midnight+: Set item level to an absolute value in PvP mode + // e.g., bonus_id=13448 → type 43, val_1=289 → ilvl becomes 289 in PvP + // This is a post-squish absolute ilvl, not a pre-squish value + case ITEM_BONUS_SET_ILEVEL_PVP: + if ( item.sim->pvp_mode ) + { + item.sim->print_debug( "Player {} item '{}' PvP ilvl set to {} (was {})", + item.player->name(), item.name(), entry.value_1, item.parsed.data.level ); + item.parsed.data.level = as( entry.value_1 ); + // The PvP ilvl is already a post-squish value (e.g., 289 in Midnight). + // Mark as midnight-scaled to prevent the fallback squish from re-squishing it. + item.parsed.has_midnight_scaling = true; + } + break; default: break; } @@ -528,10 +541,12 @@ void item_database::sort_item_bonuses( item_t& item ) bool a_is_scaling = false; bool b_is_scaling = false; for ( const auto& entry : a_entries ) - if ( entry.type == ITEM_BONUS_POST_SQUISH_ITEM_LEVEL || entry.type == ITEM_BONUS_CRAFTING_QUALITY ) + if ( entry.type == ITEM_BONUS_POST_SQUISH_ITEM_LEVEL || entry.type == ITEM_BONUS_CRAFTING_QUALITY || + entry.type == ITEM_BONUS_SET_ILEVEL_PVP ) a_is_scaling = true; for ( const auto& entry : b_entries ) - if ( entry.type == ITEM_BONUS_POST_SQUISH_ITEM_LEVEL || entry.type == ITEM_BONUS_CRAFTING_QUALITY ) + if ( entry.type == ITEM_BONUS_POST_SQUISH_ITEM_LEVEL || entry.type == ITEM_BONUS_CRAFTING_QUALITY || + entry.type == ITEM_BONUS_SET_ILEVEL_PVP ) b_is_scaling = true; if ( a_is_scaling != b_is_scaling ) diff --git a/engine/player/player.cpp b/engine/player/player.cpp index 0ec3efedb75..563d82bdb99 100644 --- a/engine/player/player.cpp +++ b/engine/player/player.cpp @@ -2066,6 +2066,26 @@ void player_t::init_items() init_meta_gem(); + // Detect PvP trinket 2-set (Gladiator's Distinction, set ID 1458, spell 365043) + if ( sim->pvp.enabled && sim->pvp.trinket_bonus ) + { + constexpr unsigned GLADIATORS_DISTINCTION_SET_ID = 1458; + constexpr unsigned GLADIATORS_DISTINCTION_SPELL_ID = 365043; + int pvp_trinket_count = 0; + for ( auto slot : { SLOT_TRINKET_1, SLOT_TRINKET_2 } ) + { + if ( items[ slot ].parsed.data.id_set == GLADIATORS_DISTINCTION_SET_ID ) + pvp_trinket_count++; + } + if ( pvp_trinket_count >= 2 ) + { + pvp_trinket_2pc_active = true; + pvp_trinket_spell = find_spell( GLADIATORS_DISTINCTION_SPELL_ID ); + sim->print_debug( "Player {} has Gladiator's Distinction 2pc active (spell {})", + name(), GLADIATORS_DISTINCTION_SPELL_ID ); + } + } + // Needs to be initialized after old set bonus system if ( sets != nullptr ) { @@ -5678,6 +5698,9 @@ double player_t::composite_damage_versatility() const if ( buffs.dmf_well_fed ) cdv += buffs.dmf_well_fed->check_value(); + if ( sim->pvp.enabled && sim->pvp.stat_scaling ) + cdv *= sim->pvp.versatility_damage_mod; + return cdv; } @@ -5694,6 +5717,9 @@ double player_t::composite_heal_versatility() const if ( buffs.dmf_well_fed ) chv += buffs.dmf_well_fed->check_value(); + if ( sim->pvp.enabled && sim->pvp.stat_scaling ) + chv *= sim->pvp.versatility_healing_mod; + return chv; } @@ -5710,6 +5736,9 @@ double player_t::composite_mitigation_versatility() const if ( buffs.dmf_well_fed ) cmv += buffs.dmf_well_fed->check_value() / 2; + if ( sim->pvp.enabled && sim->pvp.stat_scaling ) + cmv *= sim->pvp.versatility_dr_mod; + return cmv; } @@ -5752,6 +5781,9 @@ double player_t::composite_player_pet_damage_multiplier( const action_state_t*, { double m = guardian ? current.guardian_damage_multiplier : current.pet_damage_multiplier; + if ( sim->pvp.enabled && sim->pvp.stat_scaling ) + m *= sim->pvp.pet_damage_mod; + return m; } @@ -5823,6 +5855,12 @@ double player_t::composite_player_heal_multiplier( const action_state_t* ) const if ( buffs.entropic_embrace && buffs.entropic_embrace->check() ) m *= 1.0 + buffs.entropic_embrace->data().effectN( 3 ).percent(); + if ( sim->pvp.enabled && sim->pvp.stat_scaling ) + { + m *= sim->pvp.healing_received_mod; + m *= pvp_dampening_multiplier; + } + return m; } @@ -5838,6 +5876,13 @@ double player_t::composite_player_absorb_multiplier( const action_state_t* ) con if ( buffs.entropic_embrace && buffs.entropic_embrace->check() ) m *= 1.0 + buffs.entropic_embrace->data().effectN( 4 ).percent(); + if ( sim->pvp.enabled && sim->pvp.stat_scaling ) + { + // absorb_received_mod from spell 134735 effect (subtype 422, currently 0 = no change) + m *= sim->pvp.absorb_received_mod; + m *= pvp_dampening_multiplier; + } + return m; } @@ -6029,6 +6074,35 @@ double player_t::composite_attribute_multiplier( attribute_e attr ) const m *= 1.0 + b->check_stack_value(); } + if ( sim->pvp.enabled && sim->pvp.stat_scaling ) + { + if ( attr == ATTR_STAMINA ) + m *= sim->pvp.stamina_mod; + } + + if ( sim->pvp.enabled && sim->pvp.trinket_bonus && pvp_trinket_2pc_active ) + { + // Gladiator's Distinction (set 1458, spell 365043): + // Effect 1 = +12% primary stat (Str/Agi/Int) + // Effect 2 = +5% stamina + // Spell 365043 may not be in extracted data — use spell effects if available, + // otherwise fall back to hardcoded values for trinket 2-set bonus + if ( pvp_trinket_spell && pvp_trinket_spell->ok() && pvp_trinket_spell->effect_count() >= 2 ) + { + if ( attr == ATTR_STAMINA ) + m *= 1.0 + pvp_trinket_spell->effectN( 2 ).percent(); + else if ( attr == static_cast( convert_hybrid_stat( STAT_STR_AGI_INT ) ) ) + m *= 1.0 + pvp_trinket_spell->effectN( 1 ).percent(); + } + else + { + if ( attr == ATTR_STAMINA ) + m *= 1.05; // +5% stamina (Midnight S1) + else if ( attr == static_cast( convert_hybrid_stat( STAT_STR_AGI_INT ) ) ) + m *= 1.12; // +12% primary stat (Midnight S1) + } + } + return m; } @@ -6160,7 +6234,12 @@ double player_t::composite_player_target_armor( player_t* t ) const double player_t::composite_mitigation_multiplier( school_e /* school */ ) const { - return 1.0; + double m = 1.0; + + if ( sim->pvp.enabled && sim->pvp.stat_scaling ) + m *= sim->pvp.resistance_mod; + + return m; } double player_t::composite_mastery_value() const @@ -6863,6 +6942,7 @@ void player_t::reset() // Reset current stats to initial stats current = initial; + pvp_dampening_multiplier = 1.0; current.sleeping = true; diff --git a/engine/player/player.hpp b/engine/player/player.hpp index 4a59cd4a3e7..e1e5ffe2d4c 100644 --- a/engine/player/player.hpp +++ b/engine/player/player.hpp @@ -465,6 +465,11 @@ struct player_t : public actor_t double auto_attack_base_modifier; double auto_attack_multiplier; + // PvP scaling fields + double pvp_dampening_multiplier = 1.0; + bool pvp_trinket_2pc_active = false; + const spell_data_t* pvp_trinket_spell = nullptr; + // Scale Factors std::unique_ptr scaling; diff --git a/engine/report/json/report_json.cpp b/engine/report/json/report_json.cpp index 6f0c287413c..42c85fa4fc2 100644 --- a/engine/report/json/report_json.cpp +++ b/engine/report/json/report_json.cpp @@ -1211,6 +1211,43 @@ void to_json( const ::report::json::report_configuration_t& report_configuration options_root[ "challenge_mode" ] = sim.challenge_mode; options_root[ "timewalk" ] = sim.timewalk; options_root[ "pvp_mode" ] = sim.pvp_mode; + + if ( sim.pvp.enabled ) + { + auto pvp_root = options_root[ "pvp" ]; + pvp_root[ "mode" ] = sim.pvp.mode; + + // Enabled systems as individual booleans + pvp_root[ "coefficients_enabled" ] = sim.pvp.coefficients; + pvp_root[ "trinket_bonus_enabled" ] = sim.pvp.trinket_bonus; + pvp_root[ "stat_scaling_enabled" ] = sim.pvp.stat_scaling; + pvp_root[ "item_scaling_enabled" ] = sim.pvp.item_scaling; + pvp_root[ "tier_penalty_enabled" ] = sim.pvp.tier_penalty; + + // Global modifiers + auto mods = pvp_root[ "global_modifiers" ]; + mods[ "crit_damage_mod" ] = sim.pvp.crit_damage_mod; + mods[ "healing_received_mod" ] = sim.pvp.healing_received_mod; + mods[ "absorb_done_mod" ] = sim.pvp.absorb_done_mod; + mods[ "absorb_received_mod" ] = sim.pvp.absorb_received_mod; + mods[ "pet_damage_mod" ] = sim.pvp.pet_damage_mod; + mods[ "mana_regen_mod" ] = sim.pvp.mana_regen_mod; + mods[ "resistance_mod" ] = sim.pvp.resistance_mod; + mods[ "versatility_damage_mod" ] = sim.pvp.versatility_damage_mod; + mods[ "versatility_healing_mod" ] = sim.pvp.versatility_healing_mod; + mods[ "versatility_dr_mod" ] = sim.pvp.versatility_dr_mod; + mods[ "tier_set_effectiveness" ] = sim.pvp.tier_set_effectiveness; + + // Dampening + auto damp = pvp_root[ "dampening" ]; + damp[ "enabled" ] = sim.pvp.dampening_enabled; + damp[ "start_sec" ] = sim.pvp.dampening_start_sec; + damp[ "final_stacks" ] = sim.pvp_dampening_stacks; + damp[ "final_reduction_pct" ] = std::min( + sim.pvp_dampening_stacks * sim.pvp.dampening_pct_per_stack, + sim.pvp.dampening_max_pct ); + } + options_root[ "rng" ] = sim.rng(); options_root[ "deterministic" ] = sim.deterministic; options_root[ "average_range" ] = sim.average_range; diff --git a/engine/report/report_html_player.cpp b/engine/report/report_html_player.cpp index 0f16114cc98..11d7c8843fa 100644 --- a/engine/report/report_html_player.cpp +++ b/engine/report/report_html_player.cpp @@ -4931,6 +4931,68 @@ void print_html_player_deaths( report::sc_html_stream& os, const player_t& p, } } +// print_html_pvp_section =================================================== + +void print_html_pvp_section( report::sc_html_stream& os, const player_t& p ) +{ + const sim_t& sim = *p.sim; + if ( !sim.pvp.enabled ) + return; + + os << "
\n" + << "

PvP Scaling

\n" + << "
\n" + << "\n" + << "\n" + << "\n"; + + os << "\n"; + os << "\n"; + os << "\n"; + os << "\n"; + os << "\n"; + os << "\n"; + os << "\n"; + os << "\n"; + + if ( sim.pvp.dampening_enabled ) + { + double final_reduction = std::min( + sim.pvp_dampening_stacks * sim.pvp.dampening_pct_per_stack, + sim.pvp.dampening_max_pct ); + os << "\n"; + } + + os << "
ModifierValue
Mode" << sim.pvp.mode << "
Crit Damage Mod" << fmt::format( "{:.2f}", sim.pvp.crit_damage_mod ) << "
Healing Received Mod" << fmt::format( "{:.2f}", sim.pvp.healing_received_mod ) << "
Absorb Done Mod" << fmt::format( "{:.2f}", sim.pvp.absorb_done_mod ) << "
Pet Damage Mod" << fmt::format( "{:.2f}", sim.pvp.pet_damage_mod ) << "
Mana Regen Mod" << fmt::format( "{:.2f}", sim.pvp.mana_regen_mod ) << "
Versatility Effectiveness" << fmt::format( "{:.2f}", sim.pvp.versatility_damage_mod ) << "
Tier Set Effectiveness" << fmt::format( "{:.2f}", sim.pvp.tier_set_effectiveness ) << "
Dampening (final)" + << fmt::format( "{:.1f}% ({} stacks)", final_reduction * 100.0, sim.pvp_dampening_stacks ) + << "
\n"; + + // Per-ability PvP coefficients table + os << "

Ability PvP Coefficients

\n" + << "\n" + << "\n" + << "\n"; + + for ( const auto* a : p.action_list ) + { + if ( !a->data().ok() ) + continue; + for ( size_t i = 1; i <= a->data().effect_count(); i++ ) + { + double c = a->data().effectN( i ).pvp_coeff(); + if ( c != 0.0 && c != 1.0 ) + { + os << "\n"; + break; + } + } + } + + os << "
AbilityPvP Coefficient
" << report_decorators::decorated_action( *a ) + << "" << fmt::format( "{:.4f}", c ) << "
\n" + << "
\n"; +} + // print_html_player_ ======================================================= void print_html_player_( report::sc_html_stream& os, const player_t& p ) @@ -4967,6 +5029,8 @@ void print_html_player_( report::sc_html_stream& os, const player_t& p ) print_html_stats( os, p ); + print_html_pvp_section( os, p ); + print_html_gear( os, p ); print_html_talents( os, p ); diff --git a/engine/sim/pvp_scaling.cpp b/engine/sim/pvp_scaling.cpp new file mode 100644 index 00000000000..593f1b88339 --- /dev/null +++ b/engine/sim/pvp_scaling.cpp @@ -0,0 +1,100 @@ +#include "sim/pvp_scaling.hpp" + +#include "sim/sim.hpp" +#include "dbc/spell_data.hpp" +#include "util/util.hpp" + +namespace pvp +{ + +void parse_coefficient_overrides( pvp_config_t& pvp ) +{ + if ( pvp.coefficient_override_str.empty() ) + return; + + auto entries = util::string_split( pvp.coefficient_override_str, "/" ); + for ( auto entry : entries ) + { + if ( entry.empty() ) + continue; + + bool is_effect = entry[ 0 ] == 'e' || entry[ 0 ] == 'E'; + if ( is_effect ) + entry.remove_prefix( 1 ); + + auto parts = util::string_split( entry, ":" ); + if ( parts.size() != 2 ) + continue; + + unsigned id = util::to_unsigned( parts[ 0 ] ); + double coeff = std::stod( std::string( parts[ 1 ] ) ); + + if ( is_effect ) + pvp.effect_overrides[ id ] = coeff; + else + pvp.coefficient_overrides[ id ] = coeff; + } +} + +void init_format_defaults( pvp_config_t& pvp, bool user_set_dampening_start ) +{ + if ( pvp.mode == "arena" ) + { + if ( !user_set_dampening_start ) + pvp.dampening_start_sec = 0.0; + } + else if ( pvp.mode == "bg" ) + { + pvp.dampening_enabled = false; + } + else if ( pvp.mode == "wargame" ) + { + if ( !user_set_dampening_start ) + pvp.dampening_start_sec = 300.0; + } +} + +void init_modifiers_from_spell( pvp_config_t& pvp, const spell_data_t* pvp_rules, sim_t* sim ) +{ + if ( !pvp_rules || !pvp_rules->ok() ) + return; + + if ( pvp_rules->effect_count() != 11 && sim ) + sim->print_debug( "PvP: spell 134735 has {} effects (expected 11)", pvp_rules->effect_count() ); + + for ( size_t i = 1; i <= pvp_rules->effect_count(); i++ ) + { + const auto& e = pvp_rules->effectN( i ); + switch ( e.subtype() ) + { + case A_MOD_HEALING_RECEIVED_PCT: + pvp.healing_received_mod = 1.0 + e.percent(); + break; + case A_MOD_ABSORB_RECEIVED_PERCENT: + pvp.absorb_received_mod = 1.0 + e.percent(); + break; + case A_448: // "Mod Crit Healing %" — reduces crit bonus damage/healing by 50% + pvp.crit_damage_mod = 1.0 + e.percent(); + break; + case A_MOD_RESILIENCE: + if ( e.base_value() != 0 ) + pvp.crit_damage_mod = 1.0 + e.percent(); + break; + case A_MOD_MANA_REGEN_PCT: + pvp.mana_regen_mod = 1.0 + e.percent(); + break; + case A_MOD_PET_DAMAGE_DONE: + pvp.pet_damage_mod = 1.0 + e.percent(); + break; + case A_MOD_RESISTANCE_PCT: + pvp.resistance_mod = 1.0 + e.percent(); + break; + default: + if ( sim ) + sim->print_debug( "PvP: Unhandled spell 134735 effect {} subtype {}", i, static_cast( e.subtype() ) ); + break; + } + } +} + +} // namespace pvp diff --git a/engine/sim/pvp_scaling.hpp b/engine/sim/pvp_scaling.hpp new file mode 100644 index 00000000000..4f04290e98b --- /dev/null +++ b/engine/sim/pvp_scaling.hpp @@ -0,0 +1,74 @@ +// engine/sim/pvp_scaling.hpp +#pragma once + +#include +#include + +// Forward declarations +struct sim_t; +struct spell_data_t; + +namespace pvp +{ + +struct pvp_config_t +{ + bool enabled = false; + std::string mode = "arena"; + + // Sub-toggles + bool coefficients = true; + bool trinket_bonus = true; + bool stat_scaling = true; + bool item_scaling = true; + bool tier_penalty = true; + + // Spell 134735 effects (populated by subtype iteration at init) + double healing_received_mod = 1.0; + double absorb_received_mod = 1.0; + double absorb_done_mod = 1.0; + double crit_damage_mod = 1.0; + double mana_regen_mod = 1.0; + double pet_damage_mod = 1.0; + double resistance_mod = 1.0; + + // Versatility PvP effectiveness + double versatility_damage_mod = 1.0; + double versatility_healing_mod = 1.0; + double versatility_dr_mod = 1.0; + + // Stamina / Primary Stat + double stamina_mod = 1.0; + double primary_stat_mod = 1.0; + + // Rating + double rating_multiplier = 1.0; + + // Tier set + double tier_set_effectiveness = 0.67; + + // Dampening (spell 110310) + bool dampening_enabled = true; + double dampening_start_sec = 300.0; + double dampening_stack_interval = 10.0; + double dampening_pct_per_stack = 0.01; + double dampening_max_pct = 1.0; + + // Coefficient overrides + std::string coefficient_override_str; + std::unordered_map coefficient_overrides; + std::unordered_map effect_overrides; + std::unordered_map item_bonus_coefficients; +}; + +// Parse "53:0.90/e280:0.85" into coefficient_overrides and effect_overrides maps +void parse_coefficient_overrides( pvp_config_t& pvp ); + +// Set format-specific defaults (dampening timing, etc.) +// user_set_dampening_start: true if user explicitly provided pvp_dampening_start option +void init_format_defaults( pvp_config_t& pvp, bool user_set_dampening_start = false ); + +// Populate pvp_config_t fields from spell 134735 effects by subtype +void init_modifiers_from_spell( pvp_config_t& pvp, const spell_data_t* pvp_rules, sim_t* sim ); + +} // namespace pvp diff --git a/engine/sim/sim.cpp b/engine/sim/sim.cpp index 8cf95dd350b..cd08725849d 100644 --- a/engine/sim/sim.cpp +++ b/engine/sim/sim.cpp @@ -1472,6 +1472,8 @@ sim_t::sim_t() use_item_verification( true ), pvp_rules(), pvp_mode( false ), + pvp(), + pvp_dampening_stacks( 0 ), auto_attacks_always_land( false ), log_spell_id( true ), active_enemies( 0 ), @@ -1832,6 +1834,37 @@ void sim_t::reset() raid_event_t::reset( this ); } +struct dampening_event_t : public event_t +{ + sim_t& sim; + + dampening_event_t( sim_t& s, timespan_t delay ) + : event_t( s, delay ), sim( s ) + { + } + + const char* name() const override + { return "pvp_dampening"; } + + void execute() override + { + sim.pvp_dampening_stacks++; + double reduction = std::min( + sim.pvp_dampening_stacks * sim.pvp.dampening_pct_per_stack, + sim.pvp.dampening_max_pct ); + + for ( auto* p : sim.player_no_pet_list ) + p->pvp_dampening_multiplier = 1.0 - reduction; + + // Stop scheduling if at max + if ( reduction < sim.pvp.dampening_max_pct ) + { + make_event( sim, sim, + timespan_t::from_seconds( sim.pvp.dampening_stack_interval ) ); + } + } +}; + /// Start combat. void sim_t::combat_begin() { @@ -1950,6 +1983,13 @@ void sim_t::combat_begin() make_event( *this, *this, timespan_t::from_millis( rng().range( 1, 5249 ) ) ); raid_event_t::combat_begin( this ); + + if ( pvp.enabled && pvp.dampening_enabled ) + { + pvp_dampening_stacks = 0; + make_event( *this, *this, + timespan_t::from_seconds( pvp.dampening_start_sec ) ); + } } // sim_t::combat_end ======================================================== @@ -2749,7 +2789,21 @@ void sim_t::init() } if ( pvp_mode ) + { + pvp.enabled = true; + pvp_rules = dbc::find_spell( this, 134735 ); + pvp::init_modifiers_from_spell( pvp, pvp_rules, this ); + pvp::init_format_defaults( pvp, pvp.dampening_start_sec != 300.0 ); + pvp::parse_coefficient_overrides( pvp ); + } + if ( pvp.enabled && !pvp_mode ) + { + pvp_mode = true; pvp_rules = dbc::find_spell( this, 134735 ); + pvp::init_modifiers_from_spell( pvp, pvp_rules, this ); + pvp::init_format_defaults( pvp, pvp.dampening_start_sec != 300.0 ); + pvp::parse_coefficient_overrides( pvp ); + } // set scaling metric if ( !scaling->scale_over.empty() ) @@ -3827,6 +3881,26 @@ void sim_t::create_options() add_option( opt_string( "enable_2_set", enable_2_set ) ); add_option( opt_string( "enable_4_set", enable_4_set ) ); add_option( opt_bool( "pvp", pvp_mode ) ); + add_option( opt_string( "pvp_mode", pvp.mode ) ); + add_option( opt_bool( "pvp_coefficients", pvp.coefficients ) ); + add_option( opt_bool( "pvp_trinket_bonus", pvp.trinket_bonus ) ); + add_option( opt_bool( "pvp_stat_scaling", pvp.stat_scaling ) ); + add_option( opt_bool( "pvp_item_scaling", pvp.item_scaling ) ); + add_option( opt_bool( "pvp_tier_penalty", pvp.tier_penalty ) ); + add_option( opt_bool( "pvp_dampening", pvp.dampening_enabled ) ); + add_option( opt_float( "pvp_dampening_start", pvp.dampening_start_sec ) ); + add_option( opt_float( "pvp_tier_effectiveness", pvp.tier_set_effectiveness ) ); + add_option( opt_float( "pvp_vers_damage_mod", pvp.versatility_damage_mod ) ); + add_option( opt_float( "pvp_vers_healing_mod", pvp.versatility_healing_mod ) ); + add_option( opt_float( "pvp_vers_dr_mod", pvp.versatility_dr_mod ) ); + add_option( opt_float( "pvp_healing_mod", pvp.healing_received_mod ) ); + add_option( opt_float( "pvp_absorb_mod", pvp.absorb_done_mod ) ); + add_option( opt_float( "pvp_absorb_received_mod", pvp.absorb_received_mod ) ); + add_option( opt_float( "pvp_crit_damage_mod", pvp.crit_damage_mod ) ); + add_option( opt_float( "pvp_pet_damage_mod", pvp.pet_damage_mod ) ); + add_option( opt_float( "pvp_mana_regen_mod", pvp.mana_regen_mod ) ); + add_option( opt_float( "pvp_resistance_mod", pvp.resistance_mod ) ); + add_option( opt_string( "pvp_coefficient_override", pvp.coefficient_override_str ) ); add_option( opt_bool( "auto_attacks_always_land", auto_attacks_always_land ) ); add_option( opt_bool( "log_spell_id", log_spell_id ) ); add_option( opt_int( "desired_targets", desired_targets ) ); diff --git a/engine/sim/sim.hpp b/engine/sim/sim.hpp index 30826b303aa..a7e87e363f2 100644 --- a/engine/sim/sim.hpp +++ b/engine/sim/sim.hpp @@ -11,6 +11,7 @@ #include "progress_bar.hpp" #include "sim_ostream.hpp" #include "sim/option.hpp" +#include "sim/pvp_scaling.hpp" #include "util/concurrency.hpp" #include "util/rng.hpp" #include "util/sample_data.hpp" @@ -180,6 +181,8 @@ struct sim_t : private sc_thread_t std::string enable_4_set; // Enables all 4 set bonuses for the tier that this is set as const spell_data_t* pvp_rules; // Hidden aura that contains the PvP crit damage reduction bool pvp_mode; // Enables PvP mode - reduces crit damage, adjusts PvP gear iLvl + pvp::pvp_config_t pvp; + int pvp_dampening_stacks = 0; bool auto_attacks_always_land; /// Allow Auto Attacks (white attacks) to always hit the enemy bool log_spell_id; // Add spell data ids to log/debug output where available. (actions, buffs) diff --git a/profiles/tests/pvp_warrior_arms.simc b/profiles/tests/pvp_warrior_arms.simc new file mode 100644 index 00000000000..b109e3b3587 --- /dev/null +++ b/profiles/tests/pvp_warrior_arms.simc @@ -0,0 +1,45 @@ +# Galactic Gladiator Arms Warrior PvP Test Profile +# Full Conquest gear, Level 90 Midnight Season 1 +# +# Bonus ID 13448: Conquest PvP scaling (type 43, sets ilvl to 289 in PvP) +# Bonus ID 13452: Conquest stat template (type 7/23) +# Base item level: 197 (scales to 289 in PvP via bonus_id=13448) + +warrior="PvP_Warrior_Arms" +source=default +spec=arms +level=90 +race=human +role=attack +position=back +talents=CcEAAAAAAAAAAAAAAAAAAAAAAAzMzsMzMzMDAAAghphxYmxyMzMzgxMDAAAAgZWmZAZMWWGYBMgZYCZGsBMjNz2YwMGgZGAmxwA + +# PvP Mode — Arena +pvp=1 +pvp_mode=arena + +# No consumables in arena +potion=disabled +flask=disabled +food=disabled +augmentation=disabled +temporary_enchant=disabled + +# Gear: Full Galactic Gladiator Conquest Set (Plate - Warrior) +# bonus_id=13448 triggers ITEM_BONUS_SET_ILEVEL_PVP (type 43) → ilvl 289 in PvP +# bonus_id=13452 applies conquest stat template +head=galactic_gladiators_plate_helm,id=255594,bonus_id=13448/13452 +neck=galactic_gladiators_necklace,id=255610,bonus_id=13448/13452 +shoulder=galactic_gladiators_plate_shoulders,id=255598,bonus_id=13448/13452 +back=galactic_gladiators_cloak,id=255604,bonus_id=13448/13452 +chest=galactic_gladiators_chestplate,id=255589,bonus_id=13448/13452 +wrist=galactic_gladiators_plate_wristguards,id=255602,bonus_id=13448/13452 +hands=galactic_gladiators_plate_gauntlets,id=255592,bonus_id=13448/13452 +waist=galactic_gladiators_plate_girdle,id=255600,bonus_id=13448/13452 +legs=galactic_gladiators_plate_legguards,id=255596,bonus_id=13448/13452 +feet=galactic_gladiators_plate_warboots,id=255590,bonus_id=13448/13452 +finger1=galactic_gladiators_ring,id=255607,bonus_id=13448/13452 +finger2=galactic_gladiators_band,id=255608,bonus_id=13448/13452 +trinket1=galactic_gladiators_badge_of_ferocity,id=255613,bonus_id=13448/13452 +trinket2=galactic_gladiators_emblem,id=255615,bonus_id=13448/13452 +main_hand=galactic_gladiators_greatsword,id=255633,bonus_id=13448/13452 diff --git a/profiles/tests/pvp_warrior_arms_honor.simc b/profiles/tests/pvp_warrior_arms_honor.simc new file mode 100644 index 00000000000..ddbb74e588e --- /dev/null +++ b/profiles/tests/pvp_warrior_arms_honor.simc @@ -0,0 +1,44 @@ +# Galactic Aspirant Arms Warrior PvP Test Profile +# Full Honor gear, Level 90 Midnight Season 1 +# +# Bonus ID 13447: Honor PvP scaling (type 43, sets ilvl to 276 in PvP) +# Bonus ID 13452: PvP stat template +# Base item level: 197 (scales to 276 in PvP via bonus_id=13447) + +warrior="PvP_Warrior_Arms_Honor" +source=default +spec=arms +level=90 +race=human +role=attack +position=back +talents=CcEAAAAAAAAAAAAAAAAAAAAAAAzMzsMzMzMDAAAghphxYmxyMzMzgxMDAAAAgZWmZAZMWWGYBMgZYCZGsBMjNz2YwMGgZGAmxwA + +# PvP Mode — Arena +pvp=1 +pvp_mode=arena + +# No consumables in arena +potion=disabled +flask=disabled +food=disabled +augmentation=disabled +temporary_enchant=disabled + +# Gear: Full Galactic Aspirant Honor Set (Plate - Warrior) +# bonus_id=13447 triggers ITEM_BONUS_SET_ILEVEL_PVP (type 43) → ilvl 276 in PvP +head=galactic_aspirants_plate_helm,id=255262,bonus_id=13447/13452 +neck=galactic_aspirants_necklace,id=255334,bonus_id=13447/13452 +shoulder=galactic_aspirants_plate_shoulders,id=255283,bonus_id=13447/13452 +back=galactic_aspirants_cloak,id=255338,bonus_id=13447/13452 +chest=galactic_aspirants_chestplate,id=255264,bonus_id=13447/13452 +wrist=galactic_aspirants_plate_cuffs,id=255291,bonus_id=13447/13452 +hands=galactic_aspirants_plate_gauntlets,id=255272,bonus_id=13447/13452 +waist=galactic_aspirants_plate_girdle,id=255286,bonus_id=13447/13452 +legs=galactic_aspirants_plate_legguards,id=255278,bonus_id=13447/13452 +feet=galactic_aspirants_plate_warboots,id=255267,bonus_id=13447/13452 +finger1=galactic_aspirants_ring,id=255331,bonus_id=13447/13452 +finger2=galactic_aspirants_band,id=255332,bonus_id=13447/13452 +trinket1=galactic_aspirants_badge_of_ferocity,id=255326,bonus_id=13447/13452 +trinket2=galactic_aspirants_emblem,id=255329,bonus_id=13447/13452 +main_hand=galactic_aspirants_greatsword,id=255346,bonus_id=13447/13452 diff --git a/source_files/cmake_engine.txt b/source_files/cmake_engine.txt index fd1baa55343..2f2645667e7 100644 --- a/source_files/cmake_engine.txt +++ b/source_files/cmake_engine.txt @@ -157,6 +157,7 @@ sim/proc.hpp sim/proc_rng.hpp sim/profileset.hpp sim/progress_bar.hpp +sim/pvp_scaling.hpp sim/raid_event.hpp sim/reforge_plot.hpp sim/scale_factor_control.hpp @@ -346,6 +347,7 @@ sim/proc.cpp sim/proc_rng.cpp sim/profileset.cpp sim/progress_bar.cpp +sim/pvp_scaling.cpp sim/raid_event.cpp sim/reforge_plot.cpp sim/scale_factor_control.cpp diff --git a/source_files/engine_make b/source_files/engine_make index 196f2b756d9..fc20b670f54 100644 --- a/source_files/engine_make +++ b/source_files/engine_make @@ -158,6 +158,7 @@ SRC += \ sim$(PATHSEP)proc_rng.cpp \ sim$(PATHSEP)profileset.cpp \ sim$(PATHSEP)progress_bar.cpp \ + sim$(PATHSEP)pvp_scaling.cpp \ sim$(PATHSEP)raid_event.cpp \ sim$(PATHSEP)reforge_plot.cpp \ sim$(PATHSEP)scale_factor_control.cpp \ From 8b06b180ecf4c84153bee642638b7ef4d18475d9 Mon Sep 17 00:00:00 2001 From: Arma Date: Thu, 19 Mar 2026 01:00:56 -0700 Subject: [PATCH 2/3] [Warrior] Fix apex (#11248) --- engine/class_modules/sc_warrior.cpp | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/engine/class_modules/sc_warrior.cpp b/engine/class_modules/sc_warrior.cpp index 1801c453eb9..e275a0c58a2 100644 --- a/engine/class_modules/sc_warrior.cpp +++ b/engine/class_modules/sc_warrior.cpp @@ -1144,8 +1144,6 @@ struct warrior_action_t : public parse_action_effects_t // Shared // Arms - parse_target_effects( d_fn( &warrior_td_t::debuffs_colossus_smash ), - p()->spell.colossus_smash_debuff ); // Fury @@ -1665,9 +1663,13 @@ struct warrior_attack_t : public warrior_action_t { double m = base_t::composite_target_multiplier( target ); auto target_data = td( target ); - if ( p()->talents.arms.master_of_warfare_3.ok() && target_data && - target_data->debuffs_colossus_smash->up() && p()->buff.heroic_might->up() ) - m *= 1.0 + ( p()->buff.heroic_might->stack_value() / 100 ); + if ( target_data && target_data->debuffs_colossus_smash->up() ) + { + auto multi = 1.0 + p()->spell.colossus_smash_debuff->effectN( 1 ).percent(); + if ( p()->talents.arms.master_of_warfare_3.ok() && p()->buff.heroic_might->up() ) + multi += p()->buff.heroic_might->stack_value() / 100; + m *= multi; + } return m; } @@ -4106,6 +4108,13 @@ struct execute_damage_t : public warrior_attack_t if( p()->talents.arms.fatality.ok() && td( state->target )->debuffs_fatal_mark->check() ) p()->active.fatality->execute_on_target( state->target ); + + if ( p()->talents.arms.master_of_warfare_1.ok() && !p()->buff.master_of_warfare_proc->up() && + p()->rng().roll( master_of_warfare_proc_chance * ++p()->master_of_warfare_attempts_since_last_proc ) ) + { + p()->buff.master_of_warfare_proc->trigger(); + p()->master_of_warfare_attempts_since_last_proc = 0; + } } }; @@ -4233,13 +4242,6 @@ struct execute_arms_t : public warrior_attack_t if ( p()->talents.arms.executioners_precision.ok() ) p()->buff.executioners_precision->trigger(); - - if ( !background && p()->talents.arms.master_of_warfare_1.ok() && !p()->buff.master_of_warfare_proc->up() && - p()->rng().roll( master_of_warfare_proc_chance * ++p()->master_of_warfare_attempts_since_last_proc ) ) - { - p()->buff.master_of_warfare_proc->trigger(); - p()->master_of_warfare_attempts_since_last_proc = 0; - } } bool target_ready( player_t* candidate_target ) override @@ -8139,7 +8141,8 @@ void warrior_t::create_buffs() ->set_stack_behavior( buff_stack_behavior::ASYNCHRONOUS ); buff.heroic_might_accumulator = make_buff( this, "heroic_might_accumulator", find_spell( 1292058 ) ); - buff.heroic_might = make_buff( this, "heroic_might", find_spell( 1292058 ) ); + buff.heroic_might = make_buff( this, "heroic_might", find_spell( 1292058 ) ) + ->set_default_value_from_effect( 1 ); // Protection Apex buff.phalanx = make_buff( this, "phalanx", spell.phalanx_buff ); From a86919af1d5b636f21fb94613dedde4f3d642bc7 Mon Sep 17 00:00:00 2001 From: dark room danny <115037920+followingthefasciaplane@users.noreply.github.com> Date: Fri, 20 Mar 2026 01:25:34 +1300 Subject: [PATCH 3/3] Documentation for PvP scaling Added comments --- engine/action/action.cpp | 6 ++ engine/action/heal.cpp | 7 ++- engine/player/player.cpp | 24 +++++--- engine/sim/pvp_scaling.cpp | 7 +++ engine/sim/pvp_scaling.hpp | 118 +++++++++++++++++++++++++------------ engine/sim/sim.cpp | 7 ++- 6 files changed, 118 insertions(+), 51 deletions(-) diff --git a/engine/action/action.cpp b/engine/action/action.cpp index 2a257cab0da..e91dac7a4db 100644 --- a/engine/action/action.cpp +++ b/engine/action/action.cpp @@ -1391,6 +1391,7 @@ double action_t::total_crit_bonus( const action_state_t* state ) const double damage_from_crit_multiplier = composite_player_critical_multiplier( state ); double base_bonus = base_crit_bonus; + // Spell 134735 effect 3 (subtype A_448): -50% crit bonus in PvP (2026-03-20) if ( sim->pvp.enabled ) base_bonus += ( sim->pvp.crit_damage_mod - 1.0 ); @@ -1409,6 +1410,9 @@ double action_t::total_crit_bonus( const action_state_t* state ) const return bonus; } +// Returns the PvP damage/healing multiplier for this spell. +// Each spell effect has a pvp_coeff field in the DBC data that scales its value in PvP combat. +// User overrides take priority over DBC values. (2026-03-20) double action_t::get_pvp_coefficient() const { unsigned sid = data().id(); @@ -1484,6 +1488,7 @@ double action_t::calculate_tick_amount( action_state_t* state, double dot_multip amount *= state->composite_ta_multiplier(); amount *= state->composite_rolling_ta_multiplier(); + // Apply per-spell PvP coefficient from DBC pvp_coeff or user overrides if ( sim->pvp.enabled && sim->pvp.coefficients ) { amount *= get_pvp_coefficient(); @@ -1563,6 +1568,7 @@ double action_t::calculate_direct_amount( action_state_t* state ) const amount *= state->composite_da_multiplier(); + // Apply per-spell PvP coefficient from DBC pvp_coeff or user overrides if ( sim->pvp.enabled && sim->pvp.coefficients ) { amount *= get_pvp_coefficient(); diff --git a/engine/action/heal.cpp b/engine/action/heal.cpp index 18b48445920..5de8d5c7762 100644 --- a/engine/action/heal.cpp +++ b/engine/action/heal.cpp @@ -113,14 +113,15 @@ double heal_t::total_crit_bonus( const action_state_t* state ) const double damage_from_crit_multiplier = composite_player_critical_multiplier( state ); double base_bonus = base_crit_bonus; - if ( sim->pvp_mode ) - base_bonus += sim->pvp_rules->effectN( 3 ).percent(); + // Spell 134735 effect 3 (subtype A_448): -50% crit bonus in PvP (2026-03-20) + if ( sim->pvp.enabled ) + base_bonus += ( sim->pvp.crit_damage_mod - 1.0 ); // applies only to bonus from crit double bonus_mult = composite_crit_damage_bonus_multiplier() * composite_target_crit_damage_bonus_multiplier( state->target ); // for healing, 'multiplier' is additive with base bonus - double bonus = ( base_crit_bonus + damage_from_crit_multiplier - 1.0 ) * bonus_mult; + double bonus = ( base_bonus + damage_from_crit_multiplier - 1.0 ) * bonus_mult; if ( sim->debug ) { diff --git a/engine/player/player.cpp b/engine/player/player.cpp index 563d82bdb99..fca3b8d029b 100644 --- a/engine/player/player.cpp +++ b/engine/player/player.cpp @@ -2066,7 +2066,8 @@ void player_t::init_items() init_meta_gem(); - // Detect PvP trinket 2-set (Gladiator's Distinction, set ID 1458, spell 365043) + // Detect PvP trinket 2-set: Gladiator's Distinction (set 1458, spell 365043). + // Grants +12% primary stat and +5% stamina when two PvP trinkets are equipped. (2026-03-20) if ( sim->pvp.enabled && sim->pvp.trinket_bonus ) { constexpr unsigned GLADIATORS_DISTINCTION_SET_ID = 1458; @@ -5698,6 +5699,7 @@ double player_t::composite_damage_versatility() const if ( buffs.dmf_well_fed ) cdv += buffs.dmf_well_fed->check_value(); + // PvP versatility override (default 1.0 = no change, user-configurable) if ( sim->pvp.enabled && sim->pvp.stat_scaling ) cdv *= sim->pvp.versatility_damage_mod; @@ -5717,6 +5719,7 @@ double player_t::composite_heal_versatility() const if ( buffs.dmf_well_fed ) chv += buffs.dmf_well_fed->check_value(); + // PvP versatility override (default 1.0 = no change, user-configurable) if ( sim->pvp.enabled && sim->pvp.stat_scaling ) chv *= sim->pvp.versatility_healing_mod; @@ -5736,6 +5739,7 @@ double player_t::composite_mitigation_versatility() const if ( buffs.dmf_well_fed ) cmv += buffs.dmf_well_fed->check_value() / 2; + // PvP versatility override (default 1.0 = no change, user-configurable) if ( sim->pvp.enabled && sim->pvp.stat_scaling ) cmv *= sim->pvp.versatility_dr_mod; @@ -5781,6 +5785,7 @@ double player_t::composite_player_pet_damage_multiplier( const action_state_t*, { double m = guardian ? current.guardian_damage_multiplier : current.pet_damage_multiplier; + // Spell 134735 effect (subtype A_MOD_PET_DAMAGE_DONE): pet damage modifier in PvP if ( sim->pvp.enabled && sim->pvp.stat_scaling ) m *= sim->pvp.pet_damage_mod; @@ -5855,6 +5860,7 @@ double player_t::composite_player_heal_multiplier( const action_state_t* ) const if ( buffs.entropic_embrace && buffs.entropic_embrace->check() ) m *= 1.0 + buffs.entropic_embrace->data().effectN( 3 ).percent(); + // Spell 134735 healing modifier + dampening (spell 110310) reduction over time if ( sim->pvp.enabled && sim->pvp.stat_scaling ) { m *= sim->pvp.healing_received_mod; @@ -5876,9 +5882,9 @@ double player_t::composite_player_absorb_multiplier( const action_state_t* ) con if ( buffs.entropic_embrace && buffs.entropic_embrace->check() ) m *= 1.0 + buffs.entropic_embrace->data().effectN( 4 ).percent(); + // Spell 134735 absorb modifier (subtype A_MOD_ABSORB_RECEIVED_PERCENT) + dampening if ( sim->pvp.enabled && sim->pvp.stat_scaling ) { - // absorb_received_mod from spell 134735 effect (subtype 422, currently 0 = no change) m *= sim->pvp.absorb_received_mod; m *= pvp_dampening_multiplier; } @@ -6074,19 +6080,18 @@ double player_t::composite_attribute_multiplier( attribute_e attr ) const m *= 1.0 + b->check_stack_value(); } + // PvP stamina override (default 1.0 = no change, user-configurable) if ( sim->pvp.enabled && sim->pvp.stat_scaling ) { if ( attr == ATTR_STAMINA ) m *= sim->pvp.stamina_mod; } + // Gladiator's Distinction 2pc (set 1458, spell 365043): + // Effect 1 = +12% primary stat (Str/Agi/Int), Effect 2 = +5% stamina + // Falls back to hardcoded Midnight S1 values if spell data unavailable. (2026-03-20) if ( sim->pvp.enabled && sim->pvp.trinket_bonus && pvp_trinket_2pc_active ) { - // Gladiator's Distinction (set 1458, spell 365043): - // Effect 1 = +12% primary stat (Str/Agi/Int) - // Effect 2 = +5% stamina - // Spell 365043 may not be in extracted data — use spell effects if available, - // otherwise fall back to hardcoded values for trinket 2-set bonus if ( pvp_trinket_spell && pvp_trinket_spell->ok() && pvp_trinket_spell->effect_count() >= 2 ) { if ( attr == ATTR_STAMINA ) @@ -6097,9 +6102,9 @@ double player_t::composite_attribute_multiplier( attribute_e attr ) const else { if ( attr == ATTR_STAMINA ) - m *= 1.05; // +5% stamina (Midnight S1) + m *= 1.05; // +5% stamina (Midnight S1, 2026-03-20) else if ( attr == static_cast( convert_hybrid_stat( STAT_STR_AGI_INT ) ) ) - m *= 1.12; // +12% primary stat (Midnight S1) + m *= 1.12; // +12% primary stat (Midnight S1, 2026-03-20) } } @@ -6236,6 +6241,7 @@ double player_t::composite_mitigation_multiplier( school_e /* school */ ) const { double m = 1.0; + // Spell 134735 effect (subtype A_MOD_RESISTANCE_PCT): armor/resistance modifier in PvP if ( sim->pvp.enabled && sim->pvp.stat_scaling ) m *= sim->pvp.resistance_mod; diff --git a/engine/sim/pvp_scaling.cpp b/engine/sim/pvp_scaling.cpp index 593f1b88339..f9a8c7d0f6e 100644 --- a/engine/sim/pvp_scaling.cpp +++ b/engine/sim/pvp_scaling.cpp @@ -7,6 +7,8 @@ namespace pvp { +// Parse user-provided PvP coefficient override string. +// Format: "spell_id:multiplier/espell_effect_id:multiplier" ('e' prefix = effect-level) void parse_coefficient_overrides( pvp_config_t& pvp ) { if ( pvp.coefficient_override_str.empty() ) @@ -36,6 +38,8 @@ void parse_coefficient_overrides( pvp_config_t& pvp ) } } +// Set dampening defaults based on PvP format. Arena starts dampening immediately, +// battlegrounds have no dampening, wargames start at 5 minutes. (2026-03-20) void init_format_defaults( pvp_config_t& pvp, bool user_set_dampening_start ) { if ( pvp.mode == "arena" ) @@ -54,6 +58,9 @@ void init_format_defaults( pvp_config_t& pvp, bool user_set_dampening_start ) } } +// Iterate spell 134735 effects by aura subtype and populate pvp_config_t fields. +// As of 12.0.1 the spell has 11 effects; only effect 3 (A_448, crit -50%) is non-zero. +// Subtypes 42/119/240 are skipped (proc trigger, PvP state check, expertise). (2026-03-20) void init_modifiers_from_spell( pvp_config_t& pvp, const spell_data_t* pvp_rules, sim_t* sim ) { if ( !pvp_rules || !pvp_rules->ok() ) diff --git a/engine/sim/pvp_scaling.hpp b/engine/sim/pvp_scaling.hpp index 4f04290e98b..7dd9b0a5a96 100644 --- a/engine/sim/pvp_scaling.hpp +++ b/engine/sim/pvp_scaling.hpp @@ -1,4 +1,31 @@ -// engine/sim/pvp_scaling.hpp +// engine/sim/pvp_scaling.hpp — PvP combat modifier configuration for WoW 12.0.1 (Midnight). +// +// Centralizes all PvP-specific scaling into pvp_config_t, which is stored on sim_t as sim->pvp. +// Enabled via the existing pvp=1 option or by setting any pvp_* option directly. +// +// Architecture: +// sim_t::init() loads spell 134735 ("PvP Rules Enabled") and calls +// pvp::init_modifiers_from_spell() to populate config fields from DBC aura subtypes. +// pvp::init_format_defaults() then adjusts dampening timing for the chosen format. +// pvp::parse_coefficient_overrides() parses any user-provided per-spell overrides. +// +// Where modifiers are applied (see diff against midnight branch): +// action.cpp — crit bonus reduction (total_crit_bonus), per-spell pvp_coeff +// multiplier in calculate_direct_amount / calculate_tick_amount, +// new virtual get_pvp_coefficient() with 4-priority lookup. +// heal.cpp — crit bonus reduction for heals (total_crit_bonus). +// player.cpp — versatility (damage/heal/DR), pet damage, healing received, +// absorb received, dampening, stamina, resistance, Gladiator's +// Distinction 2pc (set 1458 / spell 365043) attribute bonuses. +// sim.cpp — dampening_event_t (spell 110310), PvP init in sim_t::init(), +// dampening scheduling in combat_begin(), all pvp_* options. +// sc_item_data — ITEM_BONUS_SET_ILEVEL_PVP (type 43) for Midnight PvP ilvl. +// report_* — HTML section and JSON block for PvP modifier reporting. +// +// All modifier defaults are 1.0 (no change). DBC data is authoritative; fields that +// are 0 in the DBC (everything except crit_damage_mod) resolve to 1.0. Users can +// override any value via pvp_* sim options. Tier set reduction and versatility +// effectiveness are handled by DBC pvp_coeff on the spells themselves. (2026-03-20) #pragma once #include @@ -14,61 +41,76 @@ namespace pvp struct pvp_config_t { bool enabled = false; - std::string mode = "arena"; - - // Sub-toggles - bool coefficients = true; - bool trinket_bonus = true; - bool stat_scaling = true; - bool item_scaling = true; - bool tier_penalty = true; - - // Spell 134735 effects (populated by subtype iteration at init) - double healing_received_mod = 1.0; - double absorb_received_mod = 1.0; - double absorb_done_mod = 1.0; - double crit_damage_mod = 1.0; - double mana_regen_mod = 1.0; - double pet_damage_mod = 1.0; - double resistance_mod = 1.0; - - // Versatility PvP effectiveness + std::string mode = "arena"; // "arena", "bg", or "wargame" + + // Sub-toggles for individual PvP systems (all default on). + // Each gates a block of logic in the engine; disable to isolate behavior. + bool coefficients = true; // per-spell DBC pvp_coeff multipliers (action.cpp) + bool trinket_bonus = true; // Gladiator's Distinction 2pc detection (player.cpp) + bool stat_scaling = true; // global stat modifiers from spell 134735 (player.cpp) + bool item_scaling = true; // PvP item level via bonus type 43 (sc_item_data.cpp) + bool tier_penalty = true; // tier set effectiveness (DBC pvp_coeff on set spells) + + // Spell 134735 effects — populated from DBC by aura subtype at init. + // As of 12.0.1 only crit_damage_mod (A_448, effect 3) is non-zero (-50%). + // All others are 0 in DBC but can be hotfixed by Blizzard without a patch. + // Remaining effects skipped: A_PROC_TRIGGER_SPELL (42), A_CHECK_PVP_STATE (119), + // A_MOD_EXPERTISE (240), subtype 504 — none are gameplay-relevant to SimC. + double healing_received_mod = 1.0; // A_MOD_HEALING_RECEIVED_PCT (118) + double absorb_received_mod = 1.0; // A_MOD_ABSORB_RECEIVED_PERCENT (422) + double absorb_done_mod = 1.0; // outgoing absorb override (no DBC source, user-only) + double crit_damage_mod = 1.0; // A_448 (448) — set to 0.50 from DBC at runtime + double mana_regen_mod = 1.0; // A_MOD_MANA_REGEN_PCT (379) + double pet_damage_mod = 1.0; // A_MOD_PET_DAMAGE_DONE (429) + double resistance_mod = 1.0; // A_MOD_RESISTANCE_PCT (101) + + // Versatility PvP effectiveness (user-configurable override, default = full). + // Not sourced from spell 134735; set manually if Blizzard reduces vers in PvP. double versatility_damage_mod = 1.0; double versatility_healing_mod = 1.0; double versatility_dr_mod = 1.0; - // Stamina / Primary Stat + // Stamina / Primary Stat (user-configurable override) double stamina_mod = 1.0; double primary_stat_mod = 1.0; - // Rating + // Rating (user-configurable override) double rating_multiplier = 1.0; - // Tier set - double tier_set_effectiveness = 0.67; + // Tier set effectiveness — reporting/override only. Actual reduction is applied + // via DBC pvp_coeff on the tier set bonus spells through get_pvp_coefficient(). + double tier_set_effectiveness = 1.0; - // Dampening (spell 110310) + // Dampening (spell 110310): progressive healing/absorb reduction. + // Implemented as dampening_event_t in sim.cpp, applied in player.cpp via + // pvp_dampening_multiplier on composite_player_heal_multiplier and + // composite_player_absorb_multiplier. + // Arena: starts at 0s. Wargame: starts at 300s. BG: disabled. (2026-03-20) bool dampening_enabled = true; - double dampening_start_sec = 300.0; - double dampening_stack_interval = 10.0; - double dampening_pct_per_stack = 0.01; - double dampening_max_pct = 1.0; - - // Coefficient overrides + double dampening_start_sec = 300.0; // overridden to 0.0 for arena format + double dampening_stack_interval = 10.0; // seconds between stacks + double dampening_pct_per_stack = 0.01; // 1% per stack + double dampening_max_pct = 1.0; // 100% max reduction + + // Per-spell coefficient overrides — format: "spell_id:coeff/eeffect_id:coeff" + // Parsed by parse_coefficient_overrides(). Looked up in get_pvp_coefficient() + // (action.cpp) with priority: effect override > spell override > item bonus > DBC. std::string coefficient_override_str; - std::unordered_map coefficient_overrides; - std::unordered_map effect_overrides; - std::unordered_map item_bonus_coefficients; + std::unordered_map coefficient_overrides; // spell-level + std::unordered_map effect_overrides; // effect-level (prefix 'e') + std::unordered_map item_bonus_coefficients; // from item bonus data }; -// Parse "53:0.90/e280:0.85" into coefficient_overrides and effect_overrides maps +// Parse user override string into coefficient_overrides and effect_overrides maps. +// Format: "53:0.90/e280:0.85" — 'e' prefix targets effect IDs, bare IDs target spells. void parse_coefficient_overrides( pvp_config_t& pvp ); -// Set format-specific defaults (dampening timing, etc.) -// user_set_dampening_start: true if user explicitly provided pvp_dampening_start option +// Set dampening defaults based on PvP format. Called after init_modifiers_from_spell. +// user_set_dampening_start: true if user explicitly provided pvp_dampening_start option. void init_format_defaults( pvp_config_t& pvp, bool user_set_dampening_start = false ); -// Populate pvp_config_t fields from spell 134735 effects by subtype +// Iterate spell 134735 effects by aura subtype and populate pvp_config_t fields. +// Called from sim_t::init(). Logs unhandled subtypes at debug level. void init_modifiers_from_spell( pvp_config_t& pvp, const spell_data_t* pvp_rules, sim_t* sim ); } // namespace pvp diff --git a/engine/sim/sim.cpp b/engine/sim/sim.cpp index cd08725849d..2510c6e63f2 100644 --- a/engine/sim/sim.cpp +++ b/engine/sim/sim.cpp @@ -1834,6 +1834,9 @@ void sim_t::reset() raid_event_t::reset( this ); } +// Dampening (spell 110310): periodic healing/absorb reduction in PvP arena. +// +1% reduction per stack every 10s, up to 100%. Starts immediately in arena, +// at 300s in wargames, disabled in battlegrounds. (2026-03-20) struct dampening_event_t : public event_t { sim_t& sim; @@ -1856,7 +1859,6 @@ struct dampening_event_t : public event_t for ( auto* p : sim.player_no_pet_list ) p->pvp_dampening_multiplier = 1.0 - reduction; - // Stop scheduling if at max if ( reduction < sim.pvp.dampening_max_pct ) { make_event( sim, sim, @@ -2788,6 +2790,9 @@ void sim_t::init() scale_itemlevel_down_only = true; } + // Initialize PvP subsystem from spell 134735 ("PvP Rules Enabled"). + // Either pvp=1 or pvp.enabled=true triggers full init. Both paths kept + // in sync so the legacy "pvp" option and the new config are interchangeable. if ( pvp_mode ) { pvp.enabled = true;