Skip to content

Commit ba519fb

Browse files
authored
Merge pull request #103 from wowsims/guardian
Update Vengeance for MoP Classic
2 parents 1e09817 + 4d9bd06 commit ba519fb

7 files changed

Lines changed: 102 additions & 96 deletions

File tree

sim/core/utils.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,10 @@ func MasteryRatingToMasteryPoints(masteryRating float64) float64 {
232232
return masteryRating / MasteryRatingPerMasteryPoint
233233
}
234234

235+
func Clamp(val float64, min float64, max float64) float64 {
236+
return math.Max(min, math.Min(val, max))
237+
}
238+
235239
// Gets the spell scaling coefficient associated with a given class
236240
// Retrieved from https://wago.tools/api/casc/1391660?download&branch=wow_classic_beta
237241
func GetClassSpellScalingCoefficient(class proto.Class) float64 {

sim/core/vengeance.go

Lines changed: 91 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

sim/death_knight/blood/blood.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@ func RegisterBloodDeathKnight() {
2828

2929
type BloodDeathKnight struct {
3030
*death_knight.DeathKnight
31-
32-
vengeance *core.VengeanceTracker
3331
}
3432

3533
func NewBloodDeathKnight(character *core.Character, options *proto.Player) *BloodDeathKnight {
@@ -41,7 +39,6 @@ func NewBloodDeathKnight(character *core.Character, options *proto.Player) *Bloo
4139
StartingRunicPower: dkOptions.Options.ClassOptions.StartingRunicPower,
4240
Spec: proto.Spec_SpecBloodDeathKnight,
4341
}, options.TalentsString, 50034),
44-
vengeance: &core.VengeanceTracker{},
4542
}
4643

4744
return bdk
@@ -84,7 +81,7 @@ func (bdk *BloodDeathKnight) ApplyTalents() {
8481
}))
8582

8683
// Vengeance
87-
core.ApplyVengeanceEffect(&bdk.Character, bdk.vengeance, 93099)
84+
bdk.RegisterVengeance(93099, nil)
8885

8986
// Mastery: Blood Shield
9087
shieldAmount := 0.0

sim/druid/guardian/tank.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ func NewGuardianDruid(character *core.Character, options *proto.Player) *Guardia
3131
bear := &GuardianDruid{
3232
Druid: druid.New(character, druid.Bear, selfBuffs, options.TalentsString),
3333
Options: tankOptions.Options,
34-
vengeance: &core.VengeanceTracker{},
3534
}
3635

3736
bear.EnableRageBar(core.RageBarOptions{
@@ -53,8 +52,7 @@ func NewGuardianDruid(character *core.Character, options *proto.Player) *Guardia
5352
type GuardianDruid struct {
5453
*druid.Druid
5554

56-
Options *proto.GuardianDruid_Options
57-
vengeance *core.VengeanceTracker
55+
Options *proto.GuardianDruid_Options
5856

5957
// Aura references
6058
EnrageAura *core.Aura
@@ -75,7 +73,7 @@ func (bear *GuardianDruid) ApplyTalents() {
7573
// bear.Druid.ApplyTalents()
7674
bear.applyMastery()
7775
bear.applyThickHide()
78-
core.ApplyVengeanceEffect(&bear.Character, bear.vengeance, 84840)
76+
bear.RegisterVengeance(84840, bear.BearFormAura)
7977
}
8078

8179
func (bear *GuardianDruid) applyMastery() {

sim/paladin/protection/protection.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ func NewProtectionPaladin(character *core.Character, options *proto.Player) *Pro
3333
prot := &ProtectionPaladin{
3434
Paladin: paladin.NewPaladin(character, options.TalentsString, protOptions.Options.ClassOptions),
3535
Options: protOptions.Options,
36-
vengeance: &core.VengeanceTracker{},
3736
}
3837

3938
return prot
@@ -43,8 +42,6 @@ type ProtectionPaladin struct {
4342
*paladin.Paladin
4443

4544
Options *proto.ProtectionPaladin_Options
46-
47-
vengeance *core.VengeanceTracker
4845
}
4946

5047
func (prot *ProtectionPaladin) GetPaladin() *paladin.Paladin {
@@ -85,7 +82,7 @@ func (prot *ProtectionPaladin) RegisterSpecializationEffects() {
8582
prot.ApplyJudgementsOfTheWise()
8683

8784
// Vengeance
88-
core.ApplyVengeanceEffect(&prot.Character, prot.vengeance, 84839)
85+
prot.RegisterVengeance(84839, nil)
8986
}
9087

9188
func (prot *ProtectionPaladin) RegisterMastery() {

sim/warrior/protection/protection.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ type ProtectionWarrior struct {
2929

3030
Options *proto.ProtectionWarrior_Options
3131

32-
core.VengeanceTracker
33-
3432
shieldSlam *core.Spell
3533
}
3634

@@ -76,7 +74,7 @@ func (war *ProtectionWarrior) RegisterSpecializationEffects() {
7674
war.AddStat(stats.BlockPercent, 15)
7775

7876
// Vengeance
79-
core.ApplyVengeanceEffect(war.GetCharacter(), &war.VengeanceTracker, 93098)
77+
war.RegisterVengeance(93098, war.DefensiveStanceAura)
8078
}
8179

8280
func (war *ProtectionWarrior) RegisterMastery() {

ui/core/components/inputs/buffs_debuffs.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,12 +272,12 @@ export const DEBUFFS_CONFIG = [
272272
},
273273
{
274274
config: DamageReduction,
275-
picker: MultiIconPicker,
275+
picker: IconPicker,
276276
stats: [Stat.StatArmor],
277277
},
278278
{
279279
config: CastSpeedDebuff,
280-
picker: IconPicker,
280+
picker: MultiIconPicker,
281281
stats: [Stat.StatArmor],
282282
},
283283
] as PickerStatOptions[];

0 commit comments

Comments
 (0)