diff --git a/sim/core/focus.go b/sim/core/focus.go new file mode 100644 index 0000000000..8bc979b50f --- /dev/null +++ b/sim/core/focus.go @@ -0,0 +1,138 @@ +package core + +import ( + "time" + + "github.com/wowsims/wotlk/sim/core/proto" +) + +// Time between focus ticks. +const MaxFocus = 100.0 +const tickDuration = time.Second * 1 +const BaseFocusPerTick = 5.0 + +// OnFocusGain is called any time focus is increased. +type OnFocusGain func(sim *Simulation) + +type focusBar struct { + unit *Unit + + focusPerTick float64 + + currentFocus float64 + + onFocusGain OnFocusGain + tickAction *PendingAction +} + +func (unit *Unit) EnableFocusBar(regenMultiplier float64, onFocusGain OnFocusGain) { + unit.focusBar = focusBar{ + unit: unit, + focusPerTick: BaseFocusPerTick * regenMultiplier, + onFocusGain: onFocusGain, + } +} + +func (unit *Unit) HasFocusBar() bool { + return unit.focusBar.unit != nil +} + +func (fb *focusBar) CurrentFocus() float64 { + return fb.currentFocus +} + +func (fb *focusBar) AddFocus(sim *Simulation, amount float64, actionID ActionID) { + if amount < 0 { + panic("Trying to add negative focus!") + } + + newFocus := MinFloat(fb.currentFocus+amount, MaxFocus) + + if sim.Log != nil { + fb.unit.Log(sim, "Gained %0.3f focus from %s (%0.3f --> %0.3f).", amount, actionID, fb.currentFocus, newFocus) + } + + fb.currentFocus = newFocus + + if fb.onFocusGain != nil { + fb.onFocusGain(sim) + } +} + +func (fb *focusBar) SpendFocus(sim *Simulation, amount float64, metrics *ResourceMetrics) { + if amount < 0 { + panic("Trying to spend negative focus!") + } + + newFocus := fb.currentFocus - amount + metrics.AddEvent(-amount, -amount) + + if sim.Log != nil { + fb.unit.Log(sim, "Spent %0.3f focus from %s (%0.3f --> %0.3f).", amount, metrics.ActionID, fb.currentFocus, newFocus) + } + + fb.currentFocus = newFocus +} + +func (fb *focusBar) reset(sim *Simulation) { + if fb.unit == nil { + return + } + + fb.currentFocus = MaxFocus + + pa := &PendingAction{ + Priority: ActionPriorityRegen, + NextActionAt: tickDuration, + } + pa.OnAction = func(sim *Simulation) { + fb.AddFocus(sim, fb.focusPerTick, ActionID{OtherID: proto.OtherAction_OtherActionFocusRegen}) + + pa.NextActionAt = sim.CurrentTime + tickDuration + sim.AddPendingAction(pa) + } + fb.tickAction = pa + sim.AddPendingAction(pa) +} + +func (fb *focusBar) Cancel(sim *Simulation) { + if fb.tickAction != nil { + fb.tickAction.Cancel(sim) + fb.tickAction = nil + } +} + +type FocusCostOptions struct { + Cost float64 +} +type FocusCost struct { + Refund float64 + ResourceMetrics *ResourceMetrics +} + +func newFocusCost(spell *Spell, options FocusCostOptions) *FocusCost { + spell.DefaultCast.Cost = options.Cost + + return &FocusCost{ + ResourceMetrics: spell.Unit.NewFocusMetrics(spell.ActionID), + } +} + +func (fc *FocusCost) MeetsRequirement(spell *Spell) bool { + spell.CurCast.Cost = MaxFloat(0, spell.CurCast.Cost*spell.Unit.PseudoStats.CostMultiplier) + return spell.Unit.CurrentFocus() >= spell.CurCast.Cost +} +func (fc *FocusCost) LogCostFailure(sim *Simulation, spell *Spell) { + spell.Unit.Log(sim, + "Failed casting %s, not enough focus. (Current Focus = %0.03f, Focus Cost = %0.03f)", + spell.ActionID, spell.Unit.CurrentFocus(), spell.CurCast.Cost) +} +func (fc *FocusCost) SpendCost(sim *Simulation, spell *Spell) { + spell.Unit.SpendFocus(sim, spell.CurCast.Cost, fc.ResourceMetrics) +} +func (fc *FocusCost) IssueRefund(sim *Simulation, spell *Spell) { +} + +func (spell *Spell) FocusMetrics() *ResourceMetrics { + return spell.Cost.(*FocusCost).ResourceMetrics +} diff --git a/sim/core/pet.go b/sim/core/pet.go index 05ae83c0f6..5cef9816ae 100644 --- a/sim/core/pet.go +++ b/sim/core/pet.go @@ -204,6 +204,7 @@ func (pet *Pet) Disable(sim *Simulation) { } pet.CancelGCDTimer(sim) + pet.focusBar.Cancel(sim) pet.AutoAttacks.CancelAutoSwing(sim) pet.enabled = false pet.DoNothing() // mark it is as doing nothing now. diff --git a/sim/core/spell.go b/sim/core/spell.go index 947f6e8faf..14a1edc7f6 100644 --- a/sim/core/spell.go +++ b/sim/core/spell.go @@ -25,6 +25,7 @@ type SpellConfig struct { EnergyCost EnergyCostOptions RageCost RageCostOptions RuneCost RuneCostOptions + FocusCost FocusCostOptions Cast CastConfig ExtraCastCondition CanCastCondition @@ -218,6 +219,8 @@ func (unit *Unit) RegisterSpell(config SpellConfig) *Spell { spell.Cost = newRageCost(spell, config.RageCost) } else if config.RuneCost.BloodRuneCost != 0 || config.RuneCost.FrostRuneCost != 0 || config.RuneCost.UnholyRuneCost != 0 || config.RuneCost.RunicPowerCost != 0 || config.RuneCost.RunicPowerGain != 0 { spell.Cost = newRuneCost(spell, config.RuneCost) + } else if config.FocusCost.Cost != 0 { + spell.Cost = newFocusCost(spell, config.FocusCost) } spell.createDots(config.Dot, false) diff --git a/sim/core/unit.go b/sim/core/unit.go index bd748f9bdb..9efea249c3 100644 --- a/sim/core/unit.go +++ b/sim/core/unit.go @@ -96,6 +96,7 @@ type Unit struct { manaBar rageBar energyBar + focusBar RunicPowerBar // All spells that can be cast by this unit. @@ -434,6 +435,7 @@ func (unit *Unit) reset(sim *Simulation, agent Agent) { } unit.manaBar.reset() + unit.focusBar.reset(sim) unit.healthBar.reset(sim) unit.UpdateManaRegenRates() diff --git a/sim/hunter/focus.go b/sim/hunter/focus.go deleted file mode 100644 index 5b32aef900..0000000000 --- a/sim/hunter/focus.go +++ /dev/null @@ -1,99 +0,0 @@ -package hunter - -import ( - "time" - - "github.com/wowsims/wotlk/sim/core" - "github.com/wowsims/wotlk/sim/core/proto" -) - -// Time between focus ticks. -const MaxFocus = 100.0 -const tickDuration = time.Second * 1 -const BaseFocusPerTick = 5.0 - -// OnFocusGain is called any time focus is increased. -type OnFocusGain func(sim *core.Simulation) - -type focusBar struct { - hunterPet *HunterPet - - focusPerTick float64 - - currentFocus float64 - - onFocusGain OnFocusGain - tickAction *core.PendingAction -} - -func (hp *HunterPet) EnableFocusBar(regenMultiplier float64, onFocusGain OnFocusGain) { - hp.focusBar = focusBar{ - hunterPet: hp, - focusPerTick: BaseFocusPerTick * regenMultiplier, - onFocusGain: onFocusGain, - } -} - -func (fb *focusBar) CurrentFocus() float64 { - return fb.currentFocus -} - -func (fb *focusBar) AddFocus(sim *core.Simulation, amount float64, actionID core.ActionID) { - if amount < 0 { - panic("Trying to add negative focus!") - } - - newFocus := core.MinFloat(fb.currentFocus+amount, MaxFocus) - - if sim.Log != nil { - fb.hunterPet.Log(sim, "Gained %0.3f focus from %s (%0.3f --> %0.3f).", amount, actionID, fb.currentFocus, newFocus) - } - - fb.currentFocus = newFocus - - if fb.onFocusGain != nil { - fb.onFocusGain(sim) - } -} - -func (fb *focusBar) SpendFocus(sim *core.Simulation, amount float64, actionID core.ActionID) { - if amount < 0 { - panic("Trying to spend negative focus!") - } - - newFocus := fb.currentFocus - amount - - if sim.Log != nil { - fb.hunterPet.Log(sim, "Spent %0.3f focus from %s (%0.3f --> %0.3f).", amount, actionID, fb.currentFocus, newFocus) - } - - fb.currentFocus = newFocus -} - -func (fb *focusBar) reset(sim *core.Simulation) { - if fb.hunterPet == nil { - return - } - - fb.currentFocus = MaxFocus - - pa := &core.PendingAction{ - Priority: core.ActionPriorityRegen, - NextActionAt: tickDuration, - } - pa.OnAction = func(sim *core.Simulation) { - fb.AddFocus(sim, fb.focusPerTick, core.ActionID{OtherID: proto.OtherAction_OtherActionFocusRegen}) - - pa.NextActionAt = sim.CurrentTime + tickDuration - sim.AddPendingAction(pa) - } - fb.tickAction = pa - sim.AddPendingAction(pa) -} - -func (fb *focusBar) Cancel(sim *core.Simulation) { - if fb.tickAction != nil { - fb.tickAction.Cancel(sim) - fb.tickAction = nil - } -} diff --git a/sim/hunter/kill_command.go b/sim/hunter/kill_command.go index 709fcc1e83..ed6d3ce788 100644 --- a/sim/hunter/kill_command.go +++ b/sim/hunter/kill_command.go @@ -21,13 +21,13 @@ func (hunter *Hunter) registerKillCommandCD() { MaxStacks: 3, OnGain: func(aura *core.Aura, sim *core.Simulation) { hunter.pet.focusDump.BonusCritRating += bonusPetSpecialCrit - if !hunter.pet.specialAbility.IsEmpty() { + if hunter.pet.specialAbility != nil { hunter.pet.specialAbility.BonusCritRating += bonusPetSpecialCrit } }, OnExpire: func(aura *core.Aura, sim *core.Simulation) { hunter.pet.focusDump.BonusCritRating -= bonusPetSpecialCrit - if !hunter.pet.specialAbility.IsEmpty() { + if hunter.pet.specialAbility != nil { hunter.pet.specialAbility.BonusCritRating -= bonusPetSpecialCrit } }, diff --git a/sim/hunter/pet.go b/sim/hunter/pet.go index f8c24bdeac..4b7bc50683 100644 --- a/sim/hunter/pet.go +++ b/sim/hunter/pet.go @@ -8,7 +8,6 @@ import ( type HunterPet struct { core.Pet - focusBar config PetConfig @@ -17,8 +16,8 @@ type HunterPet struct { CobraStrikesAura *core.Aura KillCommandAura *core.Aura - specialAbility PetAbility - focusDump PetAbility + specialAbility *core.Spell + focusDump *core.Spell uptimePercent float64 hasOwnerCooldown bool @@ -86,17 +85,11 @@ func (hp *HunterPet) Talents() *proto.HunterPetTalents { } func (hp *HunterPet) Initialize() { - //if hp.hunterOwner.Options.PetSingleAbility { - // hp.specialAbility = hp.NewPetAbility(hp.config.FocusDump, true) - // hp.config.RandomSelection = false - //} else { hp.specialAbility = hp.NewPetAbility(hp.config.SpecialAbility, true) hp.focusDump = hp.NewPetAbility(hp.config.FocusDump, false) - //} } func (hp *HunterPet) Reset(sim *core.Simulation) { - hp.focusBar.reset(sim) hp.uptimePercent = core.MinFloat(1, core.MaxFloat(0, hp.hunterOwner.Options.PetUptime)) } @@ -104,7 +97,6 @@ func (hp *HunterPet) OnGCDReady(sim *core.Simulation) { percentRemaining := sim.GetRemainingDurationPercent() if percentRemaining < 1.0-hp.uptimePercent { // once fight is % completed, disable pet. hp.Disable(sim) - hp.focusBar.Cancel(sim) return } @@ -118,14 +110,14 @@ func (hp *HunterPet) OnGCDReady(sim *core.Simulation) { target := hp.CurrentTarget if hp.config.RandomSelection { if sim.RandomFloat("Hunter Pet Ability") < 0.5 { - if !hp.specialAbility.TryCast(sim, target, hp) { - if !hp.focusDump.TryCast(sim, target, hp) { + if !hp.specialAbility.CanCast(sim, target) || !hp.specialAbility.Cast(sim, target) { + if !hp.focusDump.Cast(sim, target) { hp.DoNothing() } } } else { - if !hp.focusDump.TryCast(sim, target, hp) { - if !hp.specialAbility.TryCast(sim, target, hp) { + if !hp.focusDump.Cast(sim, target) { + if !hp.specialAbility.CanCast(sim, target) || !hp.specialAbility.Cast(sim, target) { hp.DoNothing() } } @@ -133,11 +125,11 @@ func (hp *HunterPet) OnGCDReady(sim *core.Simulation) { return } - if hp.specialAbility.TryCast(sim, target, hp) { + if !hp.specialAbility.CanCast(sim, target) || hp.specialAbility.Cast(sim, target) { // For abilities that don't use the GCD. if hp.GCD.IsReady(sim) { - if hp.focusDump.Type != Unknown { - if !hp.focusDump.TryCast(sim, target, hp) { + if hp.focusDump != nil { + if !hp.focusDump.Cast(sim, target) { hp.DoNothing() } } else { @@ -145,8 +137,8 @@ func (hp *HunterPet) OnGCDReady(sim *core.Simulation) { } } } else { - if hp.focusDump.Type != Unknown { - if !hp.focusDump.TryCast(sim, target, hp) { + if hp.focusDump != nil { + if !hp.focusDump.Cast(sim, target) { hp.DoNothing() } } else { diff --git a/sim/hunter/pet_abilities.go b/sim/hunter/pet_abilities.go index 7d537d5882..9366dbaafb 100644 --- a/sim/hunter/pet_abilities.go +++ b/sim/hunter/pet_abilities.go @@ -50,37 +50,7 @@ const BiteSpellID = 52474 const ClawSpellID = 52472 const SmackSpellID = 52476 -type PetAbility struct { - Type PetAbilityType - - // Focus cost - Cost float64 - - *core.Spell -} - -func (ability *PetAbility) IsEmpty() bool { - return ability.Spell == nil -} - -// Returns whether the ability was successfully cast. -func (ability *PetAbility) TryCast(sim *core.Simulation, target *core.Unit, hp *HunterPet) bool { - if ability.IsEmpty() { - return false - } - if hp.currentFocus < ability.Cost { - return false - } - if !ability.IsReady(sim) { - return false - } - - hp.SpendFocus(sim, ability.Cost*hp.PseudoStats.CostMultiplier, ability.ActionID) - ability.Cast(sim, target) - return true -} - -func (hp *HunterPet) NewPetAbility(abilityType PetAbilityType, isPrimary bool) PetAbility { +func (hp *HunterPet) NewPetAbility(abilityType PetAbilityType, isPrimary bool) *core.Spell { switch abilityType { case AcidSpit: return hp.newAcidSpit() @@ -139,50 +109,48 @@ func (hp *HunterPet) NewPetAbility(abilityType PetAbilityType, isPrimary bool) P case VenomWebSpray: return hp.newVenomWebSpray() case Unknown: - return PetAbility{} + return nil default: panic("Invalid pet ability type") } } -func (hp *HunterPet) newFocusDump(pat PetAbilityType, spellID int32) PetAbility { - return PetAbility{ - Type: pat, - Cost: 25, - - Spell: hp.RegisterSpell(core.SpellConfig{ - ActionID: core.ActionID{SpellID: spellID}, - SpellSchool: core.SpellSchoolPhysical, - ProcMask: core.ProcMaskMeleeMHSpecial, - Flags: core.SpellFlagMeleeMetrics | core.SpellFlagIncludeTargetBonusDamage, +func (hp *HunterPet) newFocusDump(pat PetAbilityType, spellID int32) *core.Spell { + return hp.RegisterSpell(core.SpellConfig{ + ActionID: core.ActionID{SpellID: spellID}, + SpellSchool: core.SpellSchoolPhysical, + ProcMask: core.ProcMaskMeleeMHSpecial, + Flags: core.SpellFlagMeleeMetrics | core.SpellFlagIncludeTargetBonusDamage, - Cast: core.CastConfig{ - DefaultCast: core.Cast{ - GCD: PetGCD, - }, - IgnoreHaste: true, + FocusCost: core.FocusCostOptions{ + Cost: 25, + }, + Cast: core.CastConfig{ + DefaultCast: core.Cast{ + GCD: PetGCD, }, + IgnoreHaste: true, + }, - DamageMultiplier: 1 * hp.hunterOwner.markedForDeathMultiplier(), - CritMultiplier: 2, - ThreatMultiplier: 1, + DamageMultiplier: 1 * hp.hunterOwner.markedForDeathMultiplier(), + CritMultiplier: 2, + ThreatMultiplier: 1, - ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { - baseDamage := sim.Roll(118, 168) + 0.07*spell.MeleeAttackPower() - baseDamage *= hp.killCommandMult() - spell.CalcAndDealDamage(sim, target, baseDamage, spell.OutcomeMeleeSpecialHitAndCrit) - }, - }), - } + ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { + baseDamage := sim.Roll(118, 168) + 0.07*spell.MeleeAttackPower() + baseDamage *= hp.killCommandMult() + spell.CalcAndDealDamage(sim, target, baseDamage, spell.OutcomeMeleeSpecialHitAndCrit) + }, + }) } -func (hp *HunterPet) newBite() PetAbility { +func (hp *HunterPet) newBite() *core.Spell { return hp.newFocusDump(Bite, BiteSpellID) } -func (hp *HunterPet) newClaw() PetAbility { +func (hp *HunterPet) newClaw() *core.Spell { return hp.newFocusDump(Claw, ClawSpellID) } -func (hp *HunterPet) newSmack() PetAbility { +func (hp *HunterPet) newSmack() *core.Spell { return hp.newFocusDump(Smack, SmackSpellID) } @@ -202,7 +170,7 @@ type PetSpecialAbilityConfig struct { OnSpellHitDealt func(*core.Simulation, *core.Spell, *core.SpellResult) } -func (hp *HunterPet) newSpecialAbility(config PetSpecialAbilityConfig) PetAbility { +func (hp *HunterPet) newSpecialAbility(config PetSpecialAbilityConfig) *core.Spell { var flags core.SpellFlag var applyEffects core.ApplySpellResults var procMask core.ProcMask @@ -230,37 +198,35 @@ func (hp *HunterPet) newSpecialAbility(config PetSpecialAbilityConfig) PetAbilit } } - return PetAbility{ - Type: config.Type, - Cost: config.Cost, - - Spell: hp.RegisterSpell(core.SpellConfig{ - ActionID: core.ActionID{SpellID: config.SpellID}, - SpellSchool: config.School, - ProcMask: procMask, - Flags: flags, - - DamageMultiplier: 1 * hp.hunterOwner.markedForDeathMultiplier(), - CritMultiplier: 2, - ThreatMultiplier: 1, - - Cast: core.CastConfig{ - DefaultCast: core.Cast{ - GCD: config.GCD, - }, - IgnoreHaste: true, - CD: core.Cooldown{ - Timer: hp.NewTimer(), - Duration: hp.hunterOwner.applyLongevity(config.CD), - }, - }, - Dot: config.Dot, - ApplyEffects: applyEffects, - }), - } + return hp.RegisterSpell(core.SpellConfig{ + ActionID: core.ActionID{SpellID: config.SpellID}, + SpellSchool: config.School, + ProcMask: procMask, + Flags: flags, + + DamageMultiplier: 1 * hp.hunterOwner.markedForDeathMultiplier(), + CritMultiplier: 2, + ThreatMultiplier: 1, + + FocusCost: core.FocusCostOptions{ + Cost: config.Cost, + }, + Cast: core.CastConfig{ + DefaultCast: core.Cast{ + GCD: config.GCD, + }, + IgnoreHaste: true, + CD: core.Cooldown{ + Timer: hp.NewTimer(), + Duration: hp.hunterOwner.applyLongevity(config.CD), + }, + }, + Dot: config.Dot, + ApplyEffects: applyEffects, + }) } -func (hp *HunterPet) newAcidSpit() PetAbility { +func (hp *HunterPet) newAcidSpit() *core.Spell { acidSpitAuras := hp.NewEnemyAuraArray(core.AcidSpitAura) return hp.newSpecialAbility(PetSpecialAbilityConfig{ Type: AcidSpit, @@ -284,7 +250,7 @@ func (hp *HunterPet) newAcidSpit() PetAbility { }) } -func (hp *HunterPet) newDemoralizingScreech() PetAbility { +func (hp *HunterPet) newDemoralizingScreech() *core.Spell { var debuffs []*core.Aura for _, target := range hp.Env.Encounter.Targets { debuffs = append(debuffs, core.DemoralizingScreechAura(&target.Unit)) @@ -310,7 +276,7 @@ func (hp *HunterPet) newDemoralizingScreech() PetAbility { }) } -func (hp *HunterPet) newFireBreath() PetAbility { +func (hp *HunterPet) newFireBreath() *core.Spell { return hp.newSpecialAbility(PetSpecialAbilityConfig{ Type: FireBreath, Cost: 20, @@ -344,7 +310,7 @@ func (hp *HunterPet) newFireBreath() PetAbility { }) } -func (hp *HunterPet) newFroststormBreath() PetAbility { +func (hp *HunterPet) newFroststormBreath() *core.Spell { return hp.newSpecialAbility(PetSpecialAbilityConfig{ Type: FroststormBreath, Cost: 20, @@ -358,16 +324,18 @@ func (hp *HunterPet) newFroststormBreath() PetAbility { }) } -func (hp *HunterPet) newFuriousHowl() PetAbility { +func (hp *HunterPet) newFuriousHowl() *core.Spell { actionID := core.ActionID{SpellID: 64495} petAura := hp.NewTemporaryStatsAura("FuriousHowl", actionID, stats.Stats{stats.AttackPower: 320, stats.RangedAttackPower: 320}, time.Second*20) ownerAura := hp.hunterOwner.NewTemporaryStatsAura("FuriousHowl", actionID, stats.Stats{stats.AttackPower: 320, stats.RangedAttackPower: 320}, time.Second*20) - const cost = 20.0 howlSpell := hp.RegisterSpell(core.SpellConfig{ ActionID: actionID, + FocusCost: core.FocusCostOptions{ + Cost: 20, + }, Cast: core.CastConfig{ CD: core.Cooldown{ Timer: hp.NewTimer(), @@ -375,10 +343,9 @@ func (hp *HunterPet) newFuriousHowl() PetAbility { }, }, ExtraCastCondition: func(sim *core.Simulation, target *core.Unit) bool { - return hp.IsEnabled() && hp.CurrentFocus() >= cost + return hp.IsEnabled() }, ApplyEffects: func(sim *core.Simulation, _ *core.Unit, _ *core.Spell) { - hp.SpendFocus(sim, cost, actionID) petAura.Activate(sim) ownerAura.Activate(sim) }, @@ -389,10 +356,10 @@ func (hp *HunterPet) newFuriousHowl() PetAbility { Type: core.CooldownTypeDPS, }) - return PetAbility{} + return nil } -func (hp *HunterPet) newGore() PetAbility { +func (hp *HunterPet) newGore() *core.Spell { return hp.newSpecialAbility(PetSpecialAbilityConfig{ Type: Gore, Cost: 20, @@ -406,7 +373,7 @@ func (hp *HunterPet) newGore() PetAbility { }) } -func (hp *HunterPet) newLavaBreath() PetAbility { +func (hp *HunterPet) newLavaBreath() *core.Spell { return hp.newSpecialAbility(PetSpecialAbilityConfig{ Type: LavaBreath, Cost: 20, @@ -420,7 +387,7 @@ func (hp *HunterPet) newLavaBreath() PetAbility { }) } -func (hp *HunterPet) newLightningBreath() PetAbility { +func (hp *HunterPet) newLightningBreath() *core.Spell { return hp.newSpecialAbility(PetSpecialAbilityConfig{ Type: LightningBreath, Cost: 20, @@ -434,7 +401,7 @@ func (hp *HunterPet) newLightningBreath() PetAbility { }) } -func (hp *HunterPet) newMonstrousBite() PetAbility { +func (hp *HunterPet) newMonstrousBite() *core.Spell { procAura := hp.RegisterAura(core.Aura{ Label: "Monstrous Bite", ActionID: core.ActionID{SpellID: 55499}, @@ -465,7 +432,7 @@ func (hp *HunterPet) newMonstrousBite() PetAbility { }) } -func (hp *HunterPet) newNetherShock() PetAbility { +func (hp *HunterPet) newNetherShock() *core.Spell { return hp.newSpecialAbility(PetSpecialAbilityConfig{ Type: NetherShock, Cost: 20, @@ -479,107 +446,101 @@ func (hp *HunterPet) newNetherShock() PetAbility { }) } -func (hp *HunterPet) newPin() PetAbility { - return PetAbility{ - Type: Pin, - - Spell: hp.RegisterSpell(core.SpellConfig{ - ActionID: core.ActionID{SpellID: 53548}, - SpellSchool: core.SpellSchoolPhysical, - ProcMask: core.ProcMaskEmpty, - - Cast: core.CastConfig{ - DefaultCast: core.Cast{ - GCD: PetGCD, - ChannelTime: time.Second * 4, - }, - IgnoreHaste: true, - CD: core.Cooldown{ - Timer: hp.NewTimer(), - Duration: hp.hunterOwner.applyLongevity(time.Second * 40), - }, - }, - - DamageMultiplier: 1 * hp.hunterOwner.markedForDeathMultiplier(), - ThreatMultiplier: 1, - - Dot: core.DotConfig{ - Aura: core.Aura{ - Label: "Pin", - }, - NumberOfTicks: 4, - TickLength: time.Second * 1, - OnSnapshot: func(sim *core.Simulation, target *core.Unit, dot *core.Dot, isRollover bool) { - dot.SnapshotBaseDamage = sim.Roll(112/4, 144/4) + 0.07*dot.Spell.MeleeAttackPower() - dot.SnapshotBaseDamage *= hp.killCommandMult() - dot.SnapshotAttackerMultiplier = dot.Spell.AttackerDamageMultiplier(dot.Spell.Unit.AttackTables[target.UnitIndex]) - }, - OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) - }, - }, - - ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { - result := spell.CalcAndDealOutcome(sim, target, spell.OutcomeMeleeSpecialHit) - if result.Landed() { - spell.Dot(result.Target).Apply(sim) - } +func (hp *HunterPet) newPin() *core.Spell { + return hp.RegisterSpell(core.SpellConfig{ + ActionID: core.ActionID{SpellID: 53548}, + SpellSchool: core.SpellSchoolPhysical, + ProcMask: core.ProcMaskEmpty, + + Cast: core.CastConfig{ + DefaultCast: core.Cast{ + GCD: PetGCD, + ChannelTime: time.Second * 4, }, - }), - } + IgnoreHaste: true, + CD: core.Cooldown{ + Timer: hp.NewTimer(), + Duration: hp.hunterOwner.applyLongevity(time.Second * 40), + }, + }, + + DamageMultiplier: 1 * hp.hunterOwner.markedForDeathMultiplier(), + ThreatMultiplier: 1, + + Dot: core.DotConfig{ + Aura: core.Aura{ + Label: "Pin", + }, + NumberOfTicks: 4, + TickLength: time.Second * 1, + OnSnapshot: func(sim *core.Simulation, target *core.Unit, dot *core.Dot, isRollover bool) { + dot.SnapshotBaseDamage = sim.Roll(112/4, 144/4) + 0.07*dot.Spell.MeleeAttackPower() + dot.SnapshotBaseDamage *= hp.killCommandMult() + dot.SnapshotAttackerMultiplier = dot.Spell.AttackerDamageMultiplier(dot.Spell.Unit.AttackTables[target.UnitIndex]) + }, + OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) + }, + }, + + ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { + result := spell.CalcAndDealOutcome(sim, target, spell.OutcomeMeleeSpecialHit) + if result.Landed() { + spell.Dot(result.Target).Apply(sim) + } + }, + }) } -func (hp *HunterPet) newPoisonSpit() PetAbility { - return PetAbility{ - Type: PoisonSpit, - Cost: 20, - - Spell: hp.RegisterSpell(core.SpellConfig{ - ActionID: core.ActionID{SpellID: 55557}, - SpellSchool: core.SpellSchoolNature, - ProcMask: core.ProcMaskEmpty, - - Cast: core.CastConfig{ - DefaultCast: core.Cast{ - GCD: PetGCD, - }, - IgnoreHaste: true, - CD: core.Cooldown{ - Timer: hp.NewTimer(), - Duration: hp.hunterOwner.applyLongevity(time.Second * 10), - }, - }, - - DamageMultiplier: 1 * hp.hunterOwner.markedForDeathMultiplier(), - ThreatMultiplier: 1, - - Dot: core.DotConfig{ - Aura: core.Aura{ - Label: "PoisonSpit", - }, - NumberOfTicks: 4, - TickLength: time.Second * 2, - OnSnapshot: func(sim *core.Simulation, target *core.Unit, dot *core.Dot, isRollover bool) { - dot.SnapshotBaseDamage = sim.Roll(104/4, 136/4) + (0.049/4)*dot.Spell.MeleeAttackPower() - dot.SnapshotBaseDamage *= hp.killCommandMult() - dot.SnapshotAttackerMultiplier = dot.Spell.AttackerDamageMultiplier(dot.Spell.Unit.AttackTables[target.UnitIndex]) - }, - OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) - }, - }, - - ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { - result := spell.CalcAndDealOutcome(sim, target, spell.OutcomeMeleeSpecialHit) - if result.Landed() { - spell.Dot(result.Target).Apply(sim) - } +func (hp *HunterPet) newPoisonSpit() *core.Spell { + return hp.RegisterSpell(core.SpellConfig{ + ActionID: core.ActionID{SpellID: 55557}, + SpellSchool: core.SpellSchoolNature, + ProcMask: core.ProcMaskEmpty, + + FocusCost: core.FocusCostOptions{ + Cost: 20, + }, + Cast: core.CastConfig{ + DefaultCast: core.Cast{ + GCD: PetGCD, }, - }), - } + IgnoreHaste: true, + CD: core.Cooldown{ + Timer: hp.NewTimer(), + Duration: hp.hunterOwner.applyLongevity(time.Second * 10), + }, + }, + + DamageMultiplier: 1 * hp.hunterOwner.markedForDeathMultiplier(), + ThreatMultiplier: 1, + + Dot: core.DotConfig{ + Aura: core.Aura{ + Label: "PoisonSpit", + }, + NumberOfTicks: 4, + TickLength: time.Second * 2, + OnSnapshot: func(sim *core.Simulation, target *core.Unit, dot *core.Dot, isRollover bool) { + dot.SnapshotBaseDamage = sim.Roll(104/4, 136/4) + (0.049/4)*dot.Spell.MeleeAttackPower() + dot.SnapshotBaseDamage *= hp.killCommandMult() + dot.SnapshotAttackerMultiplier = dot.Spell.AttackerDamageMultiplier(dot.Spell.Unit.AttackTables[target.UnitIndex]) + }, + OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) + }, + }, + + ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { + result := spell.CalcAndDealOutcome(sim, target, spell.OutcomeMeleeSpecialHit) + if result.Landed() { + spell.Dot(result.Target).Apply(sim) + } + }, + }) } -func (hp *HunterPet) newRake() PetAbility { +func (hp *HunterPet) newRake() *core.Spell { return hp.newSpecialAbility(PetSpecialAbilityConfig{ Type: Rake, Cost: 20, @@ -613,7 +574,7 @@ func (hp *HunterPet) newRake() PetAbility { }) } -func (hp *HunterPet) newRavage() PetAbility { +func (hp *HunterPet) newRavage() *core.Spell { return hp.newSpecialAbility(PetSpecialAbilityConfig{ Type: Ravage, Cost: 0, @@ -626,9 +587,8 @@ func (hp *HunterPet) newRavage() PetAbility { }) } -func (hp *HunterPet) newSavageRend() PetAbility { +func (hp *HunterPet) newSavageRend() *core.Spell { actionID := core.ActionID{SpellID: 53582} - const cost = 20.0 procAura := hp.RegisterAura(core.Aura{ Label: "Savage Rend", @@ -648,6 +608,9 @@ func (hp *HunterPet) newSavageRend() PetAbility { ProcMask: core.ProcMaskSpellDamage, Flags: core.SpellFlagMeleeMetrics | core.SpellFlagIncludeTargetBonusDamage | core.SpellFlagApplyArmorReduction, + FocusCost: core.FocusCostOptions{ + Cost: 20, + }, Cast: core.CastConfig{ CD: core.Cooldown{ Timer: hp.NewTimer(), @@ -655,7 +618,7 @@ func (hp *HunterPet) newSavageRend() PetAbility { }, }, ExtraCastCondition: func(sim *core.Simulation, target *core.Unit) bool { - return hp.IsEnabled() && hp.CurrentFocus() >= cost + return hp.IsEnabled() }, DamageMultiplier: 1 * hp.hunterOwner.markedForDeathMultiplier(), @@ -683,7 +646,6 @@ func (hp *HunterPet) newSavageRend() PetAbility { baseDamage *= hp.killCommandMult() result := spell.CalcAndDealDamage(sim, target, baseDamage, spell.OutcomeMeleeSpecialHitAndCrit) - hp.SpendFocus(sim, cost, actionID) if result.Landed() { spell.Dot(target).Apply(sim) if result.DidCrit() { @@ -698,60 +660,58 @@ func (hp *HunterPet) newSavageRend() PetAbility { Type: core.CooldownTypeDPS, }) - return PetAbility{} -} - -func (hp *HunterPet) newScorpidPoison() PetAbility { - return PetAbility{ - Type: ScorpidPoison, - Cost: 20, - - Spell: hp.RegisterSpell(core.SpellConfig{ - ActionID: core.ActionID{SpellID: 55728}, - SpellSchool: core.SpellSchoolNature, - ProcMask: core.ProcMaskEmpty, - - Cast: core.CastConfig{ - DefaultCast: core.Cast{ - GCD: PetGCD, - }, - IgnoreHaste: true, - CD: core.Cooldown{ - Timer: hp.NewTimer(), - Duration: hp.hunterOwner.applyLongevity(time.Second * 10), - }, - }, - - DamageMultiplier: 1 * hp.hunterOwner.markedForDeathMultiplier(), - ThreatMultiplier: 1, - - Dot: core.DotConfig{ - Aura: core.Aura{ - Label: "ScorpidPoison", - }, - NumberOfTicks: 5, - TickLength: time.Second * 2, - OnSnapshot: func(sim *core.Simulation, target *core.Unit, dot *core.Dot, isRollover bool) { - dot.SnapshotBaseDamage = sim.Roll(100/5, 130/5) + (0.07/5)*dot.Spell.MeleeAttackPower() - dot.SnapshotBaseDamage *= hp.killCommandMult() - dot.SnapshotAttackerMultiplier = dot.Spell.AttackerDamageMultiplier(dot.Spell.Unit.AttackTables[target.UnitIndex]) - }, - OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) - }, - }, - - ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { - result := spell.CalcAndDealOutcome(sim, target, spell.OutcomeMeleeSpecialHit) - if result.Landed() { - spell.Dot(target).Apply(sim) - } + return nil +} + +func (hp *HunterPet) newScorpidPoison() *core.Spell { + return hp.RegisterSpell(core.SpellConfig{ + ActionID: core.ActionID{SpellID: 55728}, + SpellSchool: core.SpellSchoolNature, + ProcMask: core.ProcMaskEmpty, + + FocusCost: core.FocusCostOptions{ + Cost: 20, + }, + Cast: core.CastConfig{ + DefaultCast: core.Cast{ + GCD: PetGCD, }, - }), - } + IgnoreHaste: true, + CD: core.Cooldown{ + Timer: hp.NewTimer(), + Duration: hp.hunterOwner.applyLongevity(time.Second * 10), + }, + }, + + DamageMultiplier: 1 * hp.hunterOwner.markedForDeathMultiplier(), + ThreatMultiplier: 1, + + Dot: core.DotConfig{ + Aura: core.Aura{ + Label: "ScorpidPoison", + }, + NumberOfTicks: 5, + TickLength: time.Second * 2, + OnSnapshot: func(sim *core.Simulation, target *core.Unit, dot *core.Dot, isRollover bool) { + dot.SnapshotBaseDamage = sim.Roll(100/5, 130/5) + (0.07/5)*dot.Spell.MeleeAttackPower() + dot.SnapshotBaseDamage *= hp.killCommandMult() + dot.SnapshotAttackerMultiplier = dot.Spell.AttackerDamageMultiplier(dot.Spell.Unit.AttackTables[target.UnitIndex]) + }, + OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) + }, + }, + + ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { + result := spell.CalcAndDealOutcome(sim, target, spell.OutcomeMeleeSpecialHit) + if result.Landed() { + spell.Dot(target).Apply(sim) + } + }, + }) } -func (hp *HunterPet) newSnatch() PetAbility { +func (hp *HunterPet) newSnatch() *core.Spell { return hp.newSpecialAbility(PetSpecialAbilityConfig{ Type: Snatch, Cost: 20, @@ -764,7 +724,7 @@ func (hp *HunterPet) newSnatch() PetAbility { }) } -func (hp *HunterPet) newSonicBlast() PetAbility { +func (hp *HunterPet) newSonicBlast() *core.Spell { return hp.newSpecialAbility(PetSpecialAbilityConfig{ Type: SonicBlast, Cost: 80, @@ -777,7 +737,7 @@ func (hp *HunterPet) newSonicBlast() PetAbility { }) } -func (hp *HunterPet) newSpiritStrike() PetAbility { +func (hp *HunterPet) newSpiritStrike() *core.Spell { return hp.newSpecialAbility(PetSpecialAbilityConfig{ Type: SpiritStrike, Cost: 20, @@ -812,66 +772,60 @@ func (hp *HunterPet) newSpiritStrike() PetAbility { }) } -func (hp *HunterPet) newSporeCloud() PetAbility { - var debuffs []*core.Aura - for _, target := range hp.Env.Encounter.Targets { - debuffs = append(debuffs, core.SporeCloudAura(&target.Unit)) - } +func (hp *HunterPet) newSporeCloud() *core.Spell { + debuffs := hp.NewEnemyAuraArray(core.SporeCloudAura) + return hp.RegisterSpell(core.SpellConfig{ + ActionID: core.ActionID{SpellID: 53598}, + SpellSchool: core.SpellSchoolNature, + ProcMask: core.ProcMaskSpellDamage, - return PetAbility{ - Type: SporeCloud, - Cost: 20, - - Spell: hp.RegisterSpell(core.SpellConfig{ - ActionID: core.ActionID{SpellID: 53598}, - SpellSchool: core.SpellSchoolNature, - ProcMask: core.ProcMaskSpellDamage, - - Cast: core.CastConfig{ - DefaultCast: core.Cast{ - GCD: PetGCD, - }, - IgnoreHaste: true, - CD: core.Cooldown{ - Timer: hp.NewTimer(), - Duration: hp.hunterOwner.applyLongevity(time.Second * 10), - }, - }, - - DamageMultiplier: 1 * hp.hunterOwner.markedForDeathMultiplier(), - ThreatMultiplier: 1, - - Dot: core.DotConfig{ - IsAOE: true, - Aura: core.Aura{ - Label: "SporeCloud", - }, - NumberOfTicks: 3, - TickLength: time.Second * 3, - OnSnapshot: func(sim *core.Simulation, target *core.Unit, dot *core.Dot, isRollover bool) { - dot.SnapshotBaseDamage = sim.Roll(22, 28) + (0.049/3)*dot.Spell.MeleeAttackPower() - dot.SnapshotBaseDamage *= hp.killCommandMult() - dot.SnapshotAttackerMultiplier = dot.Spell.AttackerDamageMultiplier(dot.Spell.Unit.AttackTables[target.UnitIndex]) - }, - OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) - for _, aoeTarget := range sim.Encounter.Targets { - dot.CalcAndDealPeriodicSnapshotDamage(sim, &aoeTarget.Unit, dot.OutcomeTick) - } - }, - }, - - ApplyEffects: func(sim *core.Simulation, _ *core.Unit, spell *core.Spell) { - spell.AOEDot().Apply(sim) - for _, debuff := range debuffs { - debuff.Activate(sim) + FocusCost: core.FocusCostOptions{ + Cost: 20, + }, + Cast: core.CastConfig{ + DefaultCast: core.Cast{ + GCD: PetGCD, + }, + IgnoreHaste: true, + CD: core.Cooldown{ + Timer: hp.NewTimer(), + Duration: hp.hunterOwner.applyLongevity(time.Second * 10), + }, + }, + + DamageMultiplier: 1 * hp.hunterOwner.markedForDeathMultiplier(), + ThreatMultiplier: 1, + + Dot: core.DotConfig{ + IsAOE: true, + Aura: core.Aura{ + Label: "SporeCloud", + }, + NumberOfTicks: 3, + TickLength: time.Second * 3, + OnSnapshot: func(sim *core.Simulation, target *core.Unit, dot *core.Dot, isRollover bool) { + dot.SnapshotBaseDamage = sim.Roll(22, 28) + (0.049/3)*dot.Spell.MeleeAttackPower() + dot.SnapshotBaseDamage *= hp.killCommandMult() + dot.SnapshotAttackerMultiplier = dot.Spell.AttackerDamageMultiplier(dot.Spell.Unit.AttackTables[target.UnitIndex]) + }, + OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) + for _, aoeTarget := range sim.Encounter.Targets { + dot.CalcAndDealPeriodicSnapshotDamage(sim, &aoeTarget.Unit, dot.OutcomeTick) } }, - }), - } + }, + + ApplyEffects: func(sim *core.Simulation, _ *core.Unit, spell *core.Spell) { + spell.AOEDot().Apply(sim) + for _, target := range spell.Unit.Env.Encounter.TargetUnits { + debuffs.Get(target).Activate(sim) + } + }, + }) } -func (hp *HunterPet) newStampede() PetAbility { +func (hp *HunterPet) newStampede() *core.Spell { debuffs := hp.NewEnemyAuraArray(core.StampedeAura) return hp.newSpecialAbility(PetSpecialAbilityConfig{ Type: Stampede, @@ -890,7 +844,7 @@ func (hp *HunterPet) newStampede() PetAbility { }) } -func (hp *HunterPet) newSting() PetAbility { +func (hp *HunterPet) newSting() *core.Spell { debuffs := hp.NewEnemyAuraArray(core.StingAura) return hp.newSpecialAbility(PetSpecialAbilityConfig{ Type: Sting, @@ -910,7 +864,7 @@ func (hp *HunterPet) newSting() PetAbility { }) } -func (hp *HunterPet) newSwipe() PetAbility { +func (hp *HunterPet) newSwipe() *core.Spell { // TODO: This is frontal cone, but might be more realistic as single-target // since pets are hard to control. return hp.newSpecialAbility(PetSpecialAbilityConfig{ @@ -926,7 +880,7 @@ func (hp *HunterPet) newSwipe() PetAbility { }) } -func (hp *HunterPet) newTendonRip() PetAbility { +func (hp *HunterPet) newTendonRip() *core.Spell { return hp.newSpecialAbility(PetSpecialAbilityConfig{ Type: TendonRip, Cost: 20, @@ -939,48 +893,43 @@ func (hp *HunterPet) newTendonRip() PetAbility { }) } -func (hp *HunterPet) newVenomWebSpray() PetAbility { - return PetAbility{ - Type: VenomWebSpray, - Cost: 0, - - Spell: hp.RegisterSpell(core.SpellConfig{ - ActionID: core.ActionID{SpellID: 55509}, - SpellSchool: core.SpellSchoolNature, - ProcMask: core.ProcMaskEmpty, - - Cast: core.CastConfig{ - CD: core.Cooldown{ - Timer: hp.NewTimer(), - Duration: hp.hunterOwner.applyLongevity(time.Second * 40), - }, - }, - - DamageMultiplier: 1 * hp.hunterOwner.markedForDeathMultiplier(), - ThreatMultiplier: 1, - - Dot: core.DotConfig{ - Aura: core.Aura{ - Label: "VenomWebSpray", - }, - NumberOfTicks: 4, - TickLength: time.Second * 1, - OnSnapshot: func(sim *core.Simulation, target *core.Unit, dot *core.Dot, isRollover bool) { - dot.SnapshotBaseDamage = 46 + 0.07*dot.Spell.MeleeAttackPower() - dot.SnapshotBaseDamage *= hp.killCommandMult() - dot.SnapshotAttackerMultiplier = dot.Spell.AttackerDamageMultiplier(dot.Spell.Unit.AttackTables[target.UnitIndex]) - }, - OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) - }, - }, - - ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { - result := spell.CalcAndDealOutcome(sim, target, spell.OutcomeMeleeSpecialHit) - if result.Landed() { - spell.Dot(target).Apply(sim) - } +func (hp *HunterPet) newVenomWebSpray() *core.Spell { + return hp.RegisterSpell(core.SpellConfig{ + ActionID: core.ActionID{SpellID: 55509}, + SpellSchool: core.SpellSchoolNature, + ProcMask: core.ProcMaskEmpty, + + Cast: core.CastConfig{ + CD: core.Cooldown{ + Timer: hp.NewTimer(), + Duration: hp.hunterOwner.applyLongevity(time.Second * 40), }, - }), - } + }, + + DamageMultiplier: 1 * hp.hunterOwner.markedForDeathMultiplier(), + ThreatMultiplier: 1, + + Dot: core.DotConfig{ + Aura: core.Aura{ + Label: "VenomWebSpray", + }, + NumberOfTicks: 4, + TickLength: time.Second * 1, + OnSnapshot: func(sim *core.Simulation, target *core.Unit, dot *core.Dot, isRollover bool) { + dot.SnapshotBaseDamage = 46 + 0.07*dot.Spell.MeleeAttackPower() + dot.SnapshotBaseDamage *= hp.killCommandMult() + dot.SnapshotAttackerMultiplier = dot.Spell.AttackerDamageMultiplier(dot.Spell.Unit.AttackTables[target.UnitIndex]) + }, + OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) + }, + }, + + ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { + result := spell.CalcAndDealOutcome(sim, target, spell.OutcomeMeleeSpecialHit) + if result.Landed() { + spell.Dot(target).Apply(sim) + } + }, + }) } diff --git a/sim/hunter/talents.go b/sim/hunter/talents.go index 40df6053ce..6342843c26 100644 --- a/sim/hunter/talents.go +++ b/sim/hunter/talents.go @@ -208,13 +208,13 @@ func (hunter *Hunter) applyCobraStrikes() { MaxStacks: 2, OnGain: func(aura *core.Aura, sim *core.Simulation) { hunter.pet.focusDump.BonusCritRating += 100 * core.CritRatingPerCritChance - if !hunter.pet.specialAbility.IsEmpty() { + if hunter.pet.specialAbility != nil { hunter.pet.specialAbility.BonusCritRating += 100 * core.CritRatingPerCritChance } }, OnExpire: func(aura *core.Aura, sim *core.Simulation) { hunter.pet.focusDump.BonusCritRating -= 100 * core.CritRatingPerCritChance - if !hunter.pet.specialAbility.IsEmpty() { + if hunter.pet.specialAbility != nil { hunter.pet.specialAbility.BonusCritRating -= 100 * core.CritRatingPerCritChance } },