From 2b93fc0ef2444a56138862b57ca30cbca058e60f Mon Sep 17 00:00:00 2001 From: James Tanner Date: Tue, 3 Oct 2023 21:04:43 -0700 Subject: [PATCH] Refactor expected damage functions to split initial and tick calcs --- sim/core/spell.go | 28 +- sim/druid/feral/rotation.go | 5 +- sim/druid/rake.go | 18 +- sim/druid/shred.go | 2 +- sim/priest/devouring_plague.go | 2 +- sim/priest/mind_blast.go | 2 +- sim/priest/mind_flay.go | 3 +- sim/priest/mind_sear.go | 3 +- sim/priest/shadow/experimental_rotation.go | 445 --------------------- sim/priest/shadow/rotation.go | 14 +- sim/priest/shadow_word_death.go | 2 +- sim/priest/shadow_word_pain.go | 2 +- sim/priest/vampiric_touch.go | 2 +- sim/warlock/corruption.go | 2 +- sim/warlock/drain_soul.go | 2 +- sim/warlock/rotation.go | 8 +- 16 files changed, 54 insertions(+), 486 deletions(-) delete mode 100644 sim/priest/shadow/experimental_rotation.go diff --git a/sim/core/spell.go b/sim/core/spell.go index d23aea9ccf..0989b9655a 100644 --- a/sim/core/spell.go +++ b/sim/core/spell.go @@ -48,7 +48,8 @@ type SpellConfig struct { ApplyEffects ApplySpellResults // Optional field. Calculates expected average damage. - ExpectedDamage ExpectedDamageCalculator + ExpectedInitialDamage ExpectedDamageCalculator + ExpectedTickDamage ExpectedDamageCalculator Dot DotConfig Hot DotConfig @@ -98,7 +99,8 @@ type Spell struct { ApplyEffects ApplySpellResults // Optional field. Calculates expected average damage. - expectedDamageInternal ExpectedDamageCalculator + expectedInitialDamageInternal ExpectedDamageCalculator + expectedTickDamageInternal ExpectedDamageCalculator // The current or most recent cast data. CurCast Cast @@ -197,7 +199,8 @@ func (unit *Unit) RegisterSpell(config SpellConfig) *Spell { ApplyEffects: config.ApplyEffects, - expectedDamageInternal: config.ExpectedDamage, + expectedInitialDamageInternal: config.ExpectedInitialDamage, + expectedTickDamageInternal: config.ExpectedTickDamage, BonusHitRating: config.BonusHitRating, BonusCritRating: config.BonusCritRating, @@ -535,21 +538,28 @@ func (spell *Spell) ApplyAOEThreat(threatAmount float64) { spell.ApplyAOEThreatIgnoreMultipliers(threatAmount * spell.Unit.PseudoStats.ThreatMultiplier) } -func (spell *Spell) expectedDamageHelper(sim *Simulation, target *Unit, useSnapshot bool) float64 { - result := spell.expectedDamageInternal(sim, target, spell, useSnapshot) +func (spell *Spell) finalizeExpectedDamage(result *SpellResult) { if !spell.SpellSchool.Matches(SpellSchoolPhysical) { result.Damage /= result.ResistanceMultiplier result.Damage *= AverageMagicPartialResistMultiplier result.ResistanceMultiplier = AverageMagicPartialResistMultiplier } result.inUse = false +} +func (spell *Spell) ExpectedInitialDamage(sim *Simulation, target *Unit) float64 { + result := spell.expectedInitialDamageInternal(sim, target, spell, false) + spell.finalizeExpectedDamage(result) return result.Damage } -func (spell *Spell) ExpectedDamage(sim *Simulation, target *Unit) float64 { - return spell.expectedDamageHelper(sim, target, false) +func (spell *Spell) ExpectedTickDamage(sim *Simulation, target *Unit) float64 { + result := spell.expectedTickDamageInternal(sim, target, spell, false) + spell.finalizeExpectedDamage(result) + return result.Damage } -func (spell *Spell) ExpectedDamageFromCurrentSnapshot(sim *Simulation, target *Unit) float64 { - return spell.expectedDamageHelper(sim, target, true) +func (spell *Spell) ExpectedTickDamageFromCurrentSnapshot(sim *Simulation, target *Unit) float64 { + result := spell.expectedTickDamageInternal(sim, target, spell, true) + spell.finalizeExpectedDamage(result) + return result.Damage } // Time until either the cast is finished or GCD is ready again, whichever is longer diff --git a/sim/druid/feral/rotation.go b/sim/druid/feral/rotation.go index 2966d68799..4046faac3c 100644 --- a/sim/druid/feral/rotation.go +++ b/sim/druid/feral/rotation.go @@ -215,8 +215,9 @@ func (cat *FeralDruid) calcBuilderDpe(sim *core.Simulation) (float64, float64) { // Calculate current damage-per-Energy of Rake vs. Shred. Used to // determine whether Rake is worth casting when player stats change upon a // dynamic proc occurring - shredDpc := cat.Shred.ExpectedDamage(sim, cat.CurrentTarget) - rakeDpc := cat.Rake.ExpectedDamage(sim, cat.CurrentTarget) + shredDpc := cat.Shred.ExpectedInitialDamage(sim, cat.CurrentTarget) + potentialRakeTicks := min(cat.Rake.CurDot().NumberOfTicks, int32(sim.GetRemainingDuration()/time.Second*3)) + rakeDpc := cat.Rake.ExpectedInitialDamage(sim, cat.CurrentTarget) + cat.Rake.ExpectedTickDamage(sim, cat.CurrentTarget)*float64(potentialRakeTicks) return rakeDpc / cat.Rake.DefaultCast.Cost, shredDpc / cat.Shred.DefaultCast.Cost } diff --git a/sim/druid/rake.go b/sim/druid/rake.go index 953e6cc634..45de0a7105 100644 --- a/sim/druid/rake.go +++ b/sim/druid/rake.go @@ -7,7 +7,6 @@ import ( ) func (druid *Druid) registerRakeSpell() { - numTicks := 3 + core.TernaryInt32(druid.HasSetBonus(ItemSetMalfurionsBattlegear, 2), 1, 0) dotCanCrit := druid.HasSetBonus(ItemSetLasherweaveBattlegear, 4) druid.Rake = druid.RegisterSpell(Cat, core.SpellConfig{ @@ -36,7 +35,7 @@ func (druid *Druid) registerRakeSpell() { Label: "Rake", Duration: time.Second * 9, }), - NumberOfTicks: numTicks, + NumberOfTicks: 3 + core.TernaryInt32(druid.HasSetBonus(ItemSetMalfurionsBattlegear, 2), 1, 0), TickLength: time.Second * 3, OnSnapshot: func(sim *core.Simulation, target *core.Unit, dot *core.Dot, isRollover bool) { dot.SnapshotBaseDamage = 358 + 0.06*dot.Spell.MeleeAttackPower() @@ -69,12 +68,18 @@ func (druid *Druid) registerRakeSpell() { } }, - ExpectedDamage: func(sim *core.Simulation, target *core.Unit, spell *core.Spell, _ bool) *core.SpellResult { + ExpectedInitialDamage: func(sim *core.Simulation, target *core.Unit, spell *core.Spell, _ bool) *core.SpellResult { baseDamage := 176 + 0.01*spell.MeleeAttackPower() - potentialTicks := min(numTicks, int32(sim.GetRemainingDuration()/time.Second*3)) - tickBase := (358 + 0.06*spell.MeleeAttackPower()) * float64(potentialTicks) - initial := spell.CalcPeriodicDamage(sim, target, baseDamage, spell.OutcomeExpectedMagicAlwaysHit) + + attackTable := spell.Unit.AttackTables[target.UnitIndex] + critChance := spell.PhysicalCritChance(attackTable) + critMod := (critChance * (spell.CritMultiplier - 1)) + initial.Damage *= 1 + critMod + return initial + }, + ExpectedTickDamage: func(sim *core.Simulation, target *core.Unit, spell *core.Spell, _ bool) *core.SpellResult { + tickBase := (358 + 0.06*spell.MeleeAttackPower()) ticks := spell.CalcPeriodicDamage(sim, target, tickBase, spell.OutcomeExpectedMagicAlwaysHit) attackTable := spell.Unit.AttackTables[target.UnitIndex] @@ -85,7 +90,6 @@ func (druid *Druid) registerRakeSpell() { ticks.Damage *= 1 + critMod } - ticks.Damage += initial.Damage * (1 + critMod) return ticks }, }) diff --git a/sim/druid/shred.go b/sim/druid/shred.go index 74505b81cd..dd21f5a370 100644 --- a/sim/druid/shred.go +++ b/sim/druid/shred.go @@ -71,7 +71,7 @@ func (druid *Druid) registerShredSpell() { spell.IssueRefund(sim) } }, - ExpectedDamage: func(sim *core.Simulation, target *core.Unit, spell *core.Spell, _ bool) *core.SpellResult { + ExpectedInitialDamage: func(sim *core.Simulation, target *core.Unit, spell *core.Spell, _ bool) *core.SpellResult { baseDamage := flatDamageBonus + spell.Unit.AutoAttacks.MH.CalculateAverageWeaponDamage(spell.MeleeAttackPower()) + spell.BonusWeaponDamage() modifier := 1.0 diff --git a/sim/priest/devouring_plague.go b/sim/priest/devouring_plague.go index 5588923fc9..6dfb265745 100644 --- a/sim/priest/devouring_plague.go +++ b/sim/priest/devouring_plague.go @@ -110,7 +110,7 @@ func (priest *Priest) registerDevouringPlagueSpell() { spell.Dot(target).Apply(sim) } }, - ExpectedDamage: func(sim *core.Simulation, target *core.Unit, spell *core.Spell, useSnapshot bool) *core.SpellResult { + ExpectedTickDamage: func(sim *core.Simulation, target *core.Unit, spell *core.Spell, useSnapshot bool) *core.SpellResult { if useSnapshot { dot := spell.Dot(target) if priest.Talents.Shadowform { diff --git a/sim/priest/mind_blast.go b/sim/priest/mind_blast.go index 721eb6d5dc..dcfc0f6b97 100644 --- a/sim/priest/mind_blast.go +++ b/sim/priest/mind_blast.go @@ -84,7 +84,7 @@ func (priest *Priest) registerMindBlastSpell() { } } }, - ExpectedDamage: func(sim *core.Simulation, target *core.Unit, spell *core.Spell, _ bool) *core.SpellResult { + ExpectedInitialDamage: func(sim *core.Simulation, target *core.Unit, spell *core.Spell, _ bool) *core.SpellResult { baseDamage := (997.0+1053.0)/2 + spellCoeff*spell.SpellPower() return spell.CalcDamage(sim, target, baseDamage, spell.OutcomeExpectedMagicHitAndCrit) }, diff --git a/sim/priest/mind_flay.go b/sim/priest/mind_flay.go index 63a62da790..94d551af28 100644 --- a/sim/priest/mind_flay.go +++ b/sim/priest/mind_flay.go @@ -146,9 +146,8 @@ func (priest *Priest) newMindFlaySpell(numTicksIdx int32) *core.Spell { } spell.DealOutcome(sim, result) }, - ExpectedDamage: func(sim *core.Simulation, target *core.Unit, spell *core.Spell, _ bool) *core.SpellResult { + ExpectedTickDamage: func(sim *core.Simulation, target *core.Unit, spell *core.Spell, _ bool) *core.SpellResult { baseDamage := 588.0/3 + miseryCoeff*spell.SpellPower() - baseDamage *= float64(numTicks) if priest.Talents.Shadowform { return spell.CalcPeriodicDamage(sim, target, baseDamage, spell.OutcomeExpectedMagicCrit) diff --git a/sim/priest/mind_sear.go b/sim/priest/mind_sear.go index c0d2857735..6b414ca72f 100644 --- a/sim/priest/mind_sear.go +++ b/sim/priest/mind_sear.go @@ -85,9 +85,8 @@ func (priest *Priest) newMindSearSpell(numTicksIdx int32) *core.Spell { } } }, - ExpectedDamage: func(sim *core.Simulation, target *core.Unit, spell *core.Spell, _ bool) *core.SpellResult { + ExpectedTickDamage: func(sim *core.Simulation, target *core.Unit, spell *core.Spell, _ bool) *core.SpellResult { baseDamage := sim.Roll(212, 228) + miseryCoeff*spell.SpellPower() - baseDamage *= float64(numTicks) return spell.CalcPeriodicDamage(sim, target, baseDamage, spell.OutcomeExpectedMagicCrit) }, }) diff --git a/sim/priest/shadow/experimental_rotation.go b/sim/priest/shadow/experimental_rotation.go deleted file mode 100644 index ee60819622..0000000000 --- a/sim/priest/shadow/experimental_rotation.go +++ /dev/null @@ -1,445 +0,0 @@ -package shadow - -import ( - //"fmt" - "time" - - "github.com/wowsims/wotlk/sim/core" -) - -const ( - MindBlastIdx int = iota - ShadowWordDeathIdx - DevouringPlagueIdx - VampiricTouchIdx - ShadowWordPainIdx - MindFlay1Idx - MindFlay2Idx - MindFlay3Idx - SpellLen -) - -func (spriest *ShadowPriest) experimentalRotation(sim *core.Simulation) { - spell := spriest.chooseSpellExperimental(sim) - - if spell == spriest.MindFlay[3] && spriest.InnerFocus != nil && spriest.InnerFocus.IsReady(sim) && spriest.ShadowWeavingAura.GetStacks() == 5 { - spriest.InnerFocus.Cast(sim, nil) - } - - if !spell.IsReady(sim) { - spriest.WaitUntil(sim, spell.ReadyAt()) - } else if success := spell.Cast(sim, spriest.CurrentTarget); !success { - spriest.WaitForMana(sim, spell.CurCast.Cost) - } -} - -func (spriest *ShadowPriest) chooseSpellExperimental(sim *core.Simulation) *core.Spell { - if !spriest.DevouringPlague.CurDot().IsActive() { - return spriest.DevouringPlague - } - - gcd := core.MaxDuration(core.GCDMin, spriest.ApplyCastSpeed(core.GCDDefault)) - vtCastTime := gcd - if spriest.VampiricTouch != nil && (!spriest.VampiricTouch.CurDot().IsActive() || sim.CurrentTime+vtCastTime >= spriest.VampiricTouch.CurDot().ExpiresAt()) { - return spriest.VampiricTouch - } - - if !spriest.ShadowWordPain.CurDot().IsActive() { - if spriest.CanRolloverSWP { - // At the beginning of the iteration, its better to wait for 5 stacks of weaving - // before taking the first snapshot. - stacks := spriest.ShadowWeavingAura.GetStacks() - if spriest.ShadowWeavingAura == nil || stacks == 5 { - return spriest.ShadowWordPain - } else if stacks == 2 && spriest.MindBlast.IsReady(sim) { - return spriest.MindBlast - } else if stacks >= 2 { - return spriest.MindFlay[5-stacks] - } else { - if spriest.options.UseMindBlast { - return spriest.MindBlast - } else if spriest.options.UseShadowWordDeath { - return spriest.ShadowWordDeath - } else { - return spriest.MindFlay[2] - } - } - } else { - return spriest.ShadowWordPain - } - } - - //if spriest.CanRolloverSWP { - // // If we can rollover SWP and we can get a sufficiently better snapshot, take it. - // snapshotDmg := spriest.ShadowWordPain.ExpectedDamageFromCurrentSnapshot(sim, spriest.CurrentTarget) - // newDmg := spriest.ShadowWordPain.ExpectedDamage(sim, spriest.CurrentTarget) - // if newDmg > snapshotDmg + 200 { - // if sim.Log != nil { - // spriest.Log(sim, "Better SWP snapshot, old: %0.02f, new: %0.02f", snapshotDmg, newDmg) - // } - // return spriest.ShadowWordPain - // } - //} - - // Time spent casting each spell. - castTime := [SpellLen]time.Duration{ - gcd, // MB - gcd, // SWD - gcd, // DP - gcd, // VT - gcd, // SWP - spriest.ApplyCastSpeed(spriest.MindFlay[1].DefaultCast.EffectiveTime()) + spriest.AverageMindFlayLatencyDelay(1, gcd), - spriest.ApplyCastSpeed(spriest.MindFlay[2].DefaultCast.EffectiveTime()) + spriest.AverageMindFlayLatencyDelay(2, gcd), - spriest.ApplyCastSpeed(spriest.MindFlay[3].DefaultCast.EffectiveTime()) + spriest.AverageMindFlayLatencyDelay(3, gcd), - } - // When the GCD would become ready again, for each cast. - castCompleteAt := [SpellLen]time.Duration{ - core.MaxDuration(sim.CurrentTime, spriest.MindBlast.ReadyAt()) + gcd, - core.MaxDuration(sim.CurrentTime, spriest.ShadowWordDeath.ReadyAt()) + gcd, - sim.CurrentTime + gcd, // DP - sim.CurrentTime + gcd, // VT - sim.CurrentTime + gcd, // SWP - sim.CurrentTime + spriest.ApplyCastSpeed(spriest.MindFlay[1].DefaultCast.EffectiveTime()) + spriest.AverageMindFlayLatencyDelay(1, gcd), - sim.CurrentTime + spriest.ApplyCastSpeed(spriest.MindFlay[2].DefaultCast.EffectiveTime()) + spriest.AverageMindFlayLatencyDelay(2, gcd), - sim.CurrentTime + spriest.ApplyCastSpeed(spriest.MindFlay[3].DefaultCast.EffectiveTime()) + spriest.AverageMindFlayLatencyDelay(3, gcd), - } - - // Time period over which a spell deals its full damage. - cadence := [SpellLen]time.Duration{ - spriest.MindBlast.CD.Duration, - spriest.ShadowWordDeath.CD.Duration, - spriest.DevouringPlague.CurDot().TickPeriod() * time.Duration(spriest.DevouringPlague.CurDot().NumberOfTicks), - spriest.VampiricTouch.CurDot().TickPeriod() * time.Duration(spriest.VampiricTouch.CurDot().NumberOfTicks), - spriest.ShadowWordPain.CurDot().TickPeriod() * time.Duration(spriest.ShadowWordPain.CurDot().NumberOfTicks), - castCompleteAt[MindFlay1Idx] - sim.CurrentTime, - castCompleteAt[MindFlay2Idx] - sim.CurrentTime, - castCompleteAt[MindFlay3Idx] - sim.CurrentTime, - } - - // Time after which casting the corresponding spell would be a delay of - // its cadence. - spellDelayStart := [SpellLen]time.Duration{ - core.MaxDuration(sim.CurrentTime, spriest.MindBlast.ReadyAt()), - core.MaxDuration(sim.CurrentTime, spriest.ShadowWordDeath.ReadyAt()), - core.MaxDuration(sim.CurrentTime, spriest.DevouringPlague.CurDot().ExpiresAt()), - core.MaxDuration(sim.CurrentTime, spriest.VampiricTouch.CurDot().ExpiresAt()-vtCastTime), - core.NeverExpires, // SWP rolls over, so it never gets delayed. - sim.CurrentTime, // MF1 - sim.CurrentTime, // MF2 - sim.CurrentTime, // MF3 - } - - dpTickDamage := spriest.DevouringPlague.ExpectedDamage(sim, spriest.CurrentTarget) - dpInitDamage := dpTickDamage * spriest.DpInitMultiplier - - // Total damage done by a cast of each spell. - spellDamage := [SpellLen]float64{ - 0, - 0, - dpInitDamage + 8*dpTickDamage, - spriest.VampiricTouch.ExpectedDamage(sim, spriest.CurrentTarget) * float64(spriest.VampiricTouch.CurDot().NumberOfTicks), - spriest.ShadowWordPain.ExpectedDamage(sim, spriest.CurrentTarget) * float64(spriest.ShadowWordPain.CurDot().NumberOfTicks), - 0, - 0, - 0, - } - if spriest.options.UseMindBlast { - spellDamage[MindBlastIdx] = spriest.MindBlast.ExpectedDamage(sim, spriest.CurrentTarget) - // Account for Glyph of Shadow and imp spirit tap - spellDamage[MindBlastIdx] += 100.0 * 30 - } - if spriest.options.UseShadowWordDeath { - spellDamage[ShadowWordDeathIdx] = spriest.ShadowWordDeath.ExpectedDamage(sim, spriest.CurrentTarget) - spellDamage[ShadowWordDeathIdx] += 100.0 * 0 - } - if spriest.Talents.MindFlay { - spellDamage[MindFlay1Idx] = spriest.MindFlay[1].ExpectedDamage(sim, spriest.CurrentTarget) - spellDamage[MindFlay2Idx] = spellDamage[MindFlay1Idx] * 2 - spellDamage[MindFlay3Idx] = spellDamage[MindFlay1Idx] * 3 - } - - spellDPS := [SpellLen]float64{ - spellDamage[MindBlastIdx] / cadence[MindBlastIdx].Seconds(), - spellDamage[ShadowWordDeathIdx] / cadence[ShadowWordDeathIdx].Seconds(), - spellDamage[DevouringPlagueIdx] / cadence[DevouringPlagueIdx].Seconds(), - spellDamage[VampiricTouchIdx] / cadence[VampiricTouchIdx].Seconds(), - spellDamage[ShadowWordPainIdx] / cadence[ShadowWordPainIdx].Seconds(), - spellDamage[MindFlay1Idx] / cadence[MindFlay1Idx].Seconds(), - spellDamage[MindFlay2Idx] / cadence[MindFlay2Idx].Seconds(), - spellDamage[MindFlay3Idx] / cadence[MindFlay3Idx].Seconds(), - } - - // These are the only efficient options worth considering. - mbCastTime := castTime[MindBlastIdx] - mf2CastTime := castTime[MindFlay2Idx] - mf3CastTime := castTime[MindFlay3Idx] - - //// For each spell option, find the combination of following spells which - //// causes the LEAST amount of delay on DP and VT, and record that delay. - //dpCastAt := spellDelayStart[DevouringPlagueIdx] - //vtCastAt := spellDelayStart[VampiricTouchIdx] - //scanUntil := core.MaxDuration(dpCastAt, vtCastAt) + gcd*3 - - //type PathOption struct { - // Spell *core.Spell - // Damage float64 - // DoneAt time.Duration - // CanMBAt time.Duration // When Mind Blast will be ready on this path. - - // PrevOption *PathOption - // DPRefreshed bool - // VTRefreshed bool - //} - - //pathOptions := []PathOption{ - // PathOption{ - // Spell: spriest.MindBlast, - // Damage: spellDamage[MindBlastIdx], - // DoneAt: castCompleteAt[MindBlastIdx], - // CanMBAt: castCompleteAt[MindBlastIdx] + spriest.MindBlast.CD.Duration, - // }, - // PathOption{ - // Spell: spriest.MindFlay[2], - // Damage: spellDamage[MindFlay2Idx], - // DoneAt: castCompleteAt[MindFlay2Idx], - // CanMBAt: spellDelayStart[MindBlastIdx], - // }, - // PathOption{ - // Spell: spriest.MindFlay[3], - // Damage: spellDamage[MindFlay3Idx], - // DoneAt: castCompleteAt[MindFlay3Idx], - // CanMBAt: spellDelayStart[MindBlastIdx], - // }, - //} - //cur := &pathOptions[0] - //if pathOptions[1].DoneAt < pathOptions[0].DoneAt { - // cur = &pathOptions[1] - //} - - ////var dotDelays [SpellLen]time.Duration - ////var maxDotDelay time.Duration - //for cur.DoneAt <= scanUntil { - // // Process the curTime, by adding new nodes for each cast option. - // if !cur.DPRefreshed && cur.DoneAt >= dpCastAt { - // pathOptions = append(pathOptions, PathOption{ - // Damage: cur.Damage - spellDPS[DevouringPlagueIdx] * (cur.DoneAt - dpCastAt).Seconds(), - // DoneAt: cur.DoneAt + gcd, - // CanMBAt: cur.CanMBAt, - // PrevOption: cur, - // DPRefreshed: true, - // VTRefreshed: cur.VTRefreshed, - // }) - // } else if !cur.VTRefreshed && cur.DoneAt >= vtCastAt { - // pathOptions = append(pathOptions, PathOption{ - // Damage: cur.Damage - spellDPS[VampiricTouchIdx] * (cur.DoneAt - vtCastAt).Seconds(), - // DoneAt: cur.DoneAt + gcd, - // CanMBAt: cur.CanMBAt, - // PrevOption: cur, - // DPRefreshed: cur.DPRefreshed, - // VTRefreshed: true, - // }) - // } else { - // pathOptions = append(pathOptions, PathOption{ - // Damage: cur.Damage + spellDamage[MindFlay3Idx], - // DoneAt: cur.DoneAt + mf3CastTime, - // CanMBAt: cur.CanMBAt, - // PrevOption: cur, - // DPRefreshed: cur.DPRefreshed, - // VTRefreshed: cur.VTRefreshed, - // }) - // pathOptions = append(pathOptions, PathOption{ - // Damage: cur.Damage + spellDamage[MindFlay2Idx], - // DoneAt: cur.DoneAt + mf2CastTime, - // CanMBAt: cur.CanMBAt, - // PrevOption: cur, - // DPRefreshed: cur.DPRefreshed, - // VTRefreshed: cur.VTRefreshed, - // }) - // if cur.DoneAt >= cur.CanMBAt { - // pathOptions = append(pathOptions, PathOption{ - // Damage: cur.Damage + spellDamage[MindBlastIdx], - // DoneAt: cur.DoneAt + mbCastTime, - // CanMBAt: cur.DoneAt + mbCastTime + spriest.MindBlast.CD.Duration, - // PrevOption: cur, - // DPRefreshed: cur.DPRefreshed, - // VTRefreshed: cur.VTRefreshed, - // }) - // } - // } - - // // Find the next cur, which is the smallest option greater than curTime. - // var bestOption *PathOption - // for i := 0; i < len(pathOptions); i++ { - // if pathOptions[i].DoneAt > cur.DoneAt { - // if bestOption == nil || pathOptions[i].DoneAt < bestOption.DoneAt || (pathOptions[i].DoneAt == bestOption.DoneAt && pathOptions[i].Damage > bestOption.Damage) { - // bestOption = &pathOptions[i] - // } - // } - // } - // cur = bestOption - //} - - //var bestOption *PathOption - //for i, _ := range pathOptions { - // option := &pathOptions[i] - // if option.DPRefreshed && option.VTRefreshed { - // if bestOption == nil || option.Damage > bestOption.Damage { - // bestOption = option - // } - // } - //} - //if bestOption == nil { - // panic(fmt.Sprintf("No best option, %d, %s\n", len(pathOptions), scanUntil - sim.CurrentTime)) - //} - - //for bestOption.PrevOption != nil { - // bestOption = bestOption.PrevOption - //} - //return bestOption.Spell - - // For each spell option, find the combination of following spells which - // causes the LEAST amount of delay on DP and VT, and record that delay. - dot1DelayAt := spellDelayStart[DevouringPlagueIdx] - dot2DelayAt := spellDelayStart[VampiricTouchIdx] - if dot1DelayAt > dot2DelayAt { - // Swap so that dot1DelayAt always comes before dot2DelayAt. - dot1DelayAt = spellDelayStart[VampiricTouchIdx] - dot2DelayAt = spellDelayStart[DevouringPlagueIdx] - } - - type PathOption struct { - DoneAt time.Duration - CanMBAt time.Duration // When Mind Blast will be ready on this path. - } - startingPathOptions := []PathOption{ - PathOption{ - DoneAt: castCompleteAt[MindBlastIdx], - CanMBAt: castCompleteAt[MindBlastIdx] + spriest.MindBlast.CD.Duration, - }, - PathOption{ - DoneAt: castCompleteAt[MindFlay2Idx], - CanMBAt: spellDelayStart[MindBlastIdx], - }, - PathOption{ - DoneAt: castCompleteAt[MindFlay3Idx], - CanMBAt: spellDelayStart[MindBlastIdx], - }, - } - - var dotDelays [SpellLen]time.Duration - var maxDotDelay time.Duration - for startIdx, startingPathOption := range startingPathOptions { - cur := startingPathOption - pathOptions := []PathOption{ - cur, - } - - for cur.DoneAt <= dot1DelayAt { - // Process the curTime, by adding new nodes for each cast option. - pathOptions = append(pathOptions, PathOption{ - DoneAt: cur.DoneAt + mf2CastTime, - CanMBAt: cur.CanMBAt, - }) - pathOptions = append(pathOptions, PathOption{ - DoneAt: cur.DoneAt + mf3CastTime, - CanMBAt: cur.CanMBAt, - }) - if cur.DoneAt >= cur.CanMBAt { - pathOptions = append(pathOptions, PathOption{ - DoneAt: cur.DoneAt + mbCastTime, - CanMBAt: cur.DoneAt + mbCastTime + spriest.MindBlast.CD.Duration, - }) - } - - // Find the next cur, which is the smallest option greater than curTime. - var bestOption PathOption - for i := 0; i < len(pathOptions); i++ { - if pathOptions[i].DoneAt > cur.DoneAt && (bestOption.DoneAt == 0 || pathOptions[i].DoneAt < bestOption.DoneAt) { - bestOption = pathOptions[i] - } - } - cur = bestOption - } - - delay := cur.DoneAt - dot1DelayAt - maxDotDelay = core.MaxDuration(maxDotDelay, delay) - if startIdx == 0 { - dotDelays[MindBlastIdx] = delay - } else if startIdx == 1 { - dotDelays[MindFlay2Idx] = delay - } else { - dotDelays[MindFlay3Idx] = delay - } - } - - // Resulting net damage for each spell, with the opportunity cost of casting - // other spells subtracted. - netSpellDamage := [SpellLen]float64{ - spellDamage[0], - spellDamage[1], - spellDamage[2], - spellDamage[3], - spellDamage[4], - spellDamage[5], - spellDamage[6], - spellDamage[7], - } - - // Subtract opportunity cost of not choosing each spell. - for chosenSpellIdx := range spellDamage { - if spellDamage[chosenSpellIdx] == 0 { - continue - } - - for otherSpellIdx := range spellDamage { - if chosenSpellIdx == otherSpellIdx { //|| otherSpellIdx == MindFlay1Idx || otherSpellIdx == MindFlay3Idx { - continue - } - - var delay time.Duration - if otherSpellIdx == DevouringPlagueIdx || otherSpellIdx == VampiricTouchIdx { - if chosenSpellIdx == MindBlastIdx || chosenSpellIdx == MindFlay2Idx || chosenSpellIdx == MindFlay3Idx { - delay = dotDelays[chosenSpellIdx] * 12 / 6 - } else { - delay = maxDotDelay * 12 / 6 - } - } else { - delay = core.MaxDuration(0, castCompleteAt[chosenSpellIdx]-spellDelayStart[otherSpellIdx]) - } - opportunityCostDmg := spellDPS[otherSpellIdx] * delay.Seconds() - //opportunityCostDmg := spellDPCT[otherSpellIdx] * delay.Seconds() - //if opportunityCostDmg < -9999 { - // panic(fmt.Sprintf("Opp cost: %0.01f, delay, %s, other dmg: %0.01f, other dpct: %0.01f\n", opportunityCostDmg, delay, spellDamage[otherSpellIdx], spellDPCT[otherSpellIdx])) - //} - netSpellDamage[chosenSpellIdx] -= opportunityCostDmg - } - } - - bestIdx := 0 - bestDamage := -999999.9 - for i, dmg := range netSpellDamage { - if dmg >= bestDamage && dmg != 0 { - if i == DevouringPlagueIdx || i == VampiricTouchIdx || i == ShadowWordPainIdx { - continue - } - bestIdx = i - bestDamage = dmg - } - } - - spells := []*core.Spell{ - spriest.MindBlast, - spriest.ShadowWordDeath, - spriest.DevouringPlague, - spriest.VampiricTouch, - spriest.ShadowWordPain, - spriest.MindFlay[1], - spriest.MindFlay[2], - spriest.MindFlay[3], - } - //if !spells[bestIdx].IsReady(sim) { - // fmt.Printf("MB dmg: %0.01f, net: %0.01f\n", spellDamage[MindBlastIdx], netSpellDamage[MindBlastIdx]) - // fmt.Printf("MF2 dmg: %0.01f, net: %0.01f\n", spellDamage[MindFlay2Idx], netSpellDamage[MindFlay2Idx]) - //} - return spells[bestIdx] -} diff --git a/sim/priest/shadow/rotation.go b/sim/priest/shadow/rotation.go index 4794910b3b..5a89af6b65 100644 --- a/sim/priest/shadow/rotation.go +++ b/sim/priest/shadow/rotation.go @@ -196,11 +196,11 @@ func (spriest *ShadowPriest) chooseSpellIdeal(sim *core.Simulation) (*core.Spell if spriest.T8FourSetBonus { //include benefit of 240 haste rating for 4 seconds. This isnt perfect because 1.6 dps per haste is an average and varies throughout the fight impDamage = 1.6 * 240 * 4 } - mbDamage = spriest.MindBlast.ExpectedDamage(sim, spriest.CurrentTarget) + float64(impDamage) + mbDamage = spriest.MindBlast.ExpectedInitialDamage(sim, spriest.CurrentTarget) + float64(impDamage) } // DP dmg - dpTickDamage := spriest.DevouringPlague.ExpectedDamage(sim, spriest.CurrentTarget) + dpTickDamage := spriest.DevouringPlague.ExpectedTickDamage(sim, spriest.CurrentTarget) dpInit := dpTickDamage * spriest.DpInitMultiplier dpDot := dpTickDamage * num_DP_ticks dpDamage = dpInit + dpDot @@ -220,7 +220,7 @@ func (spriest *ShadowPriest) chooseSpellIdeal(sim *core.Simulation) (*core.Spell dpDamage = 0 } - vtDamage = spriest.VampiricTouch.ExpectedDamage(sim, spriest.CurrentTarget) * num_VT_ticks + vtDamage = spriest.VampiricTouch.ExpectedTickDamage(sim, spriest.CurrentTarget) * num_VT_ticks // If there is at least 2 VT ticks then it's worth using if timeUntilBLStarts > gcd.Seconds() && numVTbeforeBL < 2 && sim.CurrentTime.Seconds() < float64(spriest.BLUsedAt) { @@ -230,11 +230,11 @@ func (spriest *ShadowPriest) chooseSpellIdeal(sim *core.Simulation) (*core.Spell // SWD dmg swdDamage = 0 if spriest.options.UseShadowWordDeath { - swdDamage = spriest.ShadowWordDeath.ExpectedDamage(sim, spriest.CurrentTarget) + swdDamage = spriest.ShadowWordDeath.ExpectedInitialDamage(sim, spriest.CurrentTarget) } - mfDamage = spriest.MindFlay[3].ExpectedDamage(sim, spriest.CurrentTarget) - swpTickDamage := spriest.ShadowWordPain.ExpectedDamage(sim, spriest.CurrentTarget) + mfDamage = spriest.MindFlay[3].ExpectedTickDamage(sim, spriest.CurrentTarget) * 3 + swpTickDamage := spriest.ShadowWordPain.ExpectedTickDamage(sim, spriest.CurrentTarget) //if spriest.rotation.RotationType == 4 { // msDamage = spriest.MindSear[5].ExpectedDamage(sim, spriest.CurrentTarget) @@ -264,7 +264,7 @@ func (spriest *ShadowPriest) chooseSpellIdeal(sim *core.Simulation) (*core.Spell currDotTickSpeed = spriest.DevouringPlague.CurDot().TickPeriod().Seconds() nextTickWait = spriest.DevouringPlague.CurDot().TimeUntilNextTick(sim) - dpDotCurr := spriest.DevouringPlague.ExpectedDamageFromCurrentSnapshot(sim, spriest.CurrentTarget) + dpDotCurr := spriest.DevouringPlague.ExpectedTickDamageFromCurrentSnapshot(sim, spriest.CurrentTarget) dpInitCurr := dpDotCurr * spriest.DpInitMultiplier cdDamage := mbDamage diff --git a/sim/priest/shadow_word_death.go b/sim/priest/shadow_word_death.go index 8531e6e940..b4e1b65fb6 100644 --- a/sim/priest/shadow_word_death.go +++ b/sim/priest/shadow_word_death.go @@ -65,7 +65,7 @@ func (priest *Priest) registerShadowWordDeathSpell() { } spell.DealDamage(sim, result) }, - ExpectedDamage: func(sim *core.Simulation, target *core.Unit, spell *core.Spell, _ bool) *core.SpellResult { + ExpectedInitialDamage: func(sim *core.Simulation, target *core.Unit, spell *core.Spell, _ bool) *core.SpellResult { baseDamage := (750.0+870.0)/2 + 0.429*spell.SpellPower() return spell.CalcDamage(sim, target, baseDamage, spell.OutcomeExpectedMagicHitAndCrit) }, diff --git a/sim/priest/shadow_word_pain.go b/sim/priest/shadow_word_pain.go index 24ecf88858..6b3569059e 100644 --- a/sim/priest/shadow_word_pain.go +++ b/sim/priest/shadow_word_pain.go @@ -90,7 +90,7 @@ func (priest *Priest) registerShadowWordPainSpell() { } spell.DealOutcome(sim, result) }, - ExpectedDamage: func(sim *core.Simulation, target *core.Unit, spell *core.Spell, useSnapshot bool) *core.SpellResult { + ExpectedTickDamage: func(sim *core.Simulation, target *core.Unit, spell *core.Spell, useSnapshot bool) *core.SpellResult { if useSnapshot { dot := spell.Dot(target) if priest.Talents.Shadowform { diff --git a/sim/priest/vampiric_touch.go b/sim/priest/vampiric_touch.go index 84fbb50bba..06473fbdce 100644 --- a/sim/priest/vampiric_touch.go +++ b/sim/priest/vampiric_touch.go @@ -63,7 +63,7 @@ func (priest *Priest) registerVampiricTouchSpell() { } spell.DealOutcome(sim, result) }, - ExpectedDamage: func(sim *core.Simulation, target *core.Unit, spell *core.Spell, useSnapshot bool) *core.SpellResult { + ExpectedTickDamage: func(sim *core.Simulation, target *core.Unit, spell *core.Spell, useSnapshot bool) *core.SpellResult { if useSnapshot { dot := spell.Dot(target) if priest.Talents.Shadowform { diff --git a/sim/warlock/corruption.go b/sim/warlock/corruption.go index 3815f6e6f0..01644e8047 100644 --- a/sim/warlock/corruption.go +++ b/sim/warlock/corruption.go @@ -72,7 +72,7 @@ func (warlock *Warlock) registerCorruptionSpell() { spell.Dot(target).Apply(sim) } }, - ExpectedDamage: func(sim *core.Simulation, target *core.Unit, spell *core.Spell, useSnapshot bool) *core.SpellResult { + ExpectedTickDamage: func(sim *core.Simulation, target *core.Unit, spell *core.Spell, useSnapshot bool) *core.SpellResult { if useSnapshot { dot := spell.Dot(target) return dot.CalcSnapshotDamage(sim, target, dot.OutcomeExpectedMagicSnapshotCrit) diff --git a/sim/warlock/drain_soul.go b/sim/warlock/drain_soul.go index 884209f71c..d1c685577e 100644 --- a/sim/warlock/drain_soul.go +++ b/sim/warlock/drain_soul.go @@ -84,7 +84,7 @@ func (warlock *Warlock) registerDrainSoulSpell() { warlock.everlastingAfflictionRefresh(sim, target) } }, - ExpectedDamage: func(sim *core.Simulation, target *core.Unit, spell *core.Spell, useSnapshot bool) *core.SpellResult { + ExpectedTickDamage: func(sim *core.Simulation, target *core.Unit, spell *core.Spell, useSnapshot bool) *core.SpellResult { if useSnapshot { dot := spell.Dot(target) return dot.CalcSnapshotDamage(sim, target, spell.OutcomeExpectedMagicAlwaysHit) diff --git a/sim/warlock/rotation.go b/sim/warlock/rotation.go index 6c50215935..313218b8bc 100644 --- a/sim/warlock/rotation.go +++ b/sim/warlock/rotation.go @@ -349,10 +349,10 @@ func (warlock *Warlock) defineRotation() { // check if reapplying corruption is worthwhile relDmgInc := warlock.calcRelativeCorruptionInc(target) - snapshotDmg := warlock.Corruption.ExpectedDamageFromCurrentSnapshot(sim, target) + snapshotDmg := warlock.Corruption.ExpectedTickDamageFromCurrentSnapshot(sim, target) snapshotDmg *= float64(sim.GetRemainingDuration()) / float64(warlock.Corruption.Dot(target).TickPeriod()) snapshotDmg *= (relDmgInc - 1) - snapshotDmg -= warlock.Corruption.ExpectedDamageFromCurrentSnapshot(sim, target) + snapshotDmg -= warlock.Corruption.ExpectedTickDamageFromCurrentSnapshot(sim, target) logInfo(sim, "Relative Corruption Inc: [%.2f], expected dmg gain: [%.2f]", relDmgInc, snapshotDmg) @@ -541,8 +541,8 @@ func (warlock *Warlock) defineRotation() { return ACLCast, mainTarget } - snapshotDmg := warlock.DrainSoul.ExpectedDamageFromCurrentSnapshot(sim, mainTarget) * float64(ticksLeft) - recastDmg := warlock.DrainSoul.ExpectedDamage(sim, mainTarget) * float64(recastTicks) + snapshotDmg := warlock.DrainSoul.ExpectedTickDamageFromCurrentSnapshot(sim, mainTarget) * float64(ticksLeft) + recastDmg := warlock.DrainSoul.ExpectedTickDamage(sim, mainTarget) * float64(recastTicks) snapshotDPS := snapshotDmg / (float64(ticksLeft) * dsDot.TickPeriod().Seconds()) recastDps := recastDmg / (float64(recastTicks)*warlock.ApplyCastSpeed(dsDot.TickLength).Seconds() + humanReactionTime.Seconds())