Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 55 additions & 2 deletions engine/action/action.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1391,8 +1391,9 @@ 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();
// 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 =
Expand All @@ -1409,6 +1410,46 @@ 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();

// 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 )
Expand Down Expand Up @@ -1447,6 +1488,12 @@ 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();
}

double init_tick_amount = amount;

if ( !sim->average_range )
Expand Down Expand Up @@ -1521,6 +1568,12 @@ 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();
}

// 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 )
Expand Down
2 changes: 2 additions & 0 deletions engine/action/action.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 4 additions & 3 deletions engine/action/heal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 )
{
Expand Down
1 change: 1 addition & 0 deletions engine/dbc/data_enums.hh
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
21 changes: 18 additions & 3 deletions engine/dbc/sc_item_data.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<unsigned>( 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;
}
Expand All @@ -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 )
Expand Down
88 changes: 87 additions & 1 deletion engine/player/player.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2066,6 +2066,27 @@ void player_t::init_items()

init_meta_gem();

// 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;
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 )
{
Expand Down Expand Up @@ -5678,6 +5699,10 @@ 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;

return cdv;
}

Expand All @@ -5694,6 +5719,10 @@ 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;

return chv;
}

Expand All @@ -5710,6 +5739,10 @@ 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;

return cmv;
}

Expand Down Expand Up @@ -5752,6 +5785,10 @@ 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;

return m;
}

Expand Down Expand Up @@ -5823,6 +5860,13 @@ 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;
m *= pvp_dampening_multiplier;
}

return m;
}

Expand All @@ -5838,6 +5882,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();

// Spell 134735 absorb modifier (subtype A_MOD_ABSORB_RECEIVED_PERCENT) + dampening
if ( sim->pvp.enabled && sim->pvp.stat_scaling )
{
m *= sim->pvp.absorb_received_mod;
m *= pvp_dampening_multiplier;
}

return m;
}

Expand Down Expand Up @@ -6029,6 +6080,34 @@ 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 )
{
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<attribute_e>( 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, 2026-03-20)
else if ( attr == static_cast<attribute_e>( convert_hybrid_stat( STAT_STR_AGI_INT ) ) )
m *= 1.12; // +12% primary stat (Midnight S1, 2026-03-20)
}
}

return m;
}

Expand Down Expand Up @@ -6160,7 +6239,13 @@ 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;

// 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;

return m;
}

double player_t::composite_mastery_value() const
Expand Down Expand Up @@ -6863,6 +6948,7 @@ void player_t::reset()

// Reset current stats to initial stats
current = initial;
pvp_dampening_multiplier = 1.0;

current.sleeping = true;

Expand Down
5 changes: 5 additions & 0 deletions engine/player/player.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<player_scaling_t> scaling;

Expand Down
37 changes: 37 additions & 0 deletions engine/report/json/report_json.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading