From c089322f1f22cef623df36e1eb4773a72d32e27d Mon Sep 17 00:00:00 2001 From: vigo Date: Sun, 29 Oct 2023 10:36:06 +0100 Subject: [PATCH] [core] introduce WeaponAttack, a per weapon (MH, OH, Ranged) AutoAttack [warrior] ranged attacks (namely Thunder Clap) now properly trigger MH Deep Wounds --- sim/core/attack.go | 449 ++++++++---------- sim/core/character.go | 19 +- sim/core/item_swaps.go | 2 - sim/druid/forms.go | 8 +- sim/encounters/ulduar/hodir_ai.go | 4 +- sim/shaman/enhancement/enhancement.go | 10 +- sim/warrior/deep_wounds.go | 14 +- sim/warrior/dps/TestArms.results | 16 +- .../protection/TestProtectionWarrior.results | 24 +- 9 files changed, 252 insertions(+), 294 deletions(-) diff --git a/sim/core/attack.go b/sim/core/attack.go index 780f7f9ce1..7918028d97 100644 --- a/sim/core/attack.go +++ b/sim/core/attack.go @@ -28,9 +28,8 @@ type Weapon struct { func (weapon *Weapon) DPS() float64 { if weapon.SwingSpeed == 0 { return 0 - } else { - return (weapon.BaseDamageMin + weapon.BaseDamageMax) / 2.0 / weapon.SwingSpeed } + return (weapon.BaseDamageMin + weapon.BaseDamageMax) / 2.0 / weapon.SwingSpeed } func newWeaponFromUnarmed(critMultiplier float64) Weapon { @@ -166,107 +165,136 @@ func (spell *Spell) IsMelee() bool { } func (aa *AutoAttacks) IsDualWielding() bool { - return aa.isDualWielding + return aa.oh != nil } func (aa *AutoAttacks) MH() *Weapon { - return &aa.mh + return aa.mh.GetWeapon() } func (aa *AutoAttacks) SetMH(weapon Weapon) { - aa.mh = weapon - aa.mhAuto.CritMultiplier = weapon.CritMultiplier - aa.curMHSwingDuration = DurationFromSeconds(aa.mh.SwingSpeed / aa.curMeleeSpeed) + aa.mh.SetWeapon(weapon) } func (aa *AutoAttacks) OH() *Weapon { - return &aa.oh + return aa.oh.GetWeapon() } func (aa *AutoAttacks) SetOH(weapon Weapon) { - aa.oh = weapon - aa.ohAuto.CritMultiplier = weapon.CritMultiplier - aa.curOHSwingDuration = DurationFromSeconds(aa.oh.SwingSpeed / aa.curMeleeSpeed) + aa.oh.SetWeapon(weapon) } func (aa *AutoAttacks) Ranged() *Weapon { - return &aa.ranged + return aa.ranged.GetWeapon() } func (aa *AutoAttacks) SetRanged(weapon Weapon) { - aa.ranged = weapon - aa.rangedAuto.CritMultiplier = weapon.CritMultiplier - aa.curRangedSwingDuration = DurationFromSeconds(aa.ranged.SwingSpeed / aa.curRangedSpeed) + aa.ranged.SetWeapon(weapon) } func (aa *AutoAttacks) AutoSwingMelee() bool { - return aa.autoSwingMelee + return aa.mh != nil } func (aa *AutoAttacks) AutoSwingRanged() bool { - return aa.autoSwingRanged + return aa.ranged != nil } func (aa *AutoAttacks) MHAuto() *Spell { - return aa.mhAuto + return aa.mh.spell } func (aa *AutoAttacks) OHAuto() *Spell { - return aa.ohAuto + return aa.oh.spell } func (aa *AutoAttacks) RangedAuto() *Spell { - return aa.rangedAuto + return aa.ranged.spell } func (aa *AutoAttacks) OffhandSwingAt() time.Duration { - return aa.offhandSwingAt + return aa.oh.swingAt } func (aa *AutoAttacks) SetOffhandSwingAt(offhandSwingAt time.Duration) { - aa.offhandSwingAt = offhandSwingAt + aa.oh.swingAt = offhandSwingAt } -type AutoAttacks struct { +func (aa *AutoAttacks) SetReplaceMHSwing(replaceSwing ReplaceMHSwing) { + aa.mh.replaceSwing = replaceSwing +} + +type WeaponAttack struct { + Weapon + agent Agent unit *Unit - mh Weapon - oh Weapon - ranged Weapon + spell *Spell - isDualWielding bool + replaceSwing ReplaceMHSwing - // If true, core engine will handle calling SwingMelee(). Set to false to manually manage - // swings, for example for hunter melee weaving. - autoSwingMelee bool + swingAt time.Duration - // If true, core engine will handle calling SwingRanged(). Unless you're a hunter, don't - // use this. - autoSwingRanged bool + curSwingSpeed float64 + curSwingDuration time.Duration +} - mainhandSwingAt time.Duration - offhandSwingAt time.Duration - rangedSwingAt time.Duration +func (wa *WeaponAttack) GetWeapon() *Weapon { + if wa == nil { + return &Weapon{} + } + return &wa.Weapon +} - // These are created in EnableAutoAttacks, and can be safely altered before finalize(), where the related spells are created - MHConfig SpellConfig - OHConfig SpellConfig - RangedConfig SpellConfig +func (wa *WeaponAttack) SetWeapon(weapon Weapon) { + wa.Weapon = weapon + wa.spell.CritMultiplier = weapon.CritMultiplier + wa.updateSwingDuration() +} - mhAuto *Spell - ohAuto *Spell - rangedAuto *Spell +func (wa *WeaponAttack) swing(sim *Simulation) { + if sim.CurrentTime < wa.swingAt { + return + } + + attackSpell := wa.spell + + if wa.replaceSwing != nil { + if wa.unit.IsUsingAPL { + // Need to check APL here to allow last-moment HS queue casts. + wa.unit.Rotation.DoNextAction(sim) + } + // Allow MH swing to be overridden for abilities like Heroic Strike. + attackSpell = wa.replaceSwing(sim, attackSpell) + } + + // Update swing timer BEFORE the cast, so that APL checks for TimeToNextAuto behave correctly + // if the attack causes APL evaluations (e.g. from rage gain). + wa.swingAt = sim.CurrentTime + wa.curSwingDuration + attackSpell.Cast(sim, wa.unit.CurrentTarget) + + if !sim.Options.Interactive { + if wa.unit.IsUsingAPL { + wa.unit.Rotation.DoNextAction(sim) + } else { + wa.agent.OnAutoAttack(sim, attackSpell) + } + } +} - ReplaceMHSwing ReplaceMHSwing +func (wa *WeaponAttack) updateSwingDuration() { + wa.curSwingDuration = DurationFromSeconds(wa.SwingSpeed / wa.curSwingSpeed) +} - // Current melee and ranged swing speeds, and corresponding swing durations, updated in UpdateSwingTimers. - curMeleeSpeed float64 - curMHSwingDuration time.Duration - curOHSwingDuration time.Duration +type AutoAttacks struct { + mh *WeaponAttack + oh *WeaponAttack + ranged *WeaponAttack - curRangedSpeed float64 - curRangedSwingDuration time.Duration + MHConfig SpellConfig + OHConfig SpellConfig + RangedConfig SpellConfig // PendingAction which handles auto attacks. autoSwingAction *PendingAction @@ -280,7 +308,6 @@ type AutoAttackOptions struct { Ranged Weapon AutoSwingMelee bool // If true, core engine will handle calling SwingMelee() for you. AutoSwingRanged bool // If true, core engine will handle calling SwingRanged() for you. - SyncType int32 ReplaceMHSwing ReplaceMHSwing } @@ -291,21 +318,42 @@ func (unit *Unit) EnableAutoAttacks(agent Agent, options AutoAttackOptions) { if options.OffHand.AttackPowerPerDPS == 0 { options.OffHand.AttackPowerPerDPS = DefaultAttackPowerPerDPS } + + var mh, oh, ranged *WeaponAttack + if options.AutoSwingMelee && options.MainHand.SwingSpeed != 0 { + mh = &WeaponAttack{ + agent: agent, + unit: unit, + Weapon: options.MainHand, + replaceSwing: options.ReplaceMHSwing, + } + } + + if options.AutoSwingMelee && options.OffHand.SwingSpeed != 0 { + oh = &WeaponAttack{ + agent: agent, + unit: unit, + Weapon: options.OffHand, + } + } + + if options.AutoSwingRanged && options.Ranged.SwingSpeed != 0 { + ranged = &WeaponAttack{ + agent: agent, + unit: unit, + Weapon: options.Ranged, + } + } + unit.AutoAttacks = AutoAttacks{ - agent: agent, - unit: unit, - mh: options.MainHand, - oh: options.OffHand, - ranged: options.Ranged, - autoSwingMelee: options.AutoSwingMelee, - autoSwingRanged: options.AutoSwingRanged, - ReplaceMHSwing: options.ReplaceMHSwing, - isDualWielding: options.MainHand.SwingSpeed != 0 && options.OffHand.SwingSpeed != 0, + mh: mh, + oh: oh, + ranged: ranged, } unit.AutoAttacks.MHConfig = SpellConfig{ ActionID: ActionID{OtherID: proto.OtherAction_OtherActionAttack, Tag: 1}, - SpellSchool: unit.AutoAttacks.mh.GetSpellSchool(), + SpellSchool: options.MainHand.GetSpellSchool(), ProcMask: ProcMaskMeleeMHAuto, Flags: SpellFlagMeleeMetrics | SpellFlagIncludeTargetBonusDamage | SpellFlagNoOnCastComplete, @@ -323,7 +371,7 @@ func (unit *Unit) EnableAutoAttacks(agent Agent, options AutoAttackOptions) { unit.AutoAttacks.OHConfig = SpellConfig{ ActionID: ActionID{OtherID: proto.OtherAction_OtherActionAttack, Tag: 2}, - SpellSchool: unit.AutoAttacks.oh.GetSpellSchool(), + SpellSchool: options.OffHand.GetSpellSchool(), ProcMask: ProcMaskMeleeOHAuto, Flags: SpellFlagMeleeMetrics | SpellFlagIncludeTargetBonusDamage | SpellFlagNoOnCastComplete, @@ -341,7 +389,7 @@ func (unit *Unit) EnableAutoAttacks(agent Agent, options AutoAttackOptions) { unit.AutoAttacks.RangedConfig = SpellConfig{ ActionID: ActionID{OtherID: proto.OtherAction_OtherActionShoot}, - SpellSchool: SpellSchoolPhysical, + SpellSchool: options.Ranged.GetSpellSchool(), ProcMask: ProcMaskRangedAuto, Flags: SpellFlagMeleeMetrics | SpellFlagIncludeTargetBonusDamage, @@ -380,47 +428,53 @@ func (unit *Unit) EnableAutoAttacks(agent Agent, options AutoAttackOptions) { func (unit *Unit) OnAutoAttack(_ *Simulation, _ *Spell) {} func (aa *AutoAttacks) finalize() { - if aa.autoSwingMelee { - aa.mhAuto = aa.unit.GetOrRegisterSpell(aa.MHConfig) - aa.ohAuto = aa.unit.GetOrRegisterSpell(aa.OHConfig) + if aa.mh != nil { + aa.mh.spell = aa.mh.unit.GetOrRegisterSpell(aa.MHConfig) } - - if aa.autoSwingRanged { - aa.rangedAuto = aa.unit.GetOrRegisterSpell(aa.RangedConfig) + if aa.oh != nil { + aa.oh.spell = aa.oh.unit.GetOrRegisterSpell(aa.OHConfig) + } + if aa.ranged != nil { + aa.ranged.spell = aa.ranged.unit.GetOrRegisterSpell(aa.RangedConfig) } } func (aa *AutoAttacks) reset(sim *Simulation) { - if !aa.autoSwingMelee && !aa.autoSwingRanged { + if aa.mh == nil && aa.ranged == nil { return } - if aa.autoSwingMelee { - aa.curMeleeSpeed = aa.unit.SwingSpeed() - aa.updateMeleeDurations() + if aa.mh != nil { + aa.mh.curSwingSpeed = aa.mh.unit.SwingSpeed() + aa.mh.updateSwingDuration() + + aa.mh.swingAt = 0 + + if aa.oh != nil { + aa.oh.curSwingSpeed = aa.mh.curSwingSpeed + aa.oh.updateSwingDuration() - aa.mainhandSwingAt = 0 - aa.offhandSwingAt = 0 + aa.oh.swingAt = 0 - // Apply random delay of 0 - 50% swing time, to one of the weapons if dual wielding - if aa.isDualWielding { - if aa.unit.Type == EnemyUnit { - aa.offhandSwingAt = DurationFromSeconds(aa.mh.SwingSpeed / 2) + // Apply random delay of 0 - 50% swing time, to one of the weapons if dual wielding + if aa.oh.unit.Type == EnemyUnit { + aa.oh.swingAt = DurationFromSeconds(aa.mh.SwingSpeed / 2) } else { if sim.RandomFloat("SwingResetWeapon") < 0.5 { - aa.mainhandSwingAt = DurationFromSeconds(sim.RandomFloat("SwingResetDelay") * aa.mh.SwingSpeed / 2) + aa.mh.swingAt = DurationFromSeconds(sim.RandomFloat("SwingResetDelay") * aa.mh.SwingSpeed / 2) } else { - aa.offhandSwingAt = DurationFromSeconds(sim.RandomFloat("SwingResetDelay") * aa.mh.SwingSpeed / 2) + aa.oh.swingAt = DurationFromSeconds(sim.RandomFloat("SwingResetDelay") * aa.mh.SwingSpeed / 2) } } } + } - if aa.autoSwingRanged { - aa.curRangedSpeed = aa.unit.RangedSwingSpeed() - aa.UpdateRangedDuration() + if aa.ranged != nil { + aa.ranged.curSwingSpeed = aa.ranged.unit.RangedSwingSpeed() + aa.ranged.updateSwingDuration() - aa.rangedSwingAt = 0 + aa.ranged.swingAt = 0 } aa.autoSwingAction = nil @@ -432,11 +486,11 @@ func (aa *AutoAttacks) startPull(sim *Simulation) { return } - if aa.autoSwingMelee { + if aa.mh != nil { aa.rescheduleMelee(sim) } - if aa.autoSwingRanged { + if aa.ranged != nil { aa.rescheduleRanged(sim) } } @@ -446,18 +500,14 @@ func (aa *AutoAttacks) rescheduleRanged(sim *Simulation) { aa.autoSwingAction.Cancel(sim) } - if aa.autoSwingAction != nil { - aa.autoSwingAction.Cancel(sim) - } - var pa *PendingAction pa = &PendingAction{ - NextActionAt: aa.rangedSwingAt, + NextActionAt: aa.ranged.swingAt, Priority: ActionPriorityAuto, OnAction: func(sim *Simulation) { - aa.SwingRanged(sim, aa.unit.CurrentTarget) - pa.NextActionAt = aa.rangedSwingAt + aa.ranged.swing(sim) + pa.NextActionAt = aa.ranged.swingAt // Cancelled means we made a new one because of a swing speed change. if !pa.cancelled { @@ -481,7 +531,7 @@ func (aa *AutoAttacks) rescheduleMelee(sim *Simulation) { NextActionAt: aa.NextAttackAt(), Priority: ActionPriorityAuto, OnAction: func(sim *Simulation) { - aa.SwingMelee(sim, aa.unit.CurrentTarget) + aa.SwingMelee(sim) pa.NextActionAt = aa.NextAttackAt() // Cancelled means we made a new one because of a swing speed change. @@ -514,166 +564,78 @@ func (aa *AutoAttacks) EnableAutoSwing(sim *Simulation) { aa.autoSwingCancelled = false - if aa.autoSwingMelee { - if aa.mainhandSwingAt < sim.CurrentTime { - aa.mainhandSwingAt = sim.CurrentTime - } - if aa.offhandSwingAt < sim.CurrentTime { - aa.offhandSwingAt = sim.CurrentTime + if aa.mh != nil { + aa.mh.swingAt = max(aa.mh.swingAt, sim.CurrentTime) + if aa.oh != nil { + aa.oh.swingAt = max(aa.oh.swingAt, sim.CurrentTime) } - aa.rescheduleMelee(sim) } - if aa.autoSwingRanged { - if aa.rangedSwingAt < sim.CurrentTime { - aa.rangedSwingAt = sim.CurrentTime - } - + if aa.ranged != nil { + aa.ranged.swingAt = max(aa.ranged.swingAt, sim.CurrentTime) aa.rescheduleRanged(sim) } } // The amount of time between two MH swings. func (aa *AutoAttacks) MainhandSwingSpeed() time.Duration { - return aa.curMHSwingDuration + return aa.mh.curSwingDuration } // The amount of time between two OH swings. func (aa *AutoAttacks) OffhandSwingSpeed() time.Duration { - return aa.curOHSwingDuration -} - -// The amount of time between two Ranged swings. -func (aa *AutoAttacks) RangedSwingSpeed() time.Duration { - return aa.curRangedSwingDuration + return aa.oh.curSwingDuration } // SwingMelee will check any swing timers if they are up, and if so, swing! -func (aa *AutoAttacks) SwingMelee(sim *Simulation, target *Unit) { - aa.TrySwingMH(sim, target) - aa.TrySwingOH(sim, target) -} - -func (aa *AutoAttacks) SwingRanged(sim *Simulation, target *Unit) { - aa.TrySwingRanged(sim, target) -} - -// Performs an auto attack using the main hand weapon, if the MH CD is ready. -func (aa *AutoAttacks) TrySwingMH(sim *Simulation, target *Unit) { - if aa.mainhandSwingAt > sim.CurrentTime { - return +func (aa *AutoAttacks) SwingMelee(sim *Simulation) { + aa.mh.swing(sim) + if aa.oh != nil { + aa.oh.swing(sim) } +} - attackSpell := aa.mhAuto - - if aa.ReplaceMHSwing != nil { - if aa.unit.IsUsingAPL { - // Need to check APL here to allow last-moment HS queue casts. - aa.unit.Rotation.DoNextAction(sim) - } - // Allow MH swing to be overridden for abilities like Heroic Strike. - attackSpell = aa.ReplaceMHSwing(sim, aa.mhAuto) - } - - // Update swing timer BEFORE the cast, so that APL checks for TimeToNextAuto behave correctly - // if the attack causes APL evaluations (e.g. from rage gain). - aa.mainhandSwingAt = sim.CurrentTime + aa.curMHSwingDuration - attackSpell.Cast(sim, target) - - if !sim.Options.Interactive { - if aa.unit.IsUsingAPL { - aa.unit.Rotation.DoNextAction(sim) - } else { - aa.agent.OnAutoAttack(sim, attackSpell) - } - } +func (aa *AutoAttacks) SwingRanged(sim *Simulation) { + aa.ranged.swing(sim) } // Optionally replaces the given swing spell with an Agent-specified MH Swing replacer. // This is for effects like Heroic Strike or Raptor Strike. func (aa *AutoAttacks) MaybeReplaceMHSwing(sim *Simulation, mhSwingSpell *Spell) *Spell { - if aa.ReplaceMHSwing == nil { + if aa.mh.replaceSwing == nil { return mhSwingSpell } // Allow MH swing to be overridden for abilities like Heroic Strike. - return aa.ReplaceMHSwing(sim, mhSwingSpell) -} - -// Performs an auto attack using the main hand weapon, if the OH CD is ready. -func (aa *AutoAttacks) TrySwingOH(sim *Simulation, target *Unit) { - if !aa.isDualWielding || aa.offhandSwingAt > sim.CurrentTime { - return - } - - // Update swing timer BEFORE the cast, so that APL checks for TimeToNextAuto behave correctly - // if the attack causes APL evaluations (e.g. from rage gain). - aa.offhandSwingAt = sim.CurrentTime + aa.curOHSwingDuration - aa.ohAuto.Cast(sim, target) - - if !sim.Options.Interactive { - if aa.unit.IsUsingAPL { - aa.unit.Rotation.DoNextAction(sim) - } else { - aa.agent.OnAutoAttack(sim, aa.ohAuto) - } - } -} - -// Performs an auto attack using the ranged weapon, if the Ranged CD is ready. -func (aa *AutoAttacks) TrySwingRanged(sim *Simulation, target *Unit) { - if aa.rangedSwingAt > sim.CurrentTime { - return - } - - // Update swing timer BEFORE the cast, so that APL checks for TimeToNextAuto behave correctly - // if the attack causes APL evaluations. - aa.rangedSwingAt = sim.CurrentTime + aa.RangedSwingSpeed() - aa.rangedAuto.Cast(sim, target) - - if !sim.Options.Interactive { - if aa.unit.IsUsingAPL { - aa.unit.Rotation.DoNextAction(sim) - } else { - aa.agent.OnAutoAttack(sim, aa.rangedAuto) - } - } -} - -func (aa *AutoAttacks) updateMeleeDurations() { - aa.curMHSwingDuration = DurationFromSeconds(aa.mh.SwingSpeed / aa.curMeleeSpeed) - if aa.isDualWielding { - aa.curOHSwingDuration = DurationFromSeconds(aa.oh.SwingSpeed / aa.curMeleeSpeed) - } -} - -func (aa *AutoAttacks) UpdateRangedDuration() { - aa.curRangedSwingDuration = DurationFromSeconds(aa.ranged.SwingSpeed / aa.curRangedSpeed) + return aa.mh.replaceSwing(sim, mhSwingSpell) } func (aa *AutoAttacks) UpdateSwingTimers(sim *Simulation) { - if aa.autoSwingRanged { - aa.curRangedSpeed = aa.unit.RangedSwingSpeed() - aa.UpdateRangedDuration() + if aa.ranged != nil { + aa.ranged.curSwingSpeed = aa.ranged.unit.RangedSwingSpeed() + aa.ranged.updateSwingDuration() // ranged attack speed changes aren't applied mid-"swing" } - if aa.autoSwingMelee { - oldSwingSpeed := aa.curMeleeSpeed + if aa.mh != nil { + oldSwingSpeed := aa.mh.curSwingSpeed - aa.curMeleeSpeed = aa.unit.SwingSpeed() - aa.updateMeleeDurations() + aa.mh.curSwingSpeed = aa.mh.unit.SwingSpeed() + aa.mh.updateSwingDuration() - f := oldSwingSpeed / aa.curMeleeSpeed + f := oldSwingSpeed / aa.mh.curSwingSpeed - if remainingSwingTime := aa.mainhandSwingAt - sim.CurrentTime; remainingSwingTime > 0 { - aa.mainhandSwingAt = sim.CurrentTime + time.Duration(float64(remainingSwingTime)*f) + if remainingSwingTime := aa.mh.swingAt - sim.CurrentTime; remainingSwingTime > 0 { + aa.mh.swingAt = sim.CurrentTime + time.Duration(float64(remainingSwingTime)*f) } - if aa.isDualWielding { - if remainingSwingTime := aa.offhandSwingAt - sim.CurrentTime; remainingSwingTime > 0 { - aa.offhandSwingAt = sim.CurrentTime + time.Duration(float64(remainingSwingTime)*f) + if aa.oh != nil { + aa.oh.curSwingSpeed = aa.mh.curSwingSpeed + aa.oh.updateSwingDuration() + + if remainingSwingTime := aa.oh.swingAt - sim.CurrentTime; remainingSwingTime > 0 { + aa.oh.swingAt = sim.CurrentTime + time.Duration(float64(remainingSwingTime)*f) } } @@ -692,7 +654,7 @@ func (aa *AutoAttacks) UpdateSwingTimers(sim *Simulation) { // StopMeleeUntil should be used whenever a non-melee spell is cast. It stops melee, then restarts it // at end of cast, but with a reset swing timer (as if swings had just landed). func (aa *AutoAttacks) StopMeleeUntil(sim *Simulation, readyAt time.Duration, desyncOH bool) { - if !aa.autoSwingMelee { // if not auto swinging, don't auto restart. + if aa.mh == nil { // if not auto swinging, don't auto restart. return } @@ -715,12 +677,12 @@ func (aa *AutoAttacks) restartMelee(sim *Simulation, desyncOH bool) { aa.autoSwingCancelled = false - aa.mainhandSwingAt = sim.CurrentTime + aa.curMHSwingDuration - if aa.isDualWielding { - aa.offhandSwingAt = sim.CurrentTime + aa.curOHSwingDuration + aa.mh.swingAt = sim.CurrentTime + aa.mh.curSwingDuration + if aa.oh != nil { + aa.oh.swingAt = sim.CurrentTime + aa.oh.curSwingDuration if desyncOH { // Used by warrior to desync offhand after unglyphed Shattering Throw. - aa.offhandSwingAt += aa.curOHSwingDuration / 2 + aa.oh.swingAt += aa.oh.curSwingDuration / 2 } } @@ -733,31 +695,30 @@ func (aa *AutoAttacks) DelayMeleeBy(sim *Simulation, delay time.Duration) { return } - aa.mainhandSwingAt += delay - if aa.isDualWielding { - aa.offhandSwingAt += delay + aa.mh.swingAt += delay + if aa.oh != nil { + aa.oh.swingAt += delay } aa.rescheduleMelee(sim) } func (aa *AutoAttacks) DelayRangedUntil(sim *Simulation, readyAt time.Duration) { - if readyAt <= aa.rangedSwingAt { + if readyAt <= aa.ranged.swingAt { return } - aa.rangedSwingAt = readyAt + aa.ranged.swingAt = readyAt aa.rescheduleRanged(sim) } // Returns the time at which the next attack will occur. func (aa *AutoAttacks) NextAttackAt() time.Duration { - if aa.isDualWielding && aa.offhandSwingAt < aa.mainhandSwingAt { - return aa.offhandSwingAt - } else { - return aa.mainhandSwingAt + if aa.oh != nil && aa.oh.swingAt < aa.mh.swingAt { + return aa.oh.swingAt } + return aa.mh.swingAt } type PPMManager struct { @@ -785,29 +746,29 @@ func (ppmm *PPMManager) Chance(procMask ProcMask) float64 { } func (aa *AutoAttacks) NewPPMManager(ppm float64, procMask ProcMask) PPMManager { - if !aa.autoSwingMelee && !aa.autoSwingRanged { + if aa.mh == nil && aa.ranged == nil { return PPMManager{} } ppmm := PPMManager{procMasks: make([]ProcMask, 0, 2), procChances: make([]float64, 0, 2)} - mergeOrAppend := func(speed float64, mask ProcMask) { - if speed == 0 || mask == 0 { + mergeOrAppend := func(wa *WeaponAttack, mask ProcMask) { + if wa == nil || mask == 0 { return } - if i := slices.Index(ppmm.procChances, speed); i != -1 { + if i := slices.Index(ppmm.procChances, wa.SwingSpeed); i != -1 { ppmm.procMasks[i] |= mask return } ppmm.procMasks = append(ppmm.procMasks, mask) - ppmm.procChances = append(ppmm.procChances, speed) + ppmm.procChances = append(ppmm.procChances, wa.SwingSpeed) } - mergeOrAppend(aa.mh.SwingSpeed, procMask&^ProcMaskRanged&^ProcMaskMeleeOH) // "everything else", even if not explicitly flagged MH - mergeOrAppend(aa.oh.SwingSpeed, procMask&ProcMaskMeleeOH) - mergeOrAppend(aa.ranged.SwingSpeed, procMask&ProcMaskRanged) + mergeOrAppend(aa.mh, procMask&^ProcMaskRanged&^ProcMaskMeleeOH) // "everything else", even if not explicitly flagged MH + mergeOrAppend(aa.oh, procMask&ProcMaskMeleeOH) + mergeOrAppend(aa.ranged, procMask&ProcMaskRanged) for i := range ppmm.procChances { ppmm.procChances[i] *= ppm / 60 @@ -820,7 +781,7 @@ func (aa *AutoAttacks) NewPPMManager(ppm float64, procMask ProcMask) PPMManager // Using NewPPMManager() is preferred; this function should only be used when // the attacker is not known at initialization time. func (aa *AutoAttacks) PPMProc(sim *Simulation, ppm float64, procMask ProcMask, label string, spell *Spell) bool { - if !aa.autoSwingMelee && !aa.autoSwingRanged { + if aa.mh == nil && aa.ranged == nil { return false } @@ -836,7 +797,7 @@ func (aa *AutoAttacks) PPMProc(sim *Simulation, ppm float64, procMask ProcMask, } func (unit *Unit) applyParryHaste() { - if !unit.PseudoStats.ParryHaste || !unit.AutoAttacks.autoSwingMelee { + if !unit.PseudoStats.ParryHaste || unit.AutoAttacks.mh == nil { return } @@ -851,8 +812,8 @@ func (unit *Unit) applyParryHaste() { return } - remainingTime := aura.Unit.AutoAttacks.mainhandSwingAt - sim.CurrentTime - swingSpeed := aura.Unit.AutoAttacks.curMHSwingDuration + remainingTime := aura.Unit.AutoAttacks.mh.swingAt - sim.CurrentTime + swingSpeed := aura.Unit.AutoAttacks.mh.curSwingDuration minRemainingTime := time.Duration(float64(swingSpeed) * 0.2) // 20% of Swing Speed defaultReduction := minRemainingTime * 2 // 40% of Swing Speed @@ -861,12 +822,12 @@ func (unit *Unit) applyParryHaste() { } parryHasteReduction := min(defaultReduction, remainingTime-minRemainingTime) - newReadyAt := aura.Unit.AutoAttacks.mainhandSwingAt - parryHasteReduction + newReadyAt := aura.Unit.AutoAttacks.mh.swingAt - parryHasteReduction if sim.Log != nil { aura.Unit.Log(sim, "MH Swing reduced by %s due to parry haste, will now occur at %s", parryHasteReduction, newReadyAt) } - aura.Unit.AutoAttacks.mainhandSwingAt = newReadyAt + aura.Unit.AutoAttacks.mh.swingAt = newReadyAt aura.Unit.AutoAttacks.rescheduleMelee(sim) }, }) diff --git a/sim/core/character.go b/sim/core/character.go index 20bd013479..4e76833981 100644 --- a/sim/core/character.go +++ b/sim/core/character.go @@ -624,16 +624,15 @@ func (character *Character) doneIteration(sim *Simulation) { } func (character *Character) GetPseudoStatsProto() []float64 { - vals := make([]float64, stats.PseudoStatsLen) - vals[proto.PseudoStat_PseudoStatMainHandDps] = character.AutoAttacks.MH().DPS() - vals[proto.PseudoStat_PseudoStatOffHandDps] = character.AutoAttacks.OH().DPS() - vals[proto.PseudoStat_PseudoStatRangedDps] = character.AutoAttacks.Ranged().DPS() - vals[proto.PseudoStat_PseudoStatBlockValueMultiplier] = character.PseudoStats.BlockValueMultiplier - // Base values are modified by Enemy attackTables, but we display for LVL 80 enemy as paperdoll default - vals[proto.PseudoStat_PseudoStatDodge] = character.PseudoStats.BaseDodge + character.GetDiminishedDodgeChance() - vals[proto.PseudoStat_PseudoStatParry] = character.PseudoStats.BaseParry + character.GetDiminishedParryChance() - //vals[proto.PseudoStat_PseudoStatMiss] = 0.05 + character.GetDiminishedMissChance() + character.PseudoStats.ReducedPhysicalHitTakenChance - return vals + return []float64{ + proto.PseudoStat_PseudoStatMainHandDps: character.AutoAttacks.MH().DPS(), + proto.PseudoStat_PseudoStatOffHandDps: character.AutoAttacks.OH().DPS(), + proto.PseudoStat_PseudoStatRangedDps: character.AutoAttacks.Ranged().DPS(), + proto.PseudoStat_PseudoStatBlockValueMultiplier: character.PseudoStats.BlockValueMultiplier, + // Base values are modified by Enemy attackTables, but we display for LVL 80 enemy as paperdoll default + proto.PseudoStat_PseudoStatDodge: character.PseudoStats.BaseDodge + character.GetDiminishedDodgeChance(), + proto.PseudoStat_PseudoStatParry: character.PseudoStats.BaseParry + character.GetDiminishedParryChance(), + } } func (character *Character) GetMetricsProto() *proto.UnitMetrics { diff --git a/sim/core/item_swaps.go b/sim/core/item_swaps.go index dfd8e23338..b14b335820 100644 --- a/sim/core/item_swaps.go +++ b/sim/core/item_swaps.go @@ -208,8 +208,6 @@ func (swap *ItemSwap) swapWeapon(slot proto.ItemSlot) { character.AutoAttacks.SetRanged(character.WeaponFromRanged(swap.rangedCritMultiplier)) } } - - character.AutoAttacks.isDualWielding = character.MainHand().SwingSpeed != 0 && character.OffHand().SwingSpeed != 0 } func (swap *ItemSwap) finalize() { diff --git a/sim/druid/forms.go b/sim/druid/forms.go index 71d00bf944..df31ee17dd 100644 --- a/sim/druid/forms.go +++ b/sim/druid/forms.go @@ -149,7 +149,7 @@ func (druid *Druid) registerCatFormSpell() { } if !druid.Env.MeasuringStats { - druid.AutoAttacks.ReplaceMHSwing = nil + druid.AutoAttacks.SetReplaceMHSwing(nil) druid.AutoAttacks.EnableAutoSwing(sim) druid.manageCooldownsEnabled() druid.UpdateManaRegenRates() @@ -181,7 +181,7 @@ func (druid *Druid) registerCatFormSpell() { } if !druid.Env.MeasuringStats { - druid.AutoAttacks.ReplaceMHSwing = nil + druid.AutoAttacks.SetReplaceMHSwing(nil) druid.AutoAttacks.EnableAutoSwing(sim) druid.manageCooldownsEnabled() druid.UpdateManaRegenRates() @@ -292,7 +292,7 @@ func (druid *Druid) registerBearFormSpell() { druid.GainHealth(sim, healthFrac*druid.MaxHealth()-druid.CurrentHealth(), healthMetrics) if !druid.Env.MeasuringStats { - druid.AutoAttacks.ReplaceMHSwing = druid.ReplaceBearMHFunc + druid.AutoAttacks.SetReplaceMHSwing(druid.ReplaceBearMHFunc) druid.AutoAttacks.EnableAutoSwing(sim) druid.manageCooldownsEnabled() @@ -324,7 +324,7 @@ func (druid *Druid) registerBearFormSpell() { druid.RemoveHealth(sim, druid.CurrentHealth()-healthFrac*druid.MaxHealth()) if !druid.Env.MeasuringStats { - druid.AutoAttacks.ReplaceMHSwing = nil + druid.AutoAttacks.SetReplaceMHSwing(nil) druid.AutoAttacks.EnableAutoSwing(sim) druid.manageCooldownsEnabled() diff --git a/sim/encounters/ulduar/hodir_ai.go b/sim/encounters/ulduar/hodir_ai.go index 05cf72ceef..234271a541 100644 --- a/sim/encounters/ulduar/hodir_ai.go +++ b/sim/encounters/ulduar/hodir_ai.go @@ -337,12 +337,12 @@ func (ai *HodirAI) registerFrozenBlowSpell(target *core.Target) { }) // Replace MH Hit when under Frozen Blows buff - ai.Target.Unit.AutoAttacks.ReplaceMHSwing = func(sim *core.Simulation, mhSwingSpell *core.Spell) *core.Spell { + ai.Target.Unit.AutoAttacks.SetReplaceMHSwing(func(sim *core.Simulation, mhSwingSpell *core.Spell) *core.Spell { if ai.FrozenBlowsAura.IsActive() { return ai.FrozenBlowsAuto } return mhSwingSpell - } + }) ai.FrozenBlowsAuto = target.GetOrRegisterSpell(core.SpellConfig{ ActionID: core.ActionID{SpellID: core.TernaryInt32(ai.raidSize == 25, 63511, 62867)}.WithTag(1), diff --git a/sim/shaman/enhancement/enhancement.go b/sim/shaman/enhancement/enhancement.go index cf7278ad45..8ac1b78112 100644 --- a/sim/shaman/enhancement/enhancement.go +++ b/sim/shaman/enhancement/enhancement.go @@ -160,25 +160,25 @@ func (enh *EnhancementShaman) ApplySyncType(syncType proto.ShamanSyncType) { switch syncType { case proto.ShamanSyncType_SyncMainhandOffhandSwings: - enh.AutoAttacks.ReplaceMHSwing = func(sim *core.Simulation, mhSwingSpell *core.Spell) *core.Spell { + enh.AutoAttacks.SetReplaceMHSwing(func(sim *core.Simulation, mhSwingSpell *core.Spell) *core.Spell { if aa := &enh.AutoAttacks; aa.OffhandSwingAt()-sim.CurrentTime > FlurryICD { if nextMHSwingAt := sim.CurrentTime + aa.MainhandSwingSpeed(); nextMHSwingAt > aa.OffhandSwingAt() { aa.SetOffhandSwingAt(nextMHSwingAt) } } return mhSwingSpell - } + }) case proto.ShamanSyncType_DelayOffhandSwings: - enh.AutoAttacks.ReplaceMHSwing = func(sim *core.Simulation, mhSwingSpell *core.Spell) *core.Spell { + enh.AutoAttacks.SetReplaceMHSwing(func(sim *core.Simulation, mhSwingSpell *core.Spell) *core.Spell { if aa := &enh.AutoAttacks; aa.OffhandSwingAt()-sim.CurrentTime > FlurryICD { if nextMHSwingAt := sim.CurrentTime + aa.MainhandSwingSpeed() + 100*time.Millisecond; nextMHSwingAt > aa.OffhandSwingAt() { aa.SetOffhandSwingAt(nextMHSwingAt) } } return mhSwingSpell - } + }) default: - enh.AutoAttacks.ReplaceMHSwing = nil + enh.AutoAttacks.SetReplaceMHSwing(nil) } } diff --git a/sim/warrior/deep_wounds.go b/sim/warrior/deep_wounds.go index 08c8948430..ebdf256998 100644 --- a/sim/warrior/deep_wounds.go +++ b/sim/warrior/deep_wounds.go @@ -50,27 +50,27 @@ func (warrior *Warrior) applyDeepWounds() { return } if result.Outcome.Matches(core.OutcomeCrit) { - warrior.procDeepWounds(sim, result.Target, spell.IsMH()) + warrior.procDeepWounds(sim, result.Target, spell.IsOH()) } }, }) } -func (warrior *Warrior) procDeepWounds(sim *core.Simulation, target *core.Unit, isMh bool) { +func (warrior *Warrior) procDeepWounds(sim *core.Simulation, target *core.Unit, isOh bool) { dot := warrior.DeepWounds.Dot(target) outstandingDamage := core.TernaryFloat64(dot.IsActive(), dot.SnapshotBaseDamage*float64(dot.NumberOfTicks-dot.TickCount), 0) attackTable := warrior.AttackTables[target.UnitIndex] var awd float64 - if isMh { - adm := warrior.AutoAttacks.MHAuto().AttackerDamageMultiplier(attackTable) - tdm := warrior.AutoAttacks.MHAuto().TargetDamageMultiplier(attackTable, false) - awd = (warrior.AutoAttacks.MH().CalculateAverageWeaponDamage(dot.Spell.MeleeAttackPower()) + dot.Spell.BonusWeaponDamage()) * adm * tdm - } else { + if isOh { adm := warrior.AutoAttacks.OHAuto().AttackerDamageMultiplier(attackTable) tdm := warrior.AutoAttacks.OHAuto().TargetDamageMultiplier(attackTable, false) awd = ((warrior.AutoAttacks.OH().CalculateAverageWeaponDamage(dot.Spell.MeleeAttackPower()) * 0.5) + dot.Spell.BonusWeaponDamage()) * adm * tdm + } else { // MH, Ranged (e.g. Thunder Clap) + adm := warrior.AutoAttacks.MHAuto().AttackerDamageMultiplier(attackTable) + tdm := warrior.AutoAttacks.MHAuto().TargetDamageMultiplier(attackTable, false) + awd = (warrior.AutoAttacks.MH().CalculateAverageWeaponDamage(dot.Spell.MeleeAttackPower()) + dot.Spell.BonusWeaponDamage()) * adm * tdm } newDamage := awd * 0.16 * float64(warrior.Talents.DeepWounds) diff --git a/sim/warrior/dps/TestArms.results b/sim/warrior/dps/TestArms.results index 98e37f5236..8a5bca164b 100644 --- a/sim/warrior/dps/TestArms.results +++ b/sim/warrior/dps/TestArms.results @@ -932,8 +932,8 @@ dps_results: { dps_results: { key: "TestArms-Settings-Human-p1_arms-Basic-arms-FullBuffs-LongMultiTarget" value: { - dps: 10652.07342 - tps: 10007.49125 + dps: 11906.57829 + tps: 11011.09514 } } dps_results: { @@ -953,8 +953,8 @@ dps_results: { dps_results: { key: "TestArms-Settings-Human-p1_arms-Basic-arms-NoBuffs-LongMultiTarget" value: { - dps: 5528.79397 - tps: 5348.07684 + dps: 6198.0661 + tps: 5883.49454 } } dps_results: { @@ -1016,8 +1016,8 @@ dps_results: { dps_results: { key: "TestArms-Settings-Orc-p1_arms-Basic-arms-FullBuffs-LongMultiTarget" value: { - dps: 10689.87998 - tps: 10049.09221 + dps: 11940.61768 + tps: 11049.68237 } } dps_results: { @@ -1037,8 +1037,8 @@ dps_results: { dps_results: { key: "TestArms-Settings-Orc-p1_arms-Basic-arms-NoBuffs-LongMultiTarget" value: { - dps: 5670.62396 - tps: 5482.18162 + dps: 6345.88031 + tps: 6022.3867 } } dps_results: { diff --git a/sim/warrior/protection/TestProtectionWarrior.results b/sim/warrior/protection/TestProtectionWarrior.results index 9cabbb145e..dee608b4ea 100644 --- a/sim/warrior/protection/TestProtectionWarrior.results +++ b/sim/warrior/protection/TestProtectionWarrior.results @@ -950,22 +950,22 @@ dps_results: { dps_results: { key: "TestProtectionWarrior-Settings-Human-p1_balanced-Basic-default-NoBuffs-LongMultiTarget" value: { - dps: 1050.46307 - tps: 3335.71088 + dps: 1084.94322 + tps: 3407.20547 } } dps_results: { key: "TestProtectionWarrior-Settings-Human-p1_balanced-Basic-default-NoBuffs-LongSingleTarget" value: { - dps: 865.20912 - tps: 2483.44764 + dps: 866.97075 + tps: 2487.10038 } } dps_results: { key: "TestProtectionWarrior-Settings-Human-p1_balanced-Basic-default-NoBuffs-ShortSingleTarget" value: { - dps: 831.10607 - tps: 2403.87441 + dps: 833.49622 + tps: 2408.83039 } } dps_results: { @@ -992,22 +992,22 @@ dps_results: { dps_results: { key: "TestProtectionWarrior-Settings-Orc-p1_balanced-Basic-default-NoBuffs-LongMultiTarget" value: { - dps: 1063.03306 - tps: 3372.37903 + dps: 1097.36592 + tps: 3443.56821 } } dps_results: { key: "TestProtectionWarrior-Settings-Orc-p1_balanced-Basic-default-NoBuffs-LongSingleTarget" value: { - dps: 880.11232 - tps: 2527.63516 + dps: 881.93425 + tps: 2531.41294 } } dps_results: { key: "TestProtectionWarrior-Settings-Orc-p1_balanced-Basic-default-NoBuffs-ShortSingleTarget" value: { - dps: 853.54235 - tps: 2468.41035 + dps: 854.88707 + tps: 2471.19863 } } dps_results: {