@@ -7,98 +7,110 @@ import (
77 "github.com/wowsims/mop/sim/core/stats"
88)
99
10- type VengeanceTracker struct {
11- eligibleDamage float64
12- apBonus float64
13- prevAPBonus float64
14- recentMaxAPBonus float64
15- lastAttackedTime time.Duration // timestamp that the character was last attacked
16- }
10+ const VengeanceScaling = 0.018 // Might be reverted to 0.015 in a later patch
11+
12+ func (character * Character ) RegisterVengeance (spellID int32 , requiredAura * Aura ) {
13+ // First register the exposed Vengeance buff Aura, which we will model
14+ // as discrete stacks with 1 AP granted per stack for ease of tracking
15+ // in the timeline and APLs.
16+ buffAura := MakeStackingAura (character , StackingStatAura {
17+ Aura : Aura {
18+ Label : "Vengeance" ,
19+ ActionID : ActionID {SpellID : spellID },
20+ Duration : time .Second * 20 ,
21+ MaxStacks : math .MaxInt32 ,
22+ },
1723
18- const (
19- VengeanceAPDecayRate = 0.1 // AP bonus decays by 10% every 2 seconds, or 5% if the character has been hit in that time
20- OutcomeVengeanceTriggers = OutcomeLanded
21- )
24+ BonusPerStack : stats.Stats {stats .AttackPower : 1 },
25+ })
2226
23- func Clamp (val float64 , min float64 , max float64 ) float64 {
24- return math .Max (min , math .Min (val , max ))
25- }
27+ // Then set up the proc trigger.
28+ vengeanceTrigger := ProcTrigger {
29+ Name : "Vengeance Trigger" ,
30+ Callback : CallbackOnSpellHitTaken ,
31+
32+ Handler : func (sim * Simulation , spell * Spell , result * SpellResult ) {
33+ // Check that the caster is an NPC.
34+ if spell .Unit .Type != EnemyUnit {
35+ return
36+ }
2637
27- func UpdateVengeance (sim * Simulation , character * Character , tracker * VengeanceTracker , aura * Aura ) {
28- // Save the current AP bonus so we can apply the new buff correctly
29- tracker .prevAPBonus = tracker .apBonus
38+ // Vengeance uses pre-outcome, pre-mitigation damage.
39+ rawDamage := result .PreOutcomeDamage / result .ResistanceMultiplier
3040
31- // If this character has been attacked in the last 2 seconds, apply half decay and add new damage to buff
32- timeSinceLastHit := sim .CurrentTime - tracker .lastAttackedTime
33- if timeSinceLastHit < time .Second * 2 {
41+ // The Weakened Blows debuff does not reduce Vengeance gains.
42+ // TODO: The game similarly hardcodes a correction for Demoralizing Banner, add that in once we implement the debuff in the sim.
43+ if (spell .SpellSchool == SpellSchoolPhysical ) && spell .Unit .GetAura ("Weakened Blows" ).IsActive () {
44+ rawDamage /= 0.9
45+ }
3446
35- // Decay existing bonus by half of the rate
36- decay := VengeanceAPDecayRate / 2
37- tracker .apBonus -= (decay * tracker .apBonus )
47+ // Note that result.PreOutcomeDamage does not include the impact of the tank's various DamageTakenMultiplier PseudoStats.
48+ // By default this is the desired behavior, since it means that tank DRs are automatically divided out in the calculation.
49+ // However, *detrimental* contributions to the relevant DamageTakenMultiplier PseudoStats *do* increase Vengeance gains in-game.
50+ // This can be relevant on certain bosses, such as Ignite Armor stacks increasing Vengeance gains on Iron Juggernaut in SoO.
51+ // TODO: Find a simple way to keep track of only detrimental contributions to DamageTakenMultiplier (and school-specific variants) with minimal overhead.
3852
39- // Add 5% of damage taken in the last 2 seconds
40- tracker . apBonus += 0.05 * tracker . eligibleDamage
53+ // Apply baseline scaling to the raw damage value.
54+ rawVengeance := VengeanceScaling * rawDamage
4155
42- // 4.3.0 change: the vengeance AP buff is always at least 33% of the incoming
43- // damage if the tank has been hit in the last 2 seconds
44- baseAPBonus := tracker .eligibleDamage / 3.0
45- tracker .apBonus = math .Max (tracker .apBonus , baseAPBonus )
46- } else {
47- // No hits in the last 2 seconds - apply full decay
48- tracker .apBonus -= (VengeanceAPDecayRate * tracker .recentMaxAPBonus )
49- }
56+ // Spells that are not mitigated by armor generate 2.5x more Vengeance.
57+ if (spell .SpellSchool != SpellSchoolPhysical ) || spell .Flags .Matches (SpellFlagIgnoreResists ) {
58+ rawVengeance *= 2.5
59+ }
5060
51- // Vengeance tooltip is wrong in sake of simplicity as stated by blizzard
52- // Actual formula used is Stamina + 10% of Base HP
53- apBonusMax := character .GetStat (stats .Stamina ) + 0.1 * character .baseStats [stats .Health ]
54- tracker .apBonus = Clamp (tracker .apBonus , 0 , apBonusMax )
61+ // TODO: Is the 0.5x Vengeance multiplier for non-periodic AoE spells still a thing for the new version of Vengeance in Classic?
5562
56- tracker . recentMaxAPBonus = math . Max ( tracker . apBonus , tracker . recentMaxAPBonus )
63+ // TODO: Weapon-based specials may be normalizing out spell.DamageMultiplier as well?
5764
58- if sim .Log != nil {
59- character .Log (sim , "Updated Vengeance for %s: Eligible Damage(%f) | AP Bonus(%f)" , character .Name , tracker .eligibleDamage , tracker .apBonus )
60- }
65+ // If the buff Aura is currently active, then perform decaying average with previous Vengeance.
66+ newVengeance := rawVengeance
6167
62- tracker .eligibleDamage = 0
68+ if buffAura .IsActive () {
69+ newVengeance += float64 (buffAura .GetStacks ()) * buffAura .RemainingDuration (sim ).Seconds () / buffAura .Duration .Seconds ()
70+ }
6371
64- // Update character stats
65- character .AddStatDynamic (sim , stats .AttackPower , - tracker .prevAPBonus )
66- character .AddStatDynamic (sim , stats .AttackPower , tracker .apBonus )
67- }
72+ // Compare to minimum ramp-up Vengeance value based on equilibrium estimate.
73+ var inferredAttackInterval time.Duration
6874
69- // To use: add a VengeanceTracker member to your spec-specific struct (e.g ProtWarrior, BloodDeathKnight, etc) then call this
70- // with your class's specific Vengeance spell ID
71- func ApplyVengeanceEffect (character * Character , tracker * VengeanceTracker , spellID int32 ) {
72- vengAura := MakePermanent (character .RegisterAura (Aura {
73- Label : "Vengeance" ,
74- Duration : NeverExpires ,
75- ActionID : ActionID {SpellID : spellID }, // Different specs use different spell IDs even though the effect is the same
76- OnSpellHitTaken : func (aura * Aura , sim * Simulation , spell * Spell , result * SpellResult ) {
77- if result .Outcome .Matches (OutcomeVengeanceTriggers ) {
78- // Vengeance is based on the taken damage amount after mitigation
79- // TODO: check how this treats dodge/parry/miss
80- // https://worldofwarcraft.blizzard.com/en-us/news/1293873/tanking-with-a-vengeance seems to suggest a string of dodges will let it fall off
81- // but simc's implementation retriggers vengeance on _any_ attack, even dodge/parry/miss.
82- // I can't find any patch notes or other resources that support one or the other though
83- tracker .lastAttackedTime = sim .CurrentTime
84- tracker .eligibleDamage += result .Damage
75+ if spell .IsMH () {
76+ // TODO: Is this supposed to be the base speed prior to attack speed multipliers?
77+ inferredAttackInterval = spell .Unit .AutoAttacks .MainhandSwingSpeed ()
78+ } else if spell .IsOH () {
79+ inferredAttackInterval = spell .Unit .AutoAttacks .OffhandSwingSpeed ()
80+ } else {
81+ inferredAttackInterval = time .Minute
8582 }
83+
84+ // TODO: Does this also need the 2.5x multiplier for spells and the 0.5x AoE multiplier in it?
85+ inferredEquilibriumVengeance := VengeanceScaling * rawDamage * buffAura .Duration .Seconds () / inferredAttackInterval .Seconds ()
86+
87+ if newVengeance < 0.5 * inferredEquilibriumVengeance {
88+ if sim .Log != nil {
89+ result .Target .Log (sim , "Triggered Vengeance ramp-up mechanism because newVengeance = %.1f and inferredEquilibriumVengeance = %.1f ." , newVengeance , inferredEquilibriumVengeance )
90+ }
91+
92+ newVengeance = 0.5 * inferredEquilibriumVengeance
93+ }
94+
95+ // Apply HP cap.
96+ newVengeance = min (newVengeance , result .Target .MaxHealth ())
97+
98+ if sim .Log != nil {
99+ result .Target .Log (sim , "Updated Vengeance for %s due to %s from %s. Raw damage value = %.1f, raw Vengeance contribution = %.1f, new Vengeance value = %.1f ." , result .Target .Label , spell .ActionID , spell .Unit .Label , rawDamage , rawVengeance , newVengeance )
100+ }
101+
102+ // Activate or refresh the buff Aura and set stacks.
103+ buffAura .Activate (sim )
104+ buffAura .SetStacks (sim , int32 (math .Round (newVengeance )))
86105 },
87- }))
88-
89- // Vengeance "ticks" every 2 seconds to update the AP buff
90- character .RegisterResetEffect (func (sim * Simulation ) {
91- // Reset values
92- tracker .prevAPBonus = 0
93- tracker .apBonus = 0
94- tracker .eligibleDamage = 0
95- tracker .recentMaxAPBonus = 0
96-
97- StartPeriodicAction (sim , PeriodicActionOptions {
98- Period : time .Second * 2 ,
99- OnAction : func (sim * Simulation ) {
100- UpdateVengeance (sim , character , tracker , vengAura )
101- },
102- })
103- })
106+ }
107+
108+ // Finally, either create a new hidden Aura for the Vengeance trigger,
109+ // or attach it to the supplied parent Aura (Bear Form for Druids,
110+ // Defensive Stance for Warriors).
111+ if requiredAura == nil {
112+ MakeProcTriggerAura (& character .Unit , vengeanceTrigger )
113+ } else {
114+ requiredAura .AttachProcTrigger (vengeanceTrigger )
115+ }
104116}
0 commit comments