diff --git a/sim/core/aura_helpers.go b/sim/core/aura_helpers.go index 8460ae8708..4d5ef5edd8 100644 --- a/sim/core/aura_helpers.go +++ b/sim/core/aura_helpers.go @@ -476,6 +476,22 @@ func (parentAura *Aura) AttachAdditivePseudoStatBuff(fieldPointer *float64, bonu return parentAura } +func (parentAura *Aura) AttachMultiplyCastSpeed(multiplier float64) *Aura { + parentAura.ApplyOnGain(func(_ *Aura, _ *Simulation) { + parentAura.Unit.MultiplyCastSpeed(multiplier) + }) + + parentAura.ApplyOnExpire(func(_ *Aura, _ *Simulation) { + parentAura.Unit.MultiplyCastSpeed(1 / multiplier) + }) + + if parentAura.IsActive() { + parentAura.Unit.MultiplyCastSpeed(multiplier) + } + + return parentAura +} + type ShieldStrengthCalculator func(unit *Unit) float64 type DamageAbsorptionAura struct { diff --git a/sim/core/buffs.go b/sim/core/buffs.go index c0ada3f24c..c8f389cc0d 100644 --- a/sim/core/buffs.go +++ b/sim/core/buffs.go @@ -203,6 +203,7 @@ func applyBuffEffects(agent Agent, raidBuffs *proto.RaidBuffs, _ *proto.PartyBuf if raidBuffs.Heroism { registerBloodlustCD(agent, 32182) } + if raidBuffs.TimeWarp { registerBloodlustCD(agent, 80353) } diff --git a/sim/core/dot.go b/sim/core/dot.go index 4e8981a00a..42aaaa3c5f 100644 --- a/sim/core/dot.go +++ b/sim/core/dot.go @@ -47,6 +47,7 @@ type Dot struct { BaseTickCount int32 // base tick count without haste applied remainingTicks int32 + tmpExtraTicks int32 // extra ticks that are added during the runtime of the dot BonusCoefficient float64 // EffectBonusCoefficient in SpellEffect client DB table, "SP mod" on Wowhead (not necessarily shown there even if > 0) @@ -97,6 +98,7 @@ func (dot *Dot) recomputeAuraDuration(sim *Simulation) { nextTick := dot.TimeUntilNextTick(sim) dot.remainingTicks = dot.BaseTickCount + dot.tmpExtraTicks = 0 if dot.affectedByCastSpeed { // round the tickPeriod to the nearest full ms, same as ingame. This can best be seen ingame in how haste caps // work. For example shadowflame should take 1009 haste rating with the 5%/3% haste buffs without rounding, but @@ -159,7 +161,7 @@ func (dot *Dot) RemainingTicks() int32 { } func (dot *Dot) TickCount() int32 { - return dot.HastedTickCount() - dot.remainingTicks + return dot.HastedTickCount() + dot.tmpExtraTicks - dot.remainingTicks } func (dot *Dot) OutstandingDmg() float64 { @@ -170,6 +172,17 @@ func (dot *Dot) BaseDuration() time.Duration { return time.Duration(dot.BaseTickCount) * dot.BaseTickLength } +// Adds a tick to the current active dot and extends it's duration +func (dot *Dot) AddTick() { + if !dot.active { + return + } + + dot.tmpExtraTicks++ + dot.remainingTicks++ + dot.UpdateExpires(dot.expires + dot.TickPeriod()) +} + // Copy's the original DoT's period and duration to the current DoT. // This is only currently used for Mage's Impact DoT spreading and Enhancement's ImprovedLava Lash. func (dot *Dot) CopyDotAndApply(sim *Simulation, originaldot *Dot) { @@ -178,6 +191,7 @@ func (dot *Dot) CopyDotAndApply(sim *Simulation, originaldot *Dot) { dot.tickPeriod = originaldot.tickPeriod dot.remainingTicks = originaldot.remainingTicks + dot.tmpExtraTicks = 0 // must be set before Activate dot.Duration = originaldot.ExpiresAt() - sim.CurrentTime // originaldot.Duration diff --git a/sim/core/mana.go b/sim/core/mana.go index 78d378fc7d..bb05bb1a31 100644 --- a/sim/core/mana.go +++ b/sim/core/mana.go @@ -38,13 +38,6 @@ func (character *Character) EnableManaBar() { } func (character *Character) EnableManaBarWithModifier(modifier float64) { - // Assumes all units have >= 20 intellect. - // See https://wowwiki-archive.fandom.com/wiki/Base_mana. - // Subtract out the non-linear part of the formula separately, so that weird - // mana values are not included when using the stat dependency manager. - character.AddStat(stats.Mana, 20-15*20*modifier) - character.AddStatDependency(stats.Intellect, stats.Mana, 15*modifier) - // Starting with cataclysm you get mp5 equal 5% of your base mana character.AddStat(stats.MP5, character.baseStats[stats.Mana]*0.05) diff --git a/sim/priest/_glyphs.go b/sim/priest/_glyphs.go deleted file mode 100644 index 9cc9f4cf49..0000000000 --- a/sim/priest/_glyphs.go +++ /dev/null @@ -1,61 +0,0 @@ -package priest - -import ( - "time" - - "github.com/wowsims/mop/sim/core" - "github.com/wowsims/mop/sim/core/proto" -) - -func (priest *Priest) ApplyGlyphs() { - - if priest.HasPrimeGlyph(proto.PriestPrimeGlyph_GlyphOfShadowWordPain) { - priest.AddStaticMod(core.SpellModConfig{ - FloatValue: 0.1, - ClassMask: int64(PriestSpellShadowWordPain), - Kind: core.SpellMod_DamageDone_Flat, - }) - } - - if priest.HasPrimeGlyph(proto.PriestPrimeGlyph_GlyphOfMindFlay) { - priest.AddStaticMod(core.SpellModConfig{ - ClassMask: int64(PriestSpellMindFlay), - FloatValue: 0.1, - Kind: core.SpellMod_DamageDone_Flat, - }) - } - - if priest.HasPrimeGlyph(proto.PriestPrimeGlyph_GlyphOfDispersion) { - priest.AddStaticMod(core.SpellModConfig{ - Kind: core.SpellMod_Cooldown_Flat, - TimeValue: time.Second * -45, - ClassMask: int64(PriestSpellDispersion), - }) - } - - if priest.HasPrimeGlyph(proto.PriestPrimeGlyph_GlyphOfShadowWordDeath) { - priest.RegisterAura(core.Aura{ - Label: "Glyph of Shadow Word: Death", - Duration: core.NeverExpires, - OnReset: func(aura *core.Aura, sim *core.Simulation) { - aura.Activate(sim) - }, - - Icd: &core.Cooldown{ - Timer: priest.NewTimer(), - Duration: time.Second * 6, - }, - - OnSpellHitDealt: func(aura *core.Aura, sim *core.Simulation, spell *core.Spell, result *core.SpellResult) { - if spell.ClassSpellMask == PriestSpellShadowWordDeath && sim.IsExecutePhase25() && aura.Icd.IsReady(sim) { - if spell.CD.Timer == nil { - return - } - - aura.Icd.Use(sim) - spell.CD.Reset() - } - }, - }) - } -} diff --git a/sim/priest/_power_infusion.go b/sim/priest/_power_infusion.go deleted file mode 100644 index 62a8992683..0000000000 --- a/sim/priest/_power_infusion.go +++ /dev/null @@ -1,61 +0,0 @@ -package priest - -import ( - "github.com/wowsims/mop/sim/core" -) - -func (priest *Priest) registerPowerInfusionSpell() { - if !priest.Talents.PowerInfusion { - return - } - - actionID := core.ActionID{SpellID: 10060, Tag: priest.Index} - - powerInfusionTarget := priest.GetUnit(priest.SelfBuffs.PowerInfusionTarget) - powerInfusionAuras := priest.NewAllyAuraArray(func(unit *core.Unit) *core.Aura { - if unit.Type == core.PetUnit { - return nil - } - return core.PowerInfusionAura(unit, actionID.Tag) - }) - - piSpell := priest.RegisterSpell(core.SpellConfig{ - ActionID: actionID, - Flags: core.SpellFlagHelpful, - - ManaCost: core.ManaCostOptions{ - BaseCostPercent: 16, - }, - Cast: core.CastConfig{ - CD: core.Cooldown{ - Timer: priest.NewTimer(), - Duration: core.PowerInfusionCD, - }, - DefaultCast: core.Cast{ - NonEmpty: true, - }, - }, - - ApplyEffects: func(sim *core.Simulation, target *core.Unit, _ *core.Spell) { - if powerInfusionTarget != nil { - powerInfusionAuras.Get(powerInfusionTarget).Activate(sim) - } else { - powerInfusionAuras.Get(target).Activate(sim) - } - }, - }) - - priest.AddMajorCooldown(core.MajorCooldown{ - Spell: piSpell, - Priority: core.CooldownPriorityBloodlust, - Type: core.CooldownTypeMana, - ShouldActivate: func(sim *core.Simulation, character *core.Character) bool { - // How can we determine the target will be able to continue casting - // for the next 15s at 20% reduced mana cost? Arbitrary value until then. - //if powerInfusionTarget.CurrentMana() < 3000 { - // return false - //} - return powerInfusionTarget != nil && !powerInfusionTarget.HasActiveAuraWithTag(core.BloodlustAuraTag) - }, - }) -} diff --git a/sim/priest/_talents.go b/sim/priest/_talents.go index 2e63082997..a2be1402a5 100644 --- a/sim/priest/_talents.go +++ b/sim/priest/_talents.go @@ -619,73 +619,6 @@ func (priest *Priest) applySinAndPunishment() { }) } -func (priest *Priest) applyShadowyApparition() { - if priest.Talents.ShadowyApparition == 0 { - return - } - - const spellScaling = 0.515 - const levelScaling = 0.514 - - spell := priest.RegisterSpell(core.SpellConfig{ - ActionID: core.ActionID{SpellID: 87532}, - MissileSpeed: 3.5, - ProcMask: core.ProcMaskEmpty, // summoned guardian, should not be able to proc stuff - verify - ClassSpellMask: PriestSpellShadowyApparation, - Flags: core.SpellFlagPassiveSpell, - DamageMultiplier: 1, - DamageMultiplierAdditive: 1, - CritMultiplier: priest.DefaultCritMultiplier(), - SpellSchool: core.SpellSchoolShadow, - - BonusCoefficient: spellScaling, - - ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { - baseDamage := priest.ClassSpellScaling * levelScaling - - // snapshot values on spawn - dmgMulti := spell.DamageMultiplier - dmgMultiAdd := spell.DamageMultiplierAdditive - - spell.WaitTravelTime(sim, func(sim *core.Simulation) { - - oldMulti := spell.DamageMultiplier - oldAdd := spell.DamageMultiplierAdditive - - // calculate dmg on hit, as the apparations profit from the debuffs on the target - // when they reach them - // spell and other modifiers are snapshotted when the apparations spawn - spell.DamageMultiplier = dmgMulti - spell.DamageMultiplierAdditive = dmgMultiAdd - - result := spell.CalcDamage(sim, target, baseDamage, spell.OutcomeMagicHitAndCrit) - spell.DealDamage(sim, result) - - // restore mods - spell.DamageMultiplier = oldMulti - spell.DamageMultiplierAdditive = oldAdd - }) - }, - }) - - core.MakeProcTriggerAura(&priest.Unit, core.ProcTrigger{ - Name: "Shadowy Apparition Aura", - Callback: core.CallbackOnPeriodicDamageDealt, - Outcome: core.OutcomeLanded, - ClassSpellMask: PriestSpellShadowWordPain, - Handler: func(sim *core.Simulation, _ *core.Spell, result *core.SpellResult) { - procChance := 0.04 * float64(priest.Talents.ShadowyApparition) - if priest.Moving { - procChance *= 5 - } - - if sim.Proc(procChance, "Shadowy Apparition Aura") { - spell.Cast(sim, result.Target) - } - }, - }) -} - // func (priest *Priest) applyDivineAegis() { // if priest.Talents.DivineAegis == 0 { // return diff --git a/sim/priest/glyphs.go b/sim/priest/glyphs.go new file mode 100644 index 0000000000..ae5327f20b --- /dev/null +++ b/sim/priest/glyphs.go @@ -0,0 +1,19 @@ +package priest + +import ( + "github.com/wowsims/mop/sim/core" + "github.com/wowsims/mop/sim/core/proto" +) + +func (priest *Priest) ApplyGlyphs() { + // Glyph of Dispersion + // Glyph of Mindspike + // Glyph of Shadow Word Death + if priest.HasMinorGlyph(proto.PriestMinorGlyph_GlyphOfTheSha) { + priest.AddStaticMod(core.SpellModConfig{ + Kind: core.SpellMod_GlobalCooldown_Flat, + TimeValue: -core.GCDDefault, + ClassMask: PriestSpellMindBender | PriestSpellShadowFiend, + }) + } +} diff --git a/sim/priest/items.go b/sim/priest/items.go index a7ed65abc0..59c28a3a8c 100644 --- a/sim/priest/items.go +++ b/sim/priest/items.go @@ -454,5 +454,134 @@ var ItemSetRegaliaOfDyingLight = core.NewItemSet(core.ItemSet{ }, }) +// T14 - Shadow +var ItemSetRegaliaOfTheGuardianSperpent = core.NewItemSet(core.ItemSet{ + Name: "Regalia of the Guardian Serpent", + Bonuses: map[int32]core.ApplySetBonus{ + 2: func(agent core.Agent, setBonusAura *core.Aura) { + setBonusAura.AttachSpellMod(core.SpellModConfig{ + Kind: core.SpellMod_BonusCrit_Percent, + ClassMask: PriestSpellShadowWordPain, + FloatValue: 10, + }) + }, + 4: func(agent core.Agent, setBonusAura *core.Aura) { + setBonusAura.AttachSpellMod(core.SpellModConfig{ + Kind: core.SpellMod_DotNumberOfTicks_Flat, + ClassMask: PriestSpellShadowWordPain | PriestSpellVampiricTouch, + IntValue: 1, + }) + }, + }, +}) + +var ItemSetRegaliaOfTheExorcist = core.NewItemSet(core.ItemSet{ + Name: "Regalia of the Exorcist", + Bonuses: map[int32]core.ApplySetBonus{ + 2: func(agent core.Agent, setBonusAura *core.Aura) { + priest := agent.(PriestAgent).GetPriest() + setBonusAura.AttachProcTrigger(core.ProcTrigger{ + Name: "Regalia of the Exorcist - 2P", + SpellFlags: core.SpellFlagPassiveSpell, + ProcChance: 0.65, + ClassSpellMask: PriestSpellShadowyApparation, + Outcome: core.OutcomeLanded, + Callback: core.CallbackOnSpellHitDealt, + Handler: func(sim *core.Simulation, spell *core.Spell, result *core.SpellResult) { + if priest.ShadowWordPain != nil && priest.ShadowWordPain.Dot(result.Target).IsActive() { + priest.ShadowWordPain.Dot(result.Target).AddTick() + } + + if priest.VampiricTouch != nil && priest.VampiricTouch.Dot(result.Target).IsActive() { + priest.VampiricTouch.Dot(result.Target).AddTick() + } + }, + }) + }, + 4: func(agent core.Agent, setBonusAura *core.Aura) { + priest := agent.(PriestAgent).GetPriest() + setBonusAura.AttachProcTrigger(core.ProcTrigger{ + Name: "Regalia of the Exorcist - 4P", + ProcMask: core.ProcMaskSpellDamage, + ProcChance: 0.1, + ClassSpellMask: PriestSpellVampiricTouch, + Outcome: core.OutcomeLanded, + Callback: core.CallbackOnPeriodicDamageDealt, + Handler: func(sim *core.Simulation, spell *core.Spell, result *core.SpellResult) { + priest.ShadowyApparition.Cast(sim, result.Target) + }, + }) + }, + }, +}) + +var ItemSetRegaliaOfTheTernionGlory = core.NewItemSet(core.ItemSet{ + Name: "Regalia of Ternion Glory", + Bonuses: map[int32]core.ApplySetBonus{ + 2: func(agent core.Agent, setBonusAura *core.Aura) { + setBonusAura.AttachSpellMod(core.SpellModConfig{ + Kind: core.SpellMod_CritMultiplier_Flat, + FloatValue: 0.4, + ClassMask: PriestSpellShadowyRecall, + }) + }, + 4: func(agent core.Agent, setBonusAura *core.Aura) { + priest := agent.(PriestAgent).GetPriest() + mod := priest.Unit.AddDynamicMod(core.SpellModConfig{ + Kind: core.SpellMod_DamageDone_Pct, + FloatValue: 0.2, + ClassMask: PriestSpellShadowWordDeath | PriestSpellMindSpike | PriestSpellMindBlast, + }) + + var orbsSpend int32 = 0 + priest.Unit.GetSecondaryResourceBar().RegisterOnSpend(func(amount int32) { + orbsSpend = amount + }) + + aura := priest.Unit.RegisterAura(core.Aura{ + Label: "Regalia of the Ternion Glory - 4P (Proc)", + ActionID: core.ActionID{SpellID: 145180}, + Duration: time.Second * 12, + OnGain: func(aura *core.Aura, sim *core.Simulation) { + mod.UpdateFloatValue(0.2 * float64(orbsSpend)) + mod.Activate() + }, + OnExpire: func(aura *core.Aura, sim *core.Simulation) { + mod.Deactivate() + }, + OnCastComplete: func(aura *core.Aura, sim *core.Simulation, spell *core.Spell) { + if spell.Matches(PriestSpellMindBlast | PriestSpellMindSpike | PriestSpellShadowWordDeath) { + return + } + + aura.Deactivate(sim) + }, + }) + + core.MakeProcTriggerAura(&priest.Unit, core.ProcTrigger{ + Name: "Regalia of the Ternion Glory - 4P", + Outcome: core.OutcomeLanded, + Callback: core.CallbackOnSpellHitDealt, + ClassSpellMask: PriestSpellDevouringPlague, + Handler: func(sim *core.Simulation, spell *core.Spell, result *core.SpellResult) { + aura.Activate(sim) + }, + }) + }, + }, +}) + +var shaWeaponIDs = []int32{86990, 86865, 86227, 97296} + func init() { + for _, id := range shaWeaponIDs { + core.NewItemEffect(id, func(agent core.Agent) { + priest := agent.(PriestAgent).GetPriest() + priest.AddStaticMod(core.SpellModConfig{ + Kind: core.SpellMod_GlobalCooldown_Flat, + TimeValue: -core.GCDDefault, + ClassMask: PriestSpellShadowFiend | PriestSpellMindBender, + }) + }) + } } diff --git a/sim/priest/mind_sear.go b/sim/priest/mind_sear.go index 292ae76e0b..2d18ae8019 100644 --- a/sim/priest/mind_sear.go +++ b/sim/priest/mind_sear.go @@ -6,6 +6,10 @@ import ( "github.com/wowsims/mop/sim/core" ) +const SearCoeff = 0.3 +const SearVariance = 0.08 +const SearScale = 0.3 + func (priest *Priest) getMindSearBaseConfig() core.SpellConfig { return core.SpellConfig{ SpellSchool: core.SpellSchoolShadow, @@ -15,21 +19,30 @@ func (priest *Priest) getMindSearBaseConfig() core.SpellConfig { DamageMultiplierAdditive: 1, ThreatMultiplier: 1, CritMultiplier: priest.DefaultCritMultiplier(), - BonusCoefficient: 0.2622, + BonusCoefficient: SearCoeff, } } func (priest *Priest) getMindSearTickSpell() *core.Spell { config := priest.getMindSearBaseConfig() + config.Flags = core.SpellFlagNoOnDamageDealt config.ActionID = core.ActionID{SpellID: 48045} config.ApplyEffects = func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { - damage := priest.ClassSpellScaling * 0.23 + damage := priest.CalcAndRollDamageRange(sim, SearScale, SearVariance) for _, aoeTarget := range sim.Encounter.TargetUnits { // Calc spell damage but deal as periodic for metric purposes result := spell.CalcDamage(sim, aoeTarget, damage, spell.OutcomeMagicHitAndCritNoHitCounter) + + // TODO: Verify actual proc behaviour + // Damage is logged as a tick, has the Flag 'Treat as Periodic' and 'Not a Proc' + // However, i.E. Trinkets proccing from 'Periodic Damage Dealt' do not trigger spell.DealPeriodicDamage(sim, result) + // For now Sear seems to trigger damage dealt and not periodic dealt for procs + spell.Unit.OnSpellHitDealt(sim, spell, result) + result.Target.OnSpellHitTaken(sim, spell, result) + // Adjust metrics just for Mind Sear as it is a edgecase and needs to be handled manually if result.DidCrit() { spell.SpellMetrics[result.Target.UnitIndex].CritTicks++ @@ -50,7 +63,7 @@ func (priest *Priest) newMindSearSpell() *core.Spell { config.ActionID = core.ActionID{SpellID: 48045} config.Flags = core.SpellFlagChanneled | core.SpellFlagAPL config.ManaCost = core.ManaCostOptions{ - BaseCostPercent: 28, + BaseCostPercent: 3, } config.Cast = core.CastConfig{ @@ -77,7 +90,7 @@ func (priest *Priest) newMindSearSpell() *core.Spell { } } config.ExpectedTickDamage = func(sim *core.Simulation, target *core.Unit, spell *core.Spell, _ bool) *core.SpellResult { - baseDamage := priest.ClassSpellScaling * 0.23 + baseDamage := priest.CalcAndRollDamageRange(sim, SearScale, SearVariance) return spell.CalcPeriodicDamage(sim, target, baseDamage, spell.OutcomeExpectedMagicCrit) } diff --git a/sim/priest/mind_spike.go b/sim/priest/mind_spike.go deleted file mode 100644 index 326094ab0b..0000000000 --- a/sim/priest/mind_spike.go +++ /dev/null @@ -1,83 +0,0 @@ -package priest - -import ( - "time" - - "github.com/wowsims/mop/sim/core" -) - -func (priest *Priest) registerMindSpike() { - mbMod := priest.AddDynamicMod(core.SpellModConfig{ - ClassMask: PriestSpellMindBlast, - Kind: core.SpellMod_BonusCrit_Percent, - FloatValue: 30, - }) - - procAura := priest.RegisterAura(core.Aura{ - Label: "Mind Spike Buff", - ActionID: core.ActionID{SpellID: 87178}, - Duration: time.Second * 12, - MaxStacks: 3, - OnStacksChange: func(aura *core.Aura, sim *core.Simulation, oldStacks, newStacks int32) { - if newStacks > 0 { - mbMod.UpdateFloatValue(float64(newStacks) * 30) - mbMod.Activate() - } else { - mbMod.Deactivate() - } - }, - OnSpellHitDealt: func(aura *core.Aura, sim *core.Simulation, spell *core.Spell, result *core.SpellResult) { - if spell.ClassSpellMask == PriestSpellMindBlast { - aura.Deactivate(sim) - } - }, - }) - - priest.RegisterSpell(core.SpellConfig{ - ActionID: core.ActionID{SpellID: 73510}, - SpellSchool: core.SpellSchoolShadow, - ProcMask: core.ProcMaskSpellDamage, - Flags: core.SpellFlagAPL, - ClassSpellMask: PriestSpellMindSpike, - - DamageMultiplier: 1, - DamageMultiplierAdditive: 1, - CritMultiplier: priest.DefaultCritMultiplier(), - ManaCost: core.ManaCostOptions{ - BaseCostPercent: 12, - PercentModifier: 100, - }, - - Cast: core.CastConfig{ - DefaultCast: core.Cast{ - GCD: core.GCDDefault, - CastTime: time.Millisecond * 1500, - }, - }, - ThreatMultiplier: 1, - - BonusCoefficient: 0.8355, - - ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { - baseDamage := priest.calcBaseDamage(sim, 1.178, 0.055) - result := spell.CalcAndDealDamage(sim, target, baseDamage, spell.OutcomeMagicHitAndCrit) - if result.Outcome.Matches(core.OutcomeLanded) { - priest.ShadowWordPain.Dot(target).Deactivate(sim) - - // only access those if they're actually registered and talented - if priest.VampiricTouch != nil { - priest.VampiricTouch.Dot(target).Deactivate(sim) - } - if priest.DevouringPlague != nil { - priest.DevouringPlague.Dot(target).Deactivate(sim) - } - procAura.Activate(sim) - procAura.AddStack(sim) - } - }, - ExpectedInitialDamage: func(sim *core.Simulation, target *core.Unit, spell *core.Spell, _ bool) *core.SpellResult { - baseDamage := priest.calcBaseDamage(sim, 1.557, 0.055) - return spell.CalcDamage(sim, target, baseDamage, spell.OutcomeExpectedMagicHitAndCrit) - }, - }) -} diff --git a/sim/priest/mindbender.go b/sim/priest/mindbender.go new file mode 100644 index 0000000000..553b85404f --- /dev/null +++ b/sim/priest/mindbender.go @@ -0,0 +1,45 @@ +package priest + +import ( + "time" + + "github.com/wowsims/mop/sim/core" +) + +func (priest *Priest) registerMindbenderSpell() { + if !priest.Talents.Mindbender { + return + } + + actionID := core.ActionID{SpellID: 123040} + + // For timeline only + priest.MindbenderAura = priest.RegisterAura(core.Aura{ + ActionID: actionID, + Label: "Mindbender", + Duration: time.Second * 15.0, + }) + + priest.MindBender = priest.RegisterSpell(core.SpellConfig{ + ActionID: actionID, + SpellSchool: core.SpellSchoolShadow, + ProcMask: core.ProcMaskEmpty, + Flags: core.SpellFlagAPL, + ClassSpellMask: PriestSpellMindBender, + + Cast: core.CastConfig{ + DefaultCast: core.Cast{ + GCD: core.GCDDefault, + }, + CD: core.Cooldown{ + Timer: priest.NewTimer(), + Duration: time.Minute, + }, + }, + + ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { + priest.MindbenderPet.EnableWithTimeout(sim, priest.MindbenderPet, time.Second*15.0) + priest.MindbenderAura.Activate(sim) + }, + }) +} diff --git a/sim/priest/mindbender_pet.go b/sim/priest/mindbender_pet.go new file mode 100644 index 0000000000..c2d288fb90 --- /dev/null +++ b/sim/priest/mindbender_pet.go @@ -0,0 +1,124 @@ +package priest + +import ( + "time" + + "github.com/wowsims/mop/sim/core" + "github.com/wowsims/mop/sim/core/stats" +) + +type MindBender struct { + core.Pet + + Priest *Priest + Shadowcrawl *core.Spell + ShadowcrawlAura *core.Aura +} + +func (priest *Priest) NewMindBender() *MindBender { + mindbender := &MindBender{ + Pet: core.NewPet(core.PetConfig{ + Name: "Mindbender", + Owner: &priest.Character, + BaseStats: baseStats, + StatInheritance: priest.mindbenderStatInheritance(), + IsGuardian: false, + EnabledOnStart: false, + HasDynamicMeleeSpeedInheritance: true, + }), + Priest: priest, + } + + manaMetric := priest.NewManaMetrics(core.ActionID{SpellID: 34433}) + core.MakePermanent(mindbender.GetOrRegisterAura(core.Aura{ + Label: "Autoattack mana regen", + OnSpellHitDealt: func(aura *core.Aura, sim *core.Simulation, spell *core.Spell, result *core.SpellResult) { + restoreMana := priest.MaxMana() * 0.0175 + priest.AddMana(sim, restoreMana, manaMetric) + }, + })) + + actionID := core.ActionID{SpellID: 63619} + mindbender.ShadowcrawlAura = mindbender.GetOrRegisterAura(core.Aura{ + Label: "Shadowcrawl", + ActionID: actionID, + Duration: time.Second * 5, + }).AttachMultiplicativePseudoStatBuff(&mindbender.PseudoStats.DamageDealtMultiplier, 1.15) + + mindbender.Shadowcrawl = mindbender.RegisterSpell(core.SpellConfig{ + ActionID: actionID, + SpellSchool: core.SpellSchoolShadow, + ProcMask: core.ProcMaskEmpty, + Flags: core.SpellFlagNoLogs, + + Cast: core.CastConfig{ + DefaultCast: core.Cast{ + GCD: time.Second * 6, + }, + }, + + ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { + mindbender.ShadowcrawlAura.Activate(sim) + }, + }) + + mindbender.PseudoStats.DamageTakenMultiplier *= 0.1 + + // never misses + mindbender.AddStats(stats.Stats{ + stats.HitRating: 8 * core.PhysicalHitRatingPerHitPercent, + stats.ExpertiseRating: 14 * core.ExpertisePerQuarterPercentReduction * 4, + }) + + mindbender.EnableAutoAttacks(mindbender, core.AutoAttackOptions{ + MainHand: core.Weapon{ + BaseDamageMin: priest.CalcScalingSpellDmg(1.76), + BaseDamageMax: priest.CalcScalingSpellDmg(1.76), + SwingSpeed: 1.5, + NormalizedSwingSpeed: 1.5, + CritMultiplier: 2, + SpellSchool: core.SpellSchoolShadow, + AttackPowerPerDPS: core.DefaultAttackPowerPerDPS, + }, + AutoSwingMelee: true, + }) + + mindbender.AutoAttacks.MHConfig().BonusCoefficient = 1 + + mindbender.EnableManaBar() + priest.AddPet(mindbender) + + return mindbender +} + +func (priest *Priest) mindbenderStatInheritance() core.PetStatInheritance { + return func(ownerStats stats.Stats) stats.Stats { + return stats.Stats{ + stats.PhysicalCritPercent: ownerStats[stats.SpellCritPercent], + stats.Intellect: (ownerStats[stats.Intellect] - 10) * 0.3, + stats.Stamina: ownerStats[stats.Stamina] * 0.75, + stats.SpellPower: 0.88 * ownerStats[stats.SpellPower], + stats.HasteRating: ownerStats[stats.HasteRating], + } + } +} + +func (mindbender *MindBender) Initialize() { +} + +func (mindbender *MindBender) ExecuteCustomRotation(sim *core.Simulation) { + mindbender.Shadowcrawl.Cast(sim, nil) +} + +func (mindbender *MindBender) Reset(sim *core.Simulation) { + mindbender.ShadowcrawlAura.Deactivate(sim) + mindbender.Disable(sim) +} + +func (mindbender *MindBender) OnPetDisable(sim *core.Simulation) { + mindbender.ShadowcrawlAura.Deactivate(sim) +} + +func (mindbender *MindBender) GetPet() *core.Pet { + return &mindbender.Pet +} diff --git a/sim/priest/power_infusion.go b/sim/priest/power_infusion.go new file mode 100644 index 0000000000..be9cac8f3c --- /dev/null +++ b/sim/priest/power_infusion.go @@ -0,0 +1,59 @@ +package priest + +import ( + "time" + + "github.com/wowsims/mop/sim/core" +) + +const PowerInfusionDuration = time.Second * 20 +const PowerInfusionCD = time.Minute * 2 + +func (priest *Priest) registerPowerInfusionSpell() { + if !priest.Talents.PowerInfusion { + return + } + actionID := core.ActionID{SpellID: 10060} + piAura := priest.GetOrRegisterAura(core.Aura{ + Label: "PowerInfusion-Aura", + ActionID: actionID, + Duration: PowerInfusionDuration, + }).AttachSpellMod(core.SpellModConfig{ + Kind: core.SpellMod_DamageDone_Pct, + FloatValue: 0.05, + }).AttachMultiplyCastSpeed(1.2) + + piAura.NewExclusiveEffect("ManaCost", true, core.ExclusiveEffect{ + Priority: -20, + OnGain: func(ee *core.ExclusiveEffect, sim *core.Simulation) { + ee.Aura.Unit.PseudoStats.SpellCostPercentModifier -= 20 + }, + OnExpire: func(ee *core.ExclusiveEffect, sim *core.Simulation) { + ee.Aura.Unit.PseudoStats.SpellCostPercentModifier += 20 + }, + }) + + piSpell := priest.RegisterSpell(core.SpellConfig{ + ActionID: actionID, + Flags: core.SpellFlagHelpful, + Cast: core.CastConfig{ + CD: core.Cooldown{ + Timer: priest.NewTimer(), + Duration: PowerInfusionCD, + }, + DefaultCast: core.Cast{ + NonEmpty: true, + }, + }, + + ApplyEffects: func(sim *core.Simulation, target *core.Unit, _ *core.Spell) { + piAura.Activate(sim) + }, + }) + + priest.AddMajorCooldown(core.MajorCooldown{ + Spell: piSpell, + Priority: core.CooldownPriorityBloodlust, + Type: core.CooldownTypeMana, + }) +} diff --git a/sim/priest/priest.go b/sim/priest/priest.go index 5b25d505bc..7b3cd52585 100644 --- a/sim/priest/priest.go +++ b/sim/priest/priest.go @@ -17,6 +17,8 @@ type Priest struct { ShadowfiendAura *core.Aura ShadowfiendPet *Shadowfiend + MindbenderPet *MindBender + MindbenderAura *core.Aura ShadowOrbsAura *core.Aura EmpoweredShadowAura *core.Aura @@ -29,30 +31,29 @@ type Priest struct { SurgeOfLightProcAura *core.Aura // might want to move these spell / talents into spec specific initialization - BindingHeal *core.Spell - CircleOfHealing *core.Spell - FlashHeal *core.Spell - GreaterHeal *core.Spell - Penance *core.Spell - PenanceHeal *core.Spell - PowerWordShield *core.Spell - PrayerOfHealing *core.Spell - PrayerOfMending *core.Spell - Renew *core.Spell - EmpoweredRenew *core.Spell - InnerFocus *core.Spell - HolyFire *core.Spell - Smite *core.Spell - DevouringPlague *core.Spell - ShadowWordPain *core.Spell - Shadowfiend *core.Spell - VampiricTouch *core.Spell + BindingHeal *core.Spell + CircleOfHealing *core.Spell + FlashHeal *core.Spell + GreaterHeal *core.Spell + Penance *core.Spell + PenanceHeal *core.Spell + PowerWordShield *core.Spell + PrayerOfHealing *core.Spell + PrayerOfMending *core.Spell + Renew *core.Spell + EmpoweredRenew *core.Spell + InnerFocus *core.Spell + HolyFire *core.Spell + Smite *core.Spell + ShadowWordPain *core.Spell + Shadowfiend *core.Spell + VampiricTouch *core.Spell + MindBender *core.Spell + ShadowyApparition *core.Spell WeakenedSouls core.AuraArray ProcPrayerOfMending core.ApplySpellResults - - ClassSpellScaling float64 } type SelfBuffs struct { @@ -66,62 +67,31 @@ func (priest *Priest) GetCharacter() *core.Character { return &priest.Character } -// func (priest *Priest) HasMajorGlyph(glyph proto.PriestMajorGlyph) bool { -// return priest.HasGlyph(int32(glyph)) -// } -// func (priest *Priest) HasMinorGlyph(glyph proto.PriestMinorGlyph) bool { -// return priest.HasGlyph(int32(glyph)) -// } - -// func (priest *Priest) AddRaidBuffs(raidBuffs *proto.RaidBuffs) { -// raidBuffs.ShadowProtection = true -// raidBuffs.DivineSpirit = true - -// raidBuffs.PowerWordFortitude = max(raidBuffs.PowerWordFortitude, core.MakeTristateValue( -// true, -// priest.Talents.ImprovedPowerWordFortitude == 2)) -// } - func (priest *Priest) AddPartyBuffs(_ *proto.PartyBuffs) { } func (priest *Priest) Initialize() { if priest.SelfBuffs.UseInnerFire { - priest.AddStat(stats.SpellPower, 531) - priest.ApplyEquipScaling(stats.Armor, 1.6) + priest.MultiplyStat(stats.SpellPower, 1.1) + priest.ApplyEquipScaling(stats.Armor, 1.1) core.MakePermanent(priest.RegisterAura(core.Aura{ Label: "Inner Fire", ActionID: core.ActionID{SpellID: 588}, })) } - priest.registerDevouringPlagueSpell() - // priest.registerShadowWordPainSpell() - - priest.registerMindBlastSpell() - priest.registerShadowWordDeathSpell() + priest.MultiplyStat(stats.Intellect, 1.05) + priest.registerShadowWordPainSpell() priest.registerShadowfiendSpell() - // priest.registerVampiricTouchSpell() - // priest.registerDispersionSpell() - priest.registerMindSpike() + priest.registerVampiricTouchSpell() - // priest.registerPowerInfusionSpell() + // priest.registerDispersionSpell() - priest.newMindFlaySpell() + priest.registerPowerInfusionSpell() priest.newMindSearSpell() -} -// func (priest *Priest) RegisterHealingSpells() { -// priest.registerPenanceHealSpell() -// priest.registerBindingHealSpell() -// priest.registerCircleOfHealingSpell() -// priest.registerFlashHealSpell() -// priest.registerGreaterHealSpell() -// priest.registerPowerWordShieldSpell() -// priest.registerPrayerOfHealingSpell() -// priest.registerPrayerOfMendingSpell() -// priest.registerRenewSpell() -// } + priest.ApplyGlyphs() +} func (priest *Priest) AddHolyEvanglismStack(sim *core.Simulation) { if priest.HolyEvangelismProcAura != nil { @@ -137,20 +107,28 @@ func (priest *Priest) AddDarkEvangelismStack(sim *core.Simulation) { } } +func (priest *Priest) ApplyTalents() { + priest.registerMindbenderSpell() +} + func (priest *Priest) Reset(_ *core.Simulation) { } func New(char *core.Character, selfBuffs SelfBuffs, talents string) *Priest { priest := &Priest{ - Character: *char, - SelfBuffs: selfBuffs, - Talents: &proto.PriestTalents{}, - ClassSpellScaling: core.GetClassSpellScalingCoefficient(proto.Class_ClassPriest), + Character: *char, + SelfBuffs: selfBuffs, + Talents: &proto.PriestTalents{}, } core.FillTalentsProto(priest.Talents.ProtoReflect(), talents) priest.EnableManaBar() priest.ShadowfiendPet = priest.NewShadowfiend() + + if priest.Talents.Mindbender { + priest.MindbenderPet = priest.NewMindBender() + } + return priest } @@ -159,11 +137,11 @@ type PriestAgent interface { GetPriest() *Priest } -func (hunter *Priest) HasMajorGlyph(glyph proto.PriestMajorGlyph) bool { - return hunter.HasGlyph(int32(glyph)) +func (priest *Priest) HasMajorGlyph(glyph proto.PriestMajorGlyph) bool { + return priest.HasGlyph(int32(glyph)) } -func (hunter *Priest) HasMinorGlyph(glyph proto.PriestMinorGlyph) bool { - return hunter.HasGlyph(int32(glyph)) +func (priest *Priest) HasMinorGlyph(glyph proto.PriestMinorGlyph) bool { + return priest.HasGlyph(int32(glyph)) } const ( @@ -171,17 +149,21 @@ const ( PriestSpellArchangel int64 = 1 << iota PriestSpellDarkArchangel PriestSpellBindingHeal + PriestSpellCascade PriestSpellCircleOfHealing PriestSpellDevouringPlague + PriestSpellDevouringPlagueDoT PriestSpellDesperatePrayer PriestSpellDispersion PriestSpellDivineAegis PriestSpellDivineHymn + PriestSpellDivineStar PriestSpellEmpoweredRenew PriestSpellFade PriestSpellFlashHeal PriestSpellGreaterHeal PriestSpellGuardianSpirit + PriestSpellHalo PriestSpellHolyFire PriestSpellHolyNova PriestSpellHolyWordChastise @@ -193,6 +175,7 @@ const ( PriestSpellInnerFocus PriestSpellInnerWill PriestSpellManaBurn + PriestSpellMindBender PriestSpellMindBlast PriestSpellMindFlay PriestSpellMindSear @@ -208,6 +191,7 @@ const ( PriestSpellPsychicScream PriestSpellRenew PriestSpellShadowOrbPassive + PriestSpellShadowyRecall PriestSpellShadowWordDeath PriestSpellShadowWordPain PriestSpellShadowFiend @@ -248,13 +232,3 @@ const ( PriestSpellMindSpike | PriestSpellVampiricTouch ) - -func (priest *Priest) calcBaseDamage(sim *core.Simulation, coefficient float64, variance float64) float64 { - baseDamage := priest.ClassSpellScaling * coefficient - if variance > 0 { - delta := priest.ClassSpellScaling * variance * 0.5 - baseDamage += sim.Roll(-delta, delta) - } - - return baseDamage -} diff --git a/sim/priest/shadow/cascade.go b/sim/priest/shadow/cascade.go new file mode 100644 index 0000000000..efbc7d3ebb --- /dev/null +++ b/sim/priest/shadow/cascade.go @@ -0,0 +1,100 @@ +package shadow + +import ( + "math" + "slices" + "time" + + "github.com/wowsims/mop/sim/core" +) + +const cascadeScale = 12 +const cascadeCoeff = 1.225 + +func (shadow *ShadowPriest) registerCascade() { + if !shadow.Talents.Cascade { + return + } + + targets := []*core.Unit{} + cascadeHandler := func(damageMod float64, bounceSpell *core.Spell, target *core.Unit, sim *core.Simulation) { + bounceSpell.DamageMultiplier *= damageMod + bounceSpell.CalcAndDealDamage(sim, target, shadow.CalcScalingSpellDmg(cascadeScale), bounceSpell.OutcomeMagicHitAndCrit) + bounceSpell.DamageMultiplier /= damageMod + + if len(targets) >= 31 { + return + } + + bounceTargets := []*core.Unit{} + for _, unit := range sim.Encounter.TargetUnits { + if unit == target { + continue + } + + if slices.Contains(targets, unit) { + continue + } + + targets = append(targets, unit) + bounceTargets = append(bounceTargets, unit) + if len(bounceTargets) == 2 { + break + } + } + + core.StartDelayedAction(sim, core.DelayedActionOptions{ + DoAt: sim.CurrentTime + time.Millisecond*100, + OnAction: func(s *core.Simulation) { + for _, unit := range bounceTargets { + bounceSpell.Cast(sim, unit) + } + }}) + } + + bounceSpell := shadow.RegisterSpell(core.SpellConfig{ + ActionID: core.ActionID{SpellID: 127632}.WithTag(1), + SpellSchool: core.SpellSchoolShadow, + Flags: core.SpellFlagPassiveSpell, + ProcMask: core.ProcMaskSpellDamage, + DamageMultiplier: 1, + CritMultiplier: shadow.DefaultCritMultiplier(), + BonusCoefficient: cascadeCoeff, + ThreatMultiplier: 1, + ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { + damageMod := 0.4 // assume minimal distance for now + cascadeHandler(damageMod, spell, target, sim) + }, + }) + + shadow.RegisterSpell(core.SpellConfig{ + ActionID: core.ActionID{SpellID: 127632}, + SpellSchool: core.SpellSchoolShadow, + Flags: core.SpellFlagAPL, + ProcMask: core.ProcMaskSpellDamage, + MissileSpeed: 24, + ManaCost: core.ManaCostOptions{ + BaseCostPercent: 10, + }, + DamageMultiplier: 1, + Cast: core.CastConfig{ + DefaultCast: core.Cast{ + GCD: core.GCDDefault, + }, + CD: core.Cooldown{ + Timer: shadow.NewTimer(), + Duration: time.Second * 25, + }, + }, + ThreatMultiplier: 1, + CritMultiplier: shadow.DefaultCritMultiplier(), + BonusCoefficient: cascadeCoeff, + ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { + damageMod := math.Min(0.4+0.6*(1-(30-shadow.DistanceFromTarget)/30), 1) + targets = []*core.Unit{target} + spell.WaitTravelTime(sim, func(s *core.Simulation) { + cascadeHandler(damageMod, bounceSpell, target, sim) + }) + }, + }) +} diff --git a/sim/priest/shadow/devouring_plague.go b/sim/priest/shadow/devouring_plague.go new file mode 100644 index 0000000000..3d22b3a713 --- /dev/null +++ b/sim/priest/shadow/devouring_plague.go @@ -0,0 +1,90 @@ +package shadow + +import ( + "time" + + "github.com/wowsims/mop/sim/core" + "github.com/wowsims/mop/sim/priest" +) + +// impact spell +const dpImpactScale = 1.566 +const dpImpactCoeff = 0.786 + +// dot spell +const DpDotScale = 0.261 +const DpDotCoeff = 0.131 + +func (shadow *ShadowPriest) registerDevouringPlagueSpell() { + actionID := core.ActionID{SpellID: 2944, Tag: 0} + shadow.DevouringPlague = shadow.RegisterSpell(core.SpellConfig{ + ActionID: actionID, + SpellSchool: core.SpellSchoolShadow, + ProcMask: core.ProcMaskSpellDamage, + Flags: core.SpellFlagDisease | core.SpellFlagAPL, + ClassSpellMask: priest.PriestSpellDevouringPlague, + DamageMultiplier: 1, + DamageMultiplierAdditive: 1, + ThreatMultiplier: 1, + CritMultiplier: shadow.DefaultCritMultiplier(), + Cast: core.CastConfig{ + DefaultCast: core.Cast{ + GCD: core.GCDDefault, + }, + }, + + BonusCoefficient: dpImpactCoeff, + + ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { + shadow.orbsConsumed = shadow.ShadowOrbs.Value() + spell.DamageMultiplier *= float64(shadow.orbsConsumed) + result := spell.CalcDamage(sim, target, shadow.CalcScalingSpellDmg(dpImpactScale), spell.OutcomeMagicHitAndCrit) + spell.DamageMultiplier /= float64(shadow.orbsConsumed) + if result.Landed() { + shadow.ShadowOrbs.Spend(shadow.orbsConsumed, actionID, sim) + spell.RelatedDotSpell.Cast(sim, target) + } + + spell.DealOutcome(sim, result) + }, + + ExtraCastCondition: func(sim *core.Simulation, target *core.Unit) bool { + + // At least 1 shadow orb needs to be present + return shadow.ShadowOrbs.CanSpend(1) + }, + }) + + shadow.DevouringPlague.RelatedDotSpell = shadow.RegisterSpell(core.SpellConfig{ + ActionID: actionID.WithTag(1), + SpellSchool: core.SpellSchoolShadow, + ProcMask: core.ProcMaskSpellDamage, + Flags: core.SpellFlagDisease | core.SpellFlagPassiveSpell, + ClassSpellMask: priest.PriestSpellDevouringPlagueDoT, + DamageMultiplier: 1, + DamageMultiplierAdditive: 1, + ThreatMultiplier: 1, + CritMultiplier: shadow.DefaultCritMultiplier(), + Dot: core.DotConfig{ + Aura: core.Aura{ + Label: "Devouring Plague", + }, + NumberOfTicks: 6, + TickLength: time.Second, + AffectedByCastSpeed: true, + BonusCoefficient: DpDotCoeff, + + OnSnapshot: func(sim *core.Simulation, target *core.Unit, dot *core.Dot, isRollover bool) { + dot.Spell.DamageMultiplier *= float64(shadow.orbsConsumed) + dot.Snapshot(target, shadow.CalcScalingSpellDmg(DpDotScale)) + dot.Spell.DamageMultiplier /= float64(shadow.orbsConsumed) + }, + OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeSnapshotCrit) + }, + }, + ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { + spell.Dot(target).Apply(sim) + }, + }) +} diff --git a/sim/priest/shadow/divine_star.go b/sim/priest/shadow/divine_star.go new file mode 100644 index 0000000000..1dd8bde4f2 --- /dev/null +++ b/sim/priest/shadow/divine_star.go @@ -0,0 +1,73 @@ +package shadow + +import ( + "time" + + "github.com/wowsims/mop/sim/core" +) + +const divineStarScale = 4.495 +const divineStarCoeff = 0.455 +const divineStarVariance = 0.5 + +func (shadow *ShadowPriest) registerDivineStar() { + if !shadow.Talents.DivineStar { + return + } + + shadow.RegisterSpell(core.SpellConfig{ + ActionID: core.ActionID{SpellID: 122128}, + SpellSchool: core.SpellSchoolShadow, + Flags: core.SpellFlagAPL, + ProcMask: core.ProcMaskSpellDamage, + DamageMultiplier: 1, + CritMultiplier: shadow.DefaultCritMultiplier(), + BonusCoefficient: divineStarCoeff, + ThreatMultiplier: 1, + MaxRange: 30, + ManaCost: core.ManaCostOptions{ + BaseCostPercent: 4.5, + }, + Cast: core.CastConfig{ + DefaultCast: core.Cast{ + GCD: core.GCDDefault, + }, + CD: core.Cooldown{ + Timer: shadow.NewTimer(), + Duration: time.Second * 15, + }, + }, + ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { + hit1 := shadow.DistanceFromTarget / 24 + hit2 := 2.5 - hit1 + + // first hit + core.StartDelayedAction(sim, core.DelayedActionOptions{ + DoAt: sim.CurrentTime + time.Second*time.Duration(hit1), + OnAction: func(s *core.Simulation) { + for _, unit := range sim.Encounter.TargetUnits { + spell.CalcAndDealDamage( + sim, + unit, + shadow.CalcAndRollDamageRange(sim, divineStarScale, divineStarVariance), + spell.OutcomeMagicHitAndCrit, + ) + } + }}) + + // second hit + core.StartDelayedAction(sim, core.DelayedActionOptions{ + DoAt: sim.CurrentTime + time.Second*time.Duration(hit2), + OnAction: func(s *core.Simulation) { + for _, unit := range sim.Encounter.TargetUnits { + spell.CalcAndDealDamage( + sim, + unit, + shadow.CalcAndRollDamageRange(sim, divineStarScale, divineStarVariance), + spell.OutcomeMagicHitAndCrit, + ) + } + }}) + }, + }) +} diff --git a/sim/priest/shadow/halo.go b/sim/priest/shadow/halo.go new file mode 100644 index 0000000000..c4cc21449d --- /dev/null +++ b/sim/priest/shadow/halo.go @@ -0,0 +1,60 @@ +package shadow + +import ( + "math" + "time" + + "github.com/wowsims/mop/sim/core" + "github.com/wowsims/mop/sim/priest" +) + +const haloScale = 19.266 +const haloVariance = 0.5 +const haloCoeff = 1.95 + +func (shadow *ShadowPriest) registerHalo() { + if !shadow.Talents.Halo { + return + } + shadow.RegisterSpell(core.SpellConfig{ + ActionID: core.ActionID{SpellID: 120696}, + SpellSchool: core.SpellSchoolShadow, + Flags: core.SpellFlagAPL, + DamageMultiplier: 1, + ThreatMultiplier: 1, + BonusCoefficient: haloCoeff, + ClassSpellMask: priest.PriestSpellHalo, + CritMultiplier: shadow.DefaultCritMultiplier(), + MissileSpeed: 10, + ManaCost: core.ManaCostOptions{ + BaseCostPercent: 13.5, + }, + Cast: core.CastConfig{ + DefaultCast: core.Cast{ + GCD: core.GCDDefault, + }, + CD: core.Cooldown{ + Timer: shadow.NewTimer(), + Duration: time.Second * 40, + }, + }, + ProcMask: core.ProcMaskSpellDamage, + MaxRange: 30, + ApplyEffects: func(sim *core.Simulation, _ *core.Unit, spell *core.Spell) { + spell.WaitTravelTime(sim, func(s *core.Simulation) { + baseDamage := shadow.CalcAndRollDamageRange(sim, haloScale, haloVariance) + distMod := calcHaloMod(shadow.DistanceFromTarget) + spell.DamageMultiplier *= distMod + for _, target := range sim.Encounter.TargetUnits { + spell.CalcAndDealDamage(sim, target, baseDamage, spell.OutcomeMagicHitAndCrit) + } + spell.DamageMultiplier /= distMod + }) + }, + }) +} + +// https://web.archive.org/web/20120626065654/http://us.battle.net/wow/en/forum/topic/5889309137?page=5#97 +func calcHaloMod(distance float64) float64 { + return 0.5*math.Pow(1.01, -1*math.Pow(((distance-25)/2), 4)) + 0.1 + 0.015*distance +} diff --git a/sim/priest/mind_blast.go b/sim/priest/shadow/mind_blast.go similarity index 55% rename from sim/priest/mind_blast.go rename to sim/priest/shadow/mind_blast.go index 0a28b0bcac..a1889183f0 100644 --- a/sim/priest/mind_blast.go +++ b/sim/priest/shadow/mind_blast.go @@ -1,24 +1,29 @@ -package priest +package shadow import ( "time" "github.com/wowsims/mop/sim/core" + "github.com/wowsims/mop/sim/priest" ) -func (priest *Priest) registerMindBlastSpell() { - priest.RegisterSpell(core.SpellConfig{ +const mbScale = 2.638 +const mbCoeff = 1.909 +const mbVariance = 0.055 + +func (shadow *ShadowPriest) registerMindBlastSpell() { + shadow.MindBlast = shadow.RegisterSpell(core.SpellConfig{ ActionID: core.ActionID{SpellID: 8092}, SpellSchool: core.SpellSchoolShadow, ProcMask: core.ProcMaskSpellDamage, Flags: core.SpellFlagAPL, - ClassSpellMask: PriestSpellMindBlast, + ClassSpellMask: priest.PriestSpellMindBlast, DamageMultiplier: 1, DamageMultiplierAdditive: 1, - CritMultiplier: priest.DefaultCritMultiplier(), + CritMultiplier: shadow.DefaultCritMultiplier(), ManaCost: core.ManaCostOptions{ - BaseCostPercent: 17, + BaseCostPercent: 3, PercentModifier: 100, }, @@ -28,21 +33,21 @@ func (priest *Priest) registerMindBlastSpell() { CastTime: time.Millisecond * 1500, }, CD: core.Cooldown{ - Timer: priest.NewTimer(), + Timer: shadow.NewTimer(), Duration: time.Second * 8, }, }, ThreatMultiplier: 1, - BonusCoefficient: 1.104, + BonusCoefficient: mbCoeff, ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { - baseDamage := priest.calcBaseDamage(sim, 1.557, 0.055) + baseDamage := shadow.CalcAndRollDamageRange(sim, mbScale, mbVariance) result := spell.CalcDamage(sim, target, baseDamage, spell.OutcomeMagicHitAndCrit) + if result.Landed() { + shadow.ShadowOrbs.Gain(1, spell.ActionID, sim) + } + spell.DealDamage(sim, result) }, - ExpectedInitialDamage: func(sim *core.Simulation, target *core.Unit, spell *core.Spell, _ bool) *core.SpellResult { - baseDamage := priest.calcBaseDamage(sim, 1.557, 0.055) - return spell.CalcDamage(sim, target, baseDamage, spell.OutcomeExpectedMagicHitAndCrit) - }, }) } diff --git a/sim/priest/mind_flay.go b/sim/priest/shadow/mind_flay.go similarity index 69% rename from sim/priest/mind_flay.go rename to sim/priest/shadow/mind_flay.go index 2cf72a94b5..a90f12f12f 100644 --- a/sim/priest/mind_flay.go +++ b/sim/priest/shadow/mind_flay.go @@ -1,22 +1,24 @@ -package priest +package shadow import ( "time" "github.com/wowsims/mop/sim/core" + "github.com/wowsims/mop/sim/priest" ) -func (priest *Priest) newMindFlaySpell() *core.Spell { - mindFlayCoefficient := 0.19799999893 - return priest.RegisterSpell(core.SpellConfig{ +const MfCoeff = 0.5 +const MfScale = 1 + +func (shadow *ShadowPriest) registerMindFlaySpell() *core.Spell { + return shadow.RegisterSpell(core.SpellConfig{ ActionID: core.ActionID{SpellID: 15407}, SpellSchool: core.SpellSchoolShadow, ProcMask: core.ProcMaskSpellDamage, Flags: core.SpellFlagChanneled | core.SpellFlagAPL, - ClassSpellMask: PriestSpellMindFlay, + ClassSpellMask: priest.PriestSpellMindFlay, ManaCost: core.ManaCostOptions{ - BaseCostPercent: 8, - PercentModifier: 100, + BaseCostPercent: 1, }, Cast: core.CastConfig{ @@ -28,18 +30,18 @@ func (priest *Priest) newMindFlaySpell() *core.Spell { DamageMultiplier: 1, DamageMultiplierAdditive: 1, ThreatMultiplier: 1, - CritMultiplier: priest.DefaultCritMultiplier(), + CritMultiplier: shadow.DefaultCritMultiplier(), Dot: core.DotConfig{ Aura: core.Aura{ - Label: "MindFlay-" + priest.Label, + Label: "MindFlay-" + shadow.Label, }, NumberOfTicks: 3, TickLength: time.Second * 1, AffectedByCastSpeed: true, HasteReducesDuration: true, - BonusCoefficient: 0.2879999876, + BonusCoefficient: MfCoeff, OnSnapshot: func(sim *core.Simulation, target *core.Unit, dot *core.Dot, isRollover bool) { - dot.Snapshot(target, priest.CalcScalingSpellDmg(mindFlayCoefficient)) + dot.Snapshot(target, shadow.CalcScalingSpellDmg(MfScale)) }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeSnapshotCrit) @@ -53,7 +55,7 @@ func (priest *Priest) newMindFlaySpell() *core.Spell { } }, ExpectedTickDamage: func(sim *core.Simulation, target *core.Unit, spell *core.Spell, _ bool) *core.SpellResult { - return spell.CalcPeriodicDamage(sim, target, priest.CalcScalingSpellDmg(mindFlayCoefficient), spell.OutcomeExpectedMagicCrit) + return spell.CalcPeriodicDamage(sim, target, shadow.CalcScalingSpellDmg(MfScale), spell.OutcomeExpectedMagicCrit) }, }) } diff --git a/sim/priest/shadow/mind_spike.go b/sim/priest/shadow/mind_spike.go new file mode 100644 index 0000000000..e87e2ed2e7 --- /dev/null +++ b/sim/priest/shadow/mind_spike.go @@ -0,0 +1,63 @@ +package shadow + +import ( + "time" + + "github.com/wowsims/mop/sim/core" + "github.com/wowsims/mop/sim/priest" +) + +const mindSpikeCoeff = 1.304 +const mindSpikeScale = 1.277 +const mindSpikeVariance = 0.054 + +func (shadow *ShadowPriest) registerMindSpike() { + shadow.MindSpike = shadow.RegisterSpell(core.SpellConfig{ + ActionID: core.ActionID{SpellID: 73510}, + SpellSchool: core.SpellSchoolShadow, + ProcMask: core.ProcMaskSpellDamage, + Flags: core.SpellFlagAPL, + ClassSpellMask: priest.PriestSpellMindSpike, + DamageMultiplier: 1, + DamageMultiplierAdditive: 1, + CritMultiplier: shadow.DefaultCritMultiplier(), + ManaCost: core.ManaCostOptions{ + BaseCostPercent: 1, + PercentModifier: 100, + }, + + Cast: core.CastConfig{ + DefaultCast: core.Cast{ + GCD: core.GCDDefault, + CastTime: time.Millisecond * 1500, + }, + }, + + ThreatMultiplier: 1, + BonusCoefficient: mindSpikeCoeff, + ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { + result := spell.CalcDamage( + sim, + target, + shadow.CalcAndRollDamageRange(sim, mindSpikeScale, mindSpikeVariance), + spell.OutcomeMagicHitAndCrit, + ) + if result.Landed() { + if shadow.SurgeOfDarkness == nil || !shadow.SurgeOfDarkness.IsActive() { + shadow.ShadowWordPain.Dot(target).Deactivate(sim) + + // only access those if they're actually registered and talented + if shadow.VampiricTouch != nil { + shadow.VampiricTouch.Dot(target).Deactivate(sim) + } + if shadow.DevouringPlague != nil { + shadow.DevouringPlague.Dot(target).Deactivate(sim) + } + } + } + + // delay hit for dummy effect of SurgeDarkness so aura is active + spell.DealDamage(sim, result) + }, + }) +} diff --git a/sim/priest/shadow/shadow.go b/sim/priest/shadow/shadow.go index 651482574e..8e7fc6e9a7 100644 --- a/sim/priest/shadow/shadow.go +++ b/sim/priest/shadow/shadow.go @@ -1,10 +1,9 @@ package shadow import ( - "time" - "github.com/wowsims/mop/sim/core" "github.com/wowsims/mop/sim/core/proto" + "github.com/wowsims/mop/sim/core/stats" "github.com/wowsims/mop/sim/priest" ) @@ -40,18 +39,25 @@ func NewShadowPriest(character *core.Character, options *proto.Player) *ShadowPr options: shadowOptions.Options, } - // TODO: Fix this to work with the new talent system. - // spriest.SelfBuffs.PowerInfusionTarget = &proto.UnitReference{} - // if spriest.Talents.PowerInfusion && shadowOptions.Options.PowerInfusionTarget != nil { - // spriest.SelfBuffs.PowerInfusionTarget = shadowOptions.Options.PowerInfusionTarget - // } - + spriest.ShadowOrbs = spriest.NewDefaultSecondaryResourceBar(core.SecondaryResourceConfig{ + Type: proto.SecondaryResourceType_SecondaryResourceTypeShadowOrbs, + Max: 3, + }) + spriest.RegisterSecondaryResourceBar(spriest.ShadowOrbs) return spriest } type ShadowPriest struct { *priest.Priest - options *proto.ShadowPriest_Options + options *proto.ShadowPriest_Options + ShadowOrbs core.SecondaryResourceBar + orbsConsumed int32 // Number of orbs consumed by the last devouring plague cast + + // Shadow Spells + DevouringPlague *core.Spell + MindSpike *core.Spell + MindBlast *core.Spell + SurgeOfDarkness *core.Aura // Required for dummy effect } func (spriest *ShadowPriest) GetPriest() *priest.Priest { @@ -60,127 +66,46 @@ func (spriest *ShadowPriest) GetPriest() *priest.Priest { func (spriest *ShadowPriest) Initialize() { spriest.Priest.Initialize() + + spriest.AddStat(stats.HitRating, -spriest.GetBaseStats()[stats.Spirit]) + spriest.AddStatDependency(stats.Spirit, stats.HitRating, 1) + spriest.registerMindBlastSpell() + spriest.registerDevouringPlagueSpell() + spriest.registerMindSpike() + spriest.registerShadowWordDeathSpell() + spriest.registerMindFlaySpell() + spriest.registerShadowyRecall() // Mastery + spriest.registerShadowyApparition() } func (spriest *ShadowPriest) Reset(sim *core.Simulation) { spriest.Priest.Reset(sim) } -func getMasteryBonus(masteryPoints float64) float64 { - return (21.6 + masteryPoints*1.45) / 100 -} - func (spriest *ShadowPriest) ApplyTalents() { - // spriest.Priest.ApplyTalents() + spriest.Priest.ApplyTalents() // apply shadow spec specific auras - // make it an aura so it's visible that it's used in the timeline spriest.AddStaticMod(core.SpellModConfig{ - FloatValue: 0.15, - Kind: core.SpellMod_DamageDone_Pct, - }) - - spriest.RegisterAura( - core.Aura{ - Label: "ShadowPower", - Duration: core.NeverExpires, - ActionID: core.ActionID{ - SpellID: 87327, - }, - OnReset: func(aura *core.Aura, sim *core.Simulation) { - aura.Activate(sim) - }, - }, - ) - - // Shadow Power - spriest.AddStaticMod(core.SpellModConfig{ - Kind: core.SpellMod_CritMultiplier_Flat, - FloatValue: 1.0, + FloatValue: 0.25, School: core.SpellSchoolShadow, - ClassMask: int64(priest.PriestShadowSpells), - }) - - shadowOrbMod := spriest.AddDynamicMod(core.SpellModConfig{ - ClassMask: int64(priest.PriestSpellMindBlast) | int64(priest.PriestSpellMindSpike), - FloatValue: getMasteryBonus(spriest.GetMasteryPoints()), Kind: core.SpellMod_DamageDone_Pct, }) - // mastery aura - spriest.ShadowOrbsAura = spriest.RegisterAura(core.Aura{ - Label: "Shadow Orb", - ActionID: core.ActionID{SpellID: 77487}, - Duration: time.Minute, - MaxStacks: 3, - OnStacksChange: func(_ *core.Aura, _ *core.Simulation, oldStacks int32, newStacks int32) { - shadowOrbMod.UpdateFloatValue(getMasteryBonus(spriest.GetMasteryPoints()) * float64(newStacks)) - shadowOrbMod.Activate() - }, - - OnSpellHitDealt: func(aura *core.Aura, sim *core.Simulation, spell *core.Spell, _ *core.SpellResult) { - if spell.ClassSpellMask&(priest.PriestSpellMindBlast|priest.PriestSpellMindSpike) == 0 { - return - } - - spriest.EmpoweredShadowAura.Deactivate(sim) - spriest.EmpoweredShadowAura.Activate(sim) - aura.Deactivate(sim) - }, - - OnExpire: func(aura *core.Aura, sim *core.Simulation) { - shadowOrbMod.Deactivate() - }, - }) - - spriest.AddOnMasteryStatChanged(func(sim *core.Simulation, oldMastery, newMastery float64) { - shadowOrbMod.UpdateFloatValue(getMasteryBonus(core.MasteryRatingToMasteryPoints(newMastery)) * float64(spriest.ShadowOrbsAura.GetStacks())) - }) - - empoweredShadowMod := spriest.AddDynamicMod(core.SpellModConfig{ - ClassMask: priest.PriestSpellDoT | priest.PriestSpellMindSear, - Kind: core.SpellMod_DamageDone_Flat, - FloatValue: getMasteryBonus(spriest.GetMasteryPoints()), - }) - - spriest.EmpoweredShadowAura = spriest.RegisterAura(core.Aura{ - Label: "Empowered Shadow", - ActionID: core.ActionID{SpellID: 95799}, - Duration: time.Second * 15, - OnGain: func(aura *core.Aura, sim *core.Simulation) { - empoweredShadowMod.UpdateFloatValue(getMasteryBonus(aura.Unit.GetMasteryPoints())) - empoweredShadowMod.Activate() + core.MakePermanent(spriest.RegisterAura(core.Aura{ + Label: "Shadowform", + ActionID: core.ActionID{ + SpellID: 15473, }, + })) - OnExpire: func(aura *core.Aura, sim *core.Simulation) { - empoweredShadowMod.Deactivate() - }, - }) + core.MakePermanent(core.MindQuickeningAura(&spriest.Unit)) - spriest.RegisterAura(core.Aura{ - Label: "Shadow Orb Power", - Duration: core.NeverExpires, - OnReset: func(aura *core.Aura, sim *core.Simulation) { - aura.Activate(sim) - }, - OnPeriodicDamageDealt: func(_ *core.Aura, sim *core.Simulation, spell *core.Spell, result *core.SpellResult) { - handleShadowOrbPower(spriest, sim, spell, result) - }, - }) -} - -func handleShadowOrbPower(spriest *ShadowPriest, sim *core.Simulation, spell *core.Spell, result *core.SpellResult) { - if !result.Landed() { - return - } - - if spell.ClassSpellMask&(priest.PriestSpellShadowWordPain|priest.PriestSpellMindFlay) > 0 { - // TODO: Fix this to work with the new talent system. - // procChance := 0.1 + float64(spriest.Talents.HarnessedShadows)*0.04 - procChance := 0.1 - if sim.Proc(procChance, "Shadow Orb Power") { - spriest.ShadowOrbsAura.Activate(sim) - spriest.ShadowOrbsAura.AddStack(sim) - } - } + spriest.registerTwistOfFate() + spriest.registerSolaceAndInstanity() + spriest.registerSurgeOfDarkness() + spriest.registerDivineInsight() + spriest.registerHalo() + spriest.registerCascade() + spriest.registerDivineStar() } diff --git a/sim/priest/shadow/shadow_word_death.go b/sim/priest/shadow/shadow_word_death.go new file mode 100644 index 0000000000..6631ea5d00 --- /dev/null +++ b/sim/priest/shadow/shadow_word_death.go @@ -0,0 +1,62 @@ +package shadow + +import ( + "time" + + "github.com/wowsims/mop/sim/core" + "github.com/wowsims/mop/sim/priest" +) + +const swdScale = 2.392 +const swdCoeff = 2.157 + +func (shadow *ShadowPriest) registerShadowWordDeathSpell() { + actionId := core.ActionID{SpellID: 32379} + swdAura := shadow.RegisterAura(core.Aura{ + Label: "Shadow Word: Death", + ActionID: actionId.WithTag(1), + Duration: 9 * time.Second, + }) + + shadow.RegisterSpell(core.SpellConfig{ + ActionID: actionId, + SpellSchool: core.SpellSchoolShadow, + ProcMask: core.ProcMaskSpellDamage, + Flags: core.SpellFlagAPL, + ClassSpellMask: priest.PriestSpellShadowWordDeath, + + ManaCost: core.ManaCostOptions{ + BaseCostPercent: 2.6, + }, + + DamageMultiplier: 1, + DamageMultiplierAdditive: 1, + CritMultiplier: shadow.DefaultCritMultiplier(), + ThreatMultiplier: 1, + BonusCoefficient: swdCoeff, + + Cast: core.CastConfig{ + DefaultCast: core.Cast{ + GCD: core.GCDDefault, + }, + CD: core.Cooldown{ + Timer: shadow.NewTimer(), + Duration: time.Second * 8, + }, + }, + ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { + spell.CalcAndDealDamage(sim, target, shadow.CalcScalingSpellDmg(swdScale), spell.OutcomeMagicHitAndCrit) + if swdAura.IsActive() { + swdAura.Deactivate(sim) + return + } + + shadow.ShadowOrbs.Gain(1, actionId, sim) + swdAura.Activate(sim) + spell.CD.Reset() + }, + ExtraCastCondition: func(sim *core.Simulation, target *core.Unit) bool { + return sim.IsExecutePhase20() + }, + }) +} diff --git a/sim/priest/shadow/shadowy_apparition.go b/sim/priest/shadow/shadowy_apparition.go new file mode 100644 index 0000000000..ccc3f5fc12 --- /dev/null +++ b/sim/priest/shadow/shadowy_apparition.go @@ -0,0 +1,40 @@ +package shadow + +import ( + "github.com/wowsims/mop/sim/core" + "github.com/wowsims/mop/sim/priest" +) + +func (shadow ShadowPriest) registerShadowyApparition() { + const apparitionScaling = 0.375 + const apparitionCoeff = 0.375 + + shadow.Priest.ShadowyApparition = shadow.RegisterSpell(core.SpellConfig{ + ActionID: core.ActionID{SpellID: 148859}, + MissileSpeed: 7, + ProcMask: core.ProcMaskEmpty, // summoned guardian, should not be able to proc stuff - verify + ClassSpellMask: priest.PriestSpellShadowyApparation, + Flags: core.SpellFlagPassiveSpell, + DamageMultiplier: 1, + DamageMultiplierAdditive: 1, + CritMultiplier: shadow.DefaultCritMultiplier(), + SpellSchool: core.SpellSchoolShadow, + BonusCoefficient: apparitionCoeff, + ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { + spell.WaitTravelTime(sim, func(sim *core.Simulation) { + baseDamage := shadow.CalcScalingSpellDmg(apparitionScaling) + spell.CalcAndDealDamage(sim, target, baseDamage, spell.OutcomeMagicHitAndCrit) + }) + }, + }) + + core.MakeProcTriggerAura(&shadow.Unit, core.ProcTrigger{ + Name: "Shadowy Apparition Aura", + Callback: core.CallbackOnPeriodicDamageDealt, + Outcome: core.OutcomeCrit, + ClassSpellMask: priest.PriestSpellShadowWordPain, + Handler: func(sim *core.Simulation, _ *core.Spell, result *core.SpellResult) { + shadow.Priest.ShadowyApparition.Cast(sim, result.Target) + }, + }) +} diff --git a/sim/priest/shadow/shadowy_recall.go b/sim/priest/shadow/shadowy_recall.go new file mode 100644 index 0000000000..8ef959e962 --- /dev/null +++ b/sim/priest/shadow/shadowy_recall.go @@ -0,0 +1,121 @@ +// Implements the shadow priest's mastery +// Every tick of a priest's DoT can be replicated +// The chance is based on the mastery a priest has +package shadow + +import ( + "github.com/wowsims/mop/sim/core" + "github.com/wowsims/mop/sim/priest" +) + +func (shadow *ShadowPriest) registerShadowyRecall() { + swpDupe := shadow.buildSingleTickSpell( + 589, + priest.SwpScaleCoeff, + priest.SwpSpellCoeff, + 0, + priest.PriestSpellShadowWordPain, + nil, + ) + mfDupe := shadow.buildSingleTickSpell( + 15407, + MfScale, + MfCoeff, + 0, + priest.PriestSpellMindFlay, + nil, + ) + vtDupe := shadow.buildSingleTickSpell( + 34914, + priest.VtScaleCoeff, + priest.VtSpellCoeff, + 0, + priest.PriestSpellVampiricTouch, + nil, + ) + searDupe := shadow.buildSingleTickSpell( + 48045, + priest.SearScale, + priest.SearCoeff, + priest.SearVariance, + priest.PriestSpellMindSear, + nil, + ) + dpDupe := shadow.buildSingleTickSpell( + 2944, + DpDotScale, + DpDotCoeff, + 0, + priest.PriestSpellDevouringPlagueDoT, + func() float64 { + return float64(shadow.orbsConsumed) + }, + ) + + spellList := []*core.Spell{swpDupe, mfDupe, vtDupe, dpDupe} + core.MakePermanent(shadow.RegisterAura(core.Aura{ + Label: "Shadowy Recall (Mastery)", + OnPeriodicDamageDealt: func(aura *core.Aura, sim *core.Simulation, spell *core.Spell, result *core.SpellResult) { + if !result.Landed() || result.Damage == 0 { + return + } + + for _, dupeSpell := range spellList { + classMask := dupeSpell.ClassSpellMask &^ priest.PriestSpellShadowyRecall + if classMask == spell.ClassSpellMask && spell != dupeSpell { + if sim.Proc((shadow.GetMasteryPoints()*1.8+8*1.8)/100, "Shadowy Recall (Proc)") { + dupeSpell.Cast(sim, result.Target) + } + + return + } + } + }, + OnSpellHitDealt: func(aura *core.Aura, sim *core.Simulation, spell *core.Spell, result *core.SpellResult) { + if !result.Landed() || result.Damage == 0 || spell.ClassSpellMask != priest.PriestSpellMindSear || spell == searDupe { + return + } + + if sim.Proc((shadow.GetMasteryPoints()*1.8+8*1.8)/100, "Shadowy Recall (Proc)") { + searDupe.Cast(sim, result.Target) + } + }, + })) +} + +func (shadow *ShadowPriest) buildSingleTickSpell(spellId int32, scale float64, coeff float64, variance float64, classMask int64, customDamageMod func() float64) *core.Spell { + return shadow.RegisterSpell(core.SpellConfig{ + ActionID: core.ActionID{SpellID: spellId}.WithTag(77486), + SpellSchool: core.SpellSchoolShadow, + Flags: core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell, + BonusCoefficient: coeff, + CritMultiplier: shadow.DefaultCritMultiplier(), + ThreatMultiplier: 1, + DamageMultiplier: 1, + ClassSpellMask: classMask | priest.PriestSpellShadowyRecall, + ProcMask: core.ProcMaskSpellDamage, + ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { + if customDamageMod != nil { + spell.DamageMultiplier *= customDamageMod() + } + baseDamage := core.TernaryFloat64( + variance > 0, + shadow.CalcAndRollDamageRange(sim, scale, variance), + shadow.CalcScalingSpellDmg(scale), + ) + result := spell.CalcDamage(sim, target, baseDamage, spell.OutcomeMagicHitAndCritNoHitCounter) + if customDamageMod != nil { + spell.DamageMultiplier /= customDamageMod() + } + if result.Landed() { + if result.DidCrit() { + spell.SpellMetrics[result.Target.UnitIndex].CritTicks++ + } else { + spell.SpellMetrics[result.Target.UnitIndex].Ticks++ + } + } + + spell.DealPeriodicDamage(sim, result) + }, + }) +} diff --git a/sim/priest/shadow/talents.go b/sim/priest/shadow/talents.go new file mode 100644 index 0000000000..68b8bad851 --- /dev/null +++ b/sim/priest/shadow/talents.go @@ -0,0 +1,226 @@ +package shadow + +import ( + "time" + + "github.com/wowsims/mop/sim/core" + "github.com/wowsims/mop/sim/priest" +) + +func (shadow *ShadowPriest) registerSurgeOfDarkness() { + shadow.SurgeOfDarkness = shadow.RegisterAura(core.Aura{ + Label: "Surge of Darkness", + ActionID: core.ActionID{SpellID: 87160}, + MaxStacks: 2, + Duration: core.NeverExpires, + OnSpellHitDealt: func(aura *core.Aura, sim *core.Simulation, spell *core.Spell, result *core.SpellResult) { + if spell != shadow.MindSpike { + return + } + + aura.RemoveStack(sim) + }, + }).AttachSpellMod(core.SpellModConfig{ + ClassMask: priest.PriestSpellMindSpike, + Kind: core.SpellMod_DamageDone_Pct, + FloatValue: 0.5, + }).AttachSpellMod(core.SpellModConfig{ + ClassMask: priest.PriestSpellMindSpike, + Kind: core.SpellMod_PowerCost_Pct, + FloatValue: -100, + }).AttachSpellMod(core.SpellModConfig{ + ClassMask: priest.PriestSpellMindSpike, + Kind: core.SpellMod_CastTime_Pct, + FloatValue: -1, + }) + + // Always register the auras above, or else APL might behave funky + // if the aura is not registered and we write a condition for it, it might evaluate to nil + // causing us to not have a condition to begin with + if !shadow.Talents.FromDarknessComesLight { + return + } + + core.MakePermanent(shadow.RegisterAura(core.Aura{ + Label: "Surge of Darkness (Talent)", + OnPeriodicDamageDealt: func(aura *core.Aura, sim *core.Simulation, spell *core.Spell, result *core.SpellResult) { + // use class mask here due to mastery duplication + if spell.ClassSpellMask&priest.PriestSpellVampiricTouch == 0 { + return + } + + if sim.Proc(0.2, "Roll Surge of Darkness") { + shadow.SurgeOfDarkness.Activate(sim) + shadow.SurgeOfDarkness.AddStack(sim) + } + }, + })) +} + +func (shadow *ShadowPriest) registerSolaceAndInstanity() { + if !shadow.Talents.SolaceAndInsanity { + return + } + + shadow.RegisterSpell(core.SpellConfig{ + ActionID: core.ActionID{SpellID: 129197}, + SpellSchool: core.SpellSchoolShadow, + ProcMask: core.ProcMaskSpellDamage, + Flags: core.SpellFlagChanneled | core.SpellFlagAPL, + ClassSpellMask: priest.PriestSpellMindFlay, + ManaCost: core.ManaCostOptions{ + BaseCostPercent: 1, + }, + + Cast: core.CastConfig{ + DefaultCast: core.Cast{ + GCD: core.GCDDefault, + }, + }, + + DamageMultiplier: 1, + DamageMultiplierAdditive: 1, + ThreatMultiplier: 1, + CritMultiplier: shadow.DefaultCritMultiplier(), + Dot: core.DotConfig{ + Aura: core.Aura{ + Label: "MindFlay-Insanity", + }, + NumberOfTicks: 3, + TickLength: time.Second * 1, + AffectedByCastSpeed: true, + HasteReducesDuration: true, + BonusCoefficient: MfCoeff, + OnSnapshot: func(sim *core.Simulation, target *core.Unit, dot *core.Dot, isRollover bool) { + dot.Snapshot(target, shadow.CalcScalingSpellDmg(MfScale)) + }, + OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeSnapshotCrit) + }, + }, + ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { + result := spell.CalcOutcome(sim, target, spell.OutcomeMagicHitNoHitCounter) + if result.Landed() { + spell.Dot(target).Apply(sim) + spell.DealOutcome(sim, result) + } + }, + ExpectedTickDamage: func(sim *core.Simulation, target *core.Unit, spell *core.Spell, _ bool) *core.SpellResult { + return spell.CalcPeriodicDamage(sim, target, shadow.CalcScalingSpellDmg(MfScale), spell.OutcomeExpectedMagicCrit) + }, + ExtraCastCondition: func(sim *core.Simulation, target *core.Unit) bool { + return shadow.DevouringPlague.Dot(target).IsActive() + }, + }) + + dmgMod := shadow.AddDynamicMod(core.SpellModConfig{ + Kind: core.SpellMod_DamageDone_Pct, + ClassMask: priest.PriestSpellMindFlay, + FloatValue: 0.33, + }) + + shadow.OnSpellRegistered(func(spell *core.Spell) { + if spell.ClassSpellMask == priest.PriestSpellDevouringPlagueDoT { + for _, target := range shadow.Env.Encounter.TargetUnits { + dot := spell.Dot(target) + if dot != nil { + dot.ApplyOnGain(func(aura *core.Aura, sim *core.Simulation) { + dmgMod.UpdateFloatValue(float64(shadow.orbsConsumed) * 1 / 3) + dmgMod.Activate() + }) + dot.ApplyOnExpire(func(aura *core.Aura, sim *core.Simulation) { + dmgMod.Deactivate() + }) + } + } + } + }) +} + +func (shadow *ShadowPriest) registerTwistOfFate() { + dmgMod := shadow.AddDynamicMod(core.SpellModConfig{ + Kind: core.SpellMod_DamageDone_Pct, + School: core.SpellSchoolShadow | core.SpellSchoolHoly, + FloatValue: 0.15, + }) + + tofAura := shadow.RegisterAura(core.Aura{ + Label: "Twist of Fate", + ActionID: core.ActionID{SpellID: 123254}, + Duration: time.Second * 10, + OnGain: func(aura *core.Aura, sim *core.Simulation) { + dmgMod.Activate() + }, + OnExpire: func(aura *core.Aura, sim *core.Simulation) { + dmgMod.Deactivate() + }, + }) + + if !shadow.Talents.TwistOfFate { + return + } + core.MakePermanent(shadow.RegisterAura(core.Aura{ + Label: "Twist of Fate (Talent)", + OnPeriodicDamageDealt: func(aura *core.Aura, sim *core.Simulation, spell *core.Spell, result *core.SpellResult) { + if sim.IsExecutePhase35() { + tofAura.Activate(sim) + } + }, + OnSpellHitDealt: func(aura *core.Aura, sim *core.Simulation, spell *core.Spell, result *core.SpellResult) { + if sim.IsExecutePhase35() { + tofAura.Activate(sim) + } + }, + })) +} + +func (shadow *ShadowPriest) registerDivineInsight() { + castTimeMod := shadow.AddDynamicMod(core.SpellModConfig{ + ClassMask: priest.PriestSpellMindBlast, + Kind: core.SpellMod_CastTime_Pct, + FloatValue: -1, + }) + + costMod := shadow.AddDynamicMod(core.SpellModConfig{ + ClassMask: priest.PriestSpellMindBlast, + Kind: core.SpellMod_PowerCost_Pct, + FloatValue: -100, + }) + + procAura := shadow.RegisterAura(core.Aura{ + Label: "Divine Insight", + Duration: time.Second * 12, + ActionID: core.ActionID{SpellID: 124430}, + OnGain: func(aura *core.Aura, sim *core.Simulation) { + castTimeMod.Activate() + costMod.Activate() + shadow.MindBlast.CD.Reset() + }, + OnExpire: func(aura *core.Aura, sim *core.Simulation) { + costMod.Deactivate() + castTimeMod.Deactivate() + }, + OnSpellHitDealt: func(aura *core.Aura, sim *core.Simulation, spell *core.Spell, result *core.SpellResult) { + if spell == shadow.MindBlast { + aura.Deactivate(sim) + } + }, + }) + + if !shadow.Talents.DivineInsight { + return + } + + core.MakePermanent(shadow.RegisterAura(core.Aura{ + Label: "Divine Insight (Talent)", + OnPeriodicDamageDealt: func(aura *core.Aura, sim *core.Simulation, spell *core.Spell, result *core.SpellResult) { + if spell.ClassSpellMask&priest.PriestSpellShadowWordPain == 0 { + return + } + + if sim.Proc(0.05, "Divine Insight (Proc)") { + procAura.Activate(sim) + } + }, + })) +} diff --git a/sim/priest/shadow_word_death.go b/sim/priest/shadow_word_death.go deleted file mode 100644 index 3261324da2..0000000000 --- a/sim/priest/shadow_word_death.go +++ /dev/null @@ -1,57 +0,0 @@ -package priest - -import ( - "time" - - "github.com/wowsims/mop/sim/core" -) - -func (priest *Priest) registerShadowWordDeathSpell() { - priest.RegisterSpell(core.SpellConfig{ - ActionID: core.ActionID{SpellID: 32379}, - SpellSchool: core.SpellSchoolShadow, - ProcMask: core.ProcMaskSpellDamage, - Flags: core.SpellFlagAPL, - ClassSpellMask: PriestSpellShadowWordDeath, - - ManaCost: core.ManaCostOptions{ - BaseCostPercent: 12, - PercentModifier: 100, - }, - - DamageMultiplier: 1, - DamageMultiplierAdditive: 1, - CritMultiplier: priest.DefaultCritMultiplier(), - ThreatMultiplier: 1, - BonusCoefficient: 0.316, - - Cast: core.CastConfig{ - DefaultCast: core.Cast{ - GCD: core.GCDDefault, - }, - CD: core.Cooldown{ - Timer: priest.NewTimer(), - Duration: time.Second * 12, - }, - }, - ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { - if sim.IsExecutePhase25() { - spell.DamageMultiplier *= 3 - } - spell.CalcAndDealDamage(sim, target, priest.ClassSpellScaling*0.357, spell.OutcomeMagicHitAndCrit) - if sim.IsExecutePhase25() { - spell.DamageMultiplier /= 3 - } - }, - ExpectedInitialDamage: func(sim *core.Simulation, target *core.Unit, spell *core.Spell, _ bool) *core.SpellResult { - if sim.IsExecutePhase25() { - spell.DamageMultiplier *= 3 - } - result := spell.CalcDamage(sim, target, priest.ClassSpellScaling*0.357, spell.OutcomeExpectedMagicHitAndCrit) - if sim.IsExecutePhase25() { - spell.DamageMultiplier /= 3 - } - return result - }, - }) -} diff --git a/sim/priest/devouring_plague.go b/sim/priest/shadow_word_pain.go similarity index 54% rename from sim/priest/devouring_plague.go rename to sim/priest/shadow_word_pain.go index 573fe03e31..d3339b2062 100644 --- a/sim/priest/devouring_plague.go +++ b/sim/priest/shadow_word_pain.go @@ -6,23 +6,27 @@ import ( "github.com/wowsims/mop/sim/core" ) -func (priest *Priest) registerDevouringPlagueSpell() { - actionID := core.ActionID{SpellID: 2944, Tag: 0} - priest.DevouringPlague = priest.RegisterSpell(core.SpellConfig{ - ActionID: actionID, - SpellSchool: core.SpellSchoolShadow, - ProcMask: core.ProcMaskSpellDamage, - Flags: core.SpellFlagDisease | core.SpellFlagAPL, - ClassSpellMask: PriestSpellDevouringPlague, +const SwpScaleCoeff = 0.743 * 0.85 +const SwpSpellCoeff = 0.366 * 0.85 + +func (priest *Priest) registerShadowWordPainSpell() { + priest.ShadowWordPain = priest.RegisterSpell(core.SpellConfig{ + ActionID: core.ActionID{SpellID: 589}, + SpellSchool: core.SpellSchoolShadow, + ProcMask: core.ProcMaskSpellDamage, + Flags: core.SpellFlagAPL, + ClassSpellMask: PriestSpellShadowWordPain, + BonusCoefficient: SwpSpellCoeff, ManaCost: core.ManaCostOptions{ - BaseCostPercent: 25, + BaseCostPercent: 4.4, PercentModifier: 100, }, + DamageMultiplier: 1, DamageMultiplierAdditive: 1, - ThreatMultiplier: 1, CritMultiplier: priest.DefaultCritMultiplier(), + Cast: core.CastConfig{ DefaultCast: core.Cast{ GCD: core.GCDDefault, @@ -31,30 +35,30 @@ func (priest *Priest) registerDevouringPlagueSpell() { Dot: core.DotConfig{ Aura: core.Aura{ - Label: "DevouringPlague", + Label: "ShadowWordPain", }, - NumberOfTicks: 8, + NumberOfTicks: 6, TickLength: time.Second * 3, AffectedByCastSpeed: true, - BonusCoefficient: 0.163, + BonusCoefficient: SwpSpellCoeff, - OnSnapshot: func(sim *core.Simulation, target *core.Unit, dot *core.Dot, _ bool) { - dot.Snapshot(target, 0.144*priest.ClassSpellScaling) + OnSnapshot: func(sim *core.Simulation, target *core.Unit, dot *core.Dot, isRollover bool) { + dot.Snapshot(target, priest.CalcScalingSpellDmg(SwpScaleCoeff)) }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeSnapshotCrit) }, }, + ThreatMultiplier: 1, ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { - result := spell.CalcOutcome(sim, target, spell.OutcomeMagicHitNoHitCounter) + result := spell.CalcPeriodicDamage(sim, target, priest.CalcScalingSpellDmg(SwpScaleCoeff), spell.OutcomeMagicHitAndCrit) if result.Landed() { spell.Dot(target).Apply(sim) + spell.DealOutcome(sim, result) } - - spell.DealOutcome(sim, result) }, ExpectedTickDamage: func(sim *core.Simulation, target *core.Unit, spell *core.Spell, useSnapshot bool) *core.SpellResult { @@ -62,8 +66,7 @@ func (priest *Priest) registerDevouringPlagueSpell() { dot := spell.Dot(target) return dot.CalcSnapshotDamage(sim, target, dot.OutcomeExpectedMagicSnapshotCrit) } else { - baseDamage := 0.144 * priest.ClassSpellScaling - return spell.CalcPeriodicDamage(sim, target, baseDamage, spell.OutcomeExpectedMagicCrit) + return spell.CalcPeriodicDamage(sim, target, priest.CalcScalingSpellDmg(SwpScaleCoeff), spell.OutcomeExpectedMagicCrit) } }, }) diff --git a/sim/priest/shadowfiend.go b/sim/priest/shadowfiend.go index 881a61ca4d..6c0170dd7f 100644 --- a/sim/priest/shadowfiend.go +++ b/sim/priest/shadowfiend.go @@ -7,7 +7,7 @@ import ( ) func (priest *Priest) registerShadowfiendSpell() { - if !priest.UseShadowfiend { + if priest.Talents.Mindbender { return } @@ -17,7 +17,7 @@ func (priest *Priest) registerShadowfiendSpell() { priest.ShadowfiendAura = priest.RegisterAura(core.Aura{ ActionID: actionID, Label: "Shadowfiend", - Duration: time.Second * 15.0, + Duration: time.Second * 12.0, }) priest.Shadowfiend = priest.RegisterSpell(core.SpellConfig{ @@ -33,12 +33,12 @@ func (priest *Priest) registerShadowfiendSpell() { }, CD: core.Cooldown{ Timer: priest.NewTimer(), - Duration: time.Minute * 5, + Duration: time.Minute * 3, }, }, ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { - priest.ShadowfiendPet.EnableWithTimeout(sim, priest.ShadowfiendPet, time.Second*15.0) + priest.ShadowfiendPet.EnableWithTimeout(sim, priest.ShadowfiendPet, time.Second*12.0) priest.ShadowfiendAura.Activate(sim) }, }) diff --git a/sim/priest/shadowfiend_pet.go b/sim/priest/shadowfiend_pet.go index 0802371582..175011a423 100644 --- a/sim/priest/shadowfiend_pet.go +++ b/sim/priest/shadowfiend_pet.go @@ -21,11 +21,12 @@ type Shadowfiend struct { } var baseStats = stats.Stats{ - stats.Strength: 0, - stats.Agility: 0, - stats.Stamina: 348, - stats.Intellect: 350, - stats.AttackPower: 350, + stats.Strength: 0, + stats.Agility: 0, + stats.Stamina: 348, + stats.Intellect: 350, + // stats.AttackPower: 896, // Level 85 + stats.AttackPower: 1077, // Level 90 stats.Mana: 12295, // with 3% crit debuff, shadowfiend crits around 9-12% (TODO: verify and narrow down) @@ -35,23 +36,18 @@ var baseStats = stats.Stats{ func (priest *Priest) NewShadowfiend() *Shadowfiend { shadowfiend := &Shadowfiend{ Pet: core.NewPet(core.PetConfig{ - Name: "Shadowfiend", - Owner: &priest.Character, - BaseStats: baseStats, - StatInheritance: priest.shadowfiendStatInheritance(), - EnabledOnStart: false, - IsGuardian: false, + Name: "Shadowfiend", + Owner: &priest.Character, + BaseStats: baseStats, + StatInheritance: priest.shadowfiendStatInheritance(), + EnabledOnStart: false, + IsGuardian: false, + HasDynamicMeleeSpeedInheritance: true, }), Priest: priest, } - shadowfiend.OnPetEnable = func(sim *core.Simulation) { - shadowfiend.AutoAttacks.PauseMeleeBy(sim, time.Duration(1)) - } - - shadowfiend.DelayInitialInheritance(time.Millisecond * 500) manaMetric := priest.NewManaMetrics(core.ActionID{SpellID: 34433}) - core.MakePermanent(shadowfiend.GetOrRegisterAura(core.Aura{ Label: "Autoattack mana regen", OnSpellHitDealt: func(aura *core.Aura, sim *core.Simulation, spell *core.Spell, result *core.SpellResult) { @@ -65,13 +61,7 @@ func (priest *Priest) NewShadowfiend() *Shadowfiend { Label: "Shadowcrawl", ActionID: actionID, Duration: time.Second * 5, - OnGain: func(aura *core.Aura, sim *core.Simulation) { - shadowfiend.PseudoStats.DamageDealtMultiplier *= 1.15 - }, - OnExpire: func(aura *core.Aura, sim *core.Simulation) { - shadowfiend.PseudoStats.DamageDealtMultiplier /= 1.15 - }, - }) + }).AttachMultiplicativePseudoStatBuff(&shadowfiend.PseudoStats.DamageDealtMultiplier, 1.15) shadowfiend.Shadowcrawl = shadowfiend.RegisterSpell(core.SpellConfig{ ActionID: actionID, @@ -100,18 +90,18 @@ func (priest *Priest) NewShadowfiend() *Shadowfiend { shadowfiend.EnableAutoAttacks(shadowfiend, core.AutoAttackOptions{ MainHand: core.Weapon{ - BaseDamageMin: 331.5, - BaseDamageMax: 406.5, + BaseDamageMin: priest.CalcScalingSpellDmg(2.0), + BaseDamageMax: priest.CalcScalingSpellDmg(2.0), SwingSpeed: 1.5, NormalizedSwingSpeed: 1.5, CritMultiplier: 2, SpellSchool: core.SpellSchoolShadow, + AttackPowerPerDPS: core.DefaultAttackPowerPerDPS, }, AutoSwingMelee: true, }) - shadowfiend.AutoAttacks.MHConfig().BonusCoefficient = 0 - + shadowfiend.AutoAttacks.MHConfig().BonusCoefficient = 1 shadowfiend.EnableManaBar() priest.AddPet(shadowfiend) @@ -122,9 +112,10 @@ func (priest *Priest) shadowfiendStatInheritance() core.PetStatInheritance { return func(ownerStats stats.Stats) stats.Stats { return stats.Stats{ //still need to nail down shadow fiend crit scaling, but removing owner crit scaling after further investigation stats.PhysicalCritPercent: ownerStats[stats.SpellCritPercent], - stats.Intellect: (ownerStats[stats.Intellect] - 10) * 0.5333, - stats.Stamina: ownerStats[stats.Stamina] * 0.3, - stats.AttackPower: 4.9 * (ownerStats[stats.SpellPower] - priest.GetBaseStats()[stats.Intellect] + 10), + stats.Intellect: (ownerStats[stats.Intellect] - 10) * 0.3, + stats.Stamina: ownerStats[stats.Stamina] * 0.75, + stats.SpellPower: ownerStats[stats.SpellPower], + stats.HasteRating: ownerStats[stats.HasteRating], } } } diff --git a/sim/priest/_vampiric_touch.go b/sim/priest/vampiric_touch.go similarity index 64% rename from sim/priest/_vampiric_touch.go rename to sim/priest/vampiric_touch.go index a93ba99998..7f9eca0774 100644 --- a/sim/priest/_vampiric_touch.go +++ b/sim/priest/vampiric_touch.go @@ -6,13 +6,11 @@ import ( "github.com/wowsims/mop/sim/core" ) -func (priest *Priest) registerVampiricTouchSpell() { - if !priest.Talents.VampiricTouch { - return - } - - replSrc := priest.Env.Raid.NewReplenishmentSource(core.ActionID{SpellID: 34914}) +const VtScaleCoeff = 0.071 +const VtSpellCoeff = 0.415 +func (priest *Priest) registerVampiricTouchSpell() { + manaMetric := priest.NewManaMetrics(core.ActionID{SpellID: 34914}) priest.VampiricTouch = priest.RegisterSpell(core.SpellConfig{ ActionID: core.ActionID{SpellID: 34914}, SpellSchool: core.SpellSchoolShadow, @@ -21,7 +19,7 @@ func (priest *Priest) registerVampiricTouchSpell() { ClassSpellMask: PriestSpellVampiricTouch, ManaCost: core.ManaCostOptions{ - BaseCostPercent: 17, + BaseCostPercent: 3, PercentModifier: 100, }, @@ -38,28 +36,20 @@ func (priest *Priest) registerVampiricTouchSpell() { Dot: core.DotConfig{ Aura: core.Aura{ Label: "VampiricTouch", - OnSpellHitTaken: func(aura *core.Aura, sim *core.Simulation, spell *core.Spell, result *core.SpellResult) { - if result.Landed() && spell.ClassSpellMask == PriestSpellMindBlast { - priest.Env.Raid.ProcReplenishment(sim, replSrc) - } - }, }, NumberOfTicks: 5, TickLength: time.Second * 3, AffectedByCastSpeed: true, - BonusCoefficient: 0.352, + BonusCoefficient: VtSpellCoeff, OnSnapshot: func(sim *core.Simulation, target *core.Unit, dot *core.Dot, _ bool) { - dot.Snapshot(target, priest.ClassSpellScaling*0.101) + dot.Snapshot(target, priest.CalcScalingSpellDmg(VtScaleCoeff)) }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - if priest.Talents.Shadowform { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeSnapshotCrit) - } else { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) - } + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeSnapshotCrit) + priest.AddMana(sim, priest.MaxMana()*0.02, manaMetric) }, }, @@ -67,17 +57,15 @@ func (priest *Priest) registerVampiricTouchSpell() { result := spell.CalcOutcome(sim, target, spell.OutcomeMagicHit) if result.Landed() { spell.Dot(target).Apply(sim) + spell.DealOutcome(sim, result) } - - spell.DealOutcome(sim, result) }, 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) } else { - baseDamage := priest.ClassSpellScaling * 0.101 - return spell.CalcPeriodicDamage(sim, target, baseDamage, spell.OutcomeExpectedMagicCrit) + return spell.CalcPeriodicDamage(sim, target, priest.CalcScalingSpellDmg(VtScaleCoeff), spell.OutcomeExpectedMagicCrit) } }, }) diff --git a/ui/core/constants/mechanics.ts b/ui/core/constants/mechanics.ts index e49c0c282f..e21081e2ee 100644 --- a/ui/core/constants/mechanics.ts +++ b/ui/core/constants/mechanics.ts @@ -43,7 +43,7 @@ export const masteryPercentPerPoint: Map = new Map([ [Spec.SpecFrostMage, 2.5], [Spec.SpecDisciplinePriest, 2.5], [Spec.SpecHolyPriest, 1.25], - [Spec.SpecShadowPriest, 1.45], + [Spec.SpecShadowPriest, 1.8], [Spec.SpecAfflictionWarlock, 1.625], [Spec.SpecDemonologyWarlock, 2.3], [Spec.SpecDestructionWarlock, 1.35], diff --git a/ui/core/proto_utils/action_id.ts b/ui/core/proto_utils/action_id.ts index feb341bd41..2abb3e0339 100644 --- a/ui/core/proto_utils/action_id.ts +++ b/ui/core/proto_utils/action_id.ts @@ -276,7 +276,7 @@ export class ActionId { // handle DRT let tag = this.tag; - if (tag >= 71086) { + if (tag >= 71086 && tag <= 71096) { name = 'Dragonwrath - ' + name; tag -= 71086; } @@ -342,6 +342,8 @@ export class ActionId { name += ' (2 Tick)'; } else if (tag == 3) { name += ' (3 Tick)'; + } else if (tag == 77486) { + name += ' (Mastery)' } } else { // Gurthalak, Voice of the Deeps @@ -361,7 +363,10 @@ export class ActionId { name += ' (2 Tick)'; } else if (tag == 3) { name += ' (3 Tick)'; + } else if (tag == 77486) { + name += ' (Mastery)'; } + break; case 'Shattering Throw': case 'Skull Banner': @@ -583,9 +588,17 @@ export class ActionId { break; case 'Devouring Plague': if (tag == 1) { - name += ' (Improved)'; + name += ' (DoT)'; + break; + } + if (tag == 77486) { + name += ' (Mastery)'; break; } + case 'Shadow Word: Death': + if (tag == 1) { + name += ' (No Orb)' + } case 'Improved Steady Shot': if (tag == 2) { name += ' (pre)'; @@ -754,6 +767,19 @@ export class ActionId { if (this.spellId === 148187) { name += ' (Hit)'; } + break; + case 'Vampiric Touch': + case 'Shadow Word: Pain': + if (tag == 77486) { + name += " (Mastery)" + } + + break; + case 'Cascade': + if (tag == 1) { + name += " (Bounce)" + } + break; default: if (tag) { @@ -1032,6 +1058,7 @@ const petNameToActionId: Record = { 'Mirror Image T12 2pc': ActionId.fromSpellId(55342), 'Rune Weapon': ActionId.fromSpellId(49028), Shadowfiend: ActionId.fromSpellId(34433), + Mindbender: ActionId.fromSpellId(123040), 'Spirit Wolf 1': ActionId.fromSpellId(51533), 'Spirit Wolf 2': ActionId.fromSpellId(51533), Valkyr: ActionId.fromSpellId(71844), diff --git a/ui/core/proto_utils/logs_parser.tsx b/ui/core/proto_utils/logs_parser.tsx index c544e52427..c7aca1425a 100644 --- a/ui/core/proto_utils/logs_parser.tsx +++ b/ui/core/proto_utils/logs_parser.tsx @@ -1071,9 +1071,10 @@ export class CastLog extends SimLog { const damageDealtLogs = logs.filter((log): log is DamageDealtLog => log.isDamageDealt()); const toBucketKey = (actionId: ActionId) => { - if (actionId.spellId == 30451) { + if (actionId.spellId == 30451 || actionId.spellId == 127632) { // Arcane Blast is unique because it can finish its cast as a different // spell than it started (if stacks drop). + // Also handle Shadow's Cascade for bouncing return actionId.toStringIgnoringTag(); } else { return actionId.toString(); diff --git a/ui/index.html b/ui/index.html index c2bbdef972..11c6cf9bd3 100644 --- a/ui/index.html +++ b/ui/index.html @@ -125,7 +125,7 @@

Mists of Pandaria

Priest - Launched + Alpha
@@ -161,7 +161,7 @@

Mists of Pandaria

Priest Shadow - Launched + Alpha
diff --git a/ui/priest/shadow/apls/default.apl.json b/ui/priest/shadow/apls/default.apl.json index 1cbc9eb654..cf0c2d2ad1 100644 --- a/ui/priest/shadow/apls/default.apl.json +++ b/ui/priest/shadow/apls/default.apl.json @@ -1,27 +1,38 @@ { "type": "TypeAPL", "prepullActions": [ + {"action":{"castSpell":{"spellId":{"spellId":8092}}},"doAtValue":{"const":{"val":"-40s"}}}, + {"action":{"castSpell":{"spellId":{"spellId":8092}}},"doAtValue":{"const":{"val":"-30s"}}}, + {"action":{"castSpell":{"spellId":{"spellId":8092}}},"doAtValue":{"const":{"val":"-20s"}}}, + {"action":{"activateAura":{"auraId":{"spellId":123254}}},"doAtValue":{"const":{"val":"-2.5s"}}}, + {"action":{"castSpell":{"spellId":{"spellId":120696}}},"doAtValue":{"const":{"val":"-2.5s"}}}, {"action":{"castSpell":{"spellId":{"otherId":"OtherActionPotion"}}},"doAtValue":{"const":{"val":"-1s"}}}, {"action":{"castSpell":{"spellId":{"spellId":73510}}},"doAtValue":{"const":{"val":"-1s"}}} ], "priorityList": [ - {"action":{"condition":{"cmp":{"op":"OpGt","lhs":{"gcdTimeToReady":{}},"rhs":{"const":{"val":"100ms"}}}},"moveDuration":{"duration":{"gcdTimeToReady":{}}}}}, - {"action":{"condition":{"cmp":{"op":"OpLt","lhs":{"currentTime":{}},"rhs":{"const":{"val":"1s"}}}},"castSpell":{"spellId":{"spellId":34433}}}}, - {"action":{"condition":{"cmp":{"op":"OpLt","lhs":{"currentTime":{}},"rhs":{"const":{"val":"2s"}}}},"castSpell":{"spellId":{"spellId":2825,"tag":-1}}}}, - {"action":{"condition":{"cmp":{"op":"OpGt","lhs":{"currentTime":{}},"rhs":{"const":{"val":"15s"}}}},"autocastOtherCooldowns":{}}}, - {"action":{"condition":{"and":{"vals":[{"cmp":{"op":"OpGe","lhs":{"currentTime":{}},"rhs":{"const":{"val":"80s"}}}},{"auraIsActive":{"auraId":{"spellId":89091}}}]}},"castSpell":{"spellId":{"itemId":58091}}}}, - {"action":{"condition":{"not":{"val":{"dotIsActive":{"spellId":{"spellId":589}}}}},"castSpell":{"spellId":{"spellId":589}}}}, - {"action":{"condition":{"cmp":{"op":"OpLt","lhs":{"currentTime":{}},"rhs":{"const":{"val":"3s"}}}},"autocastOtherCooldowns":{}}}, - {"action":{"condition":{"and":{"vals":[{"not":{"val":{"auraIsActive":{"auraId":{"spellId":95799}}}}},{"not":{"val":{"auraIsActive":{"auraId":{"spellId":77487}}}}}]}},"sequence":{"name":"opener","actions":[{"castSpell":{"spellId":{"spellId":15407}}},{"channelSpell":{"spellId":{"spellId":15407},"interruptIf":{"spellChanneledTicks":{"spellId":{"spellId":15407}}},"allowRecast":true}},{"castSpell":{"spellId":{"spellId":15407}}},{"castSpell":{"spellId":{"spellId":34914}}}]}}}, - {"action":{"condition":{"and":{"vals":[{"not":{"val":{"auraIsActive":{"auraId":{"spellId":95799}}}}},{"not":{"val":{"auraIsActive":{"auraId":{"spellId":77487}}}}}]}},"sequence":{"name":"opener","actions":[{"castSpell":{"spellId":{"spellId":15407}}},{"castSpell":{"spellId":{"spellId":2944}}}]}}}, - {"action":{"condition":{"and":{"vals":[{"not":{"val":{"auraIsActive":{"auraId":{"spellId":95799}}}}},{"not":{"val":{"auraIsActive":{"auraId":{"spellId":77487}}}}}]}},"sequence":{"name":"opener","actions":[{"castSpell":{"spellId":{"spellId":8092}}}]}}}, - {"action":{"condition":{"and":{"vals":[{"not":{"val":{"auraIsActive":{"auraId":{"spellId":95799}}}}},{"not":{"val":{"auraIsActive":{"auraId":{"spellId":77487}}}}}]}},"channelSpell":{"spellId":{"spellId":15407},"interruptIf":{"cmp":{"op":"OpLe","lhs":{"gcdTimeToReady":{}},"rhs":{"channelClipDelay":{}}}}}}}, - {"action":{"condition":{"or":{"vals":[{"auraIsActive":{"auraId":{"spellId":77487}}}]}},"castSpell":{"spellId":{"spellId":8092}}}}, - {"action":{"condition":{"cmp":{"op":"OpLt","lhs":{"dotRemainingTime":{"spellId":{"spellId":34914}}},"rhs":{"math":{"op":"OpAdd","lhs":{"spellCastTime":{"spellId":{"spellId":34914}}},"rhs":{"dotTickFrequency":{"spellId":{"spellId":34914}}}}}}},"castSpell":{"spellId":{"spellId":34914}}}}, - {"action":{"condition":{"cmp":{"op":"OpLt","lhs":{"dotRemainingTime":{"spellId":{"spellId":2944}}},"rhs":{"dotTickFrequency":{"spellId":{"spellId":2944}}}}},"castSpell":{"spellId":{"spellId":2944}}}}, - {"action":{"condition":{"and":{"vals":[{"cmp":{"op":"OpEq","lhs":{"auraNumStacks":{"auraId":{"spellId":87118}}},"rhs":{"const":{"val":"5"}}}},{"cmp":{"op":"OpGt","lhs":{"dotRemainingTime":{"spellId":{"spellId":34914}}},"rhs":{"const":{"val":"5s"}}}},{"cmp":{"op":"OpGt","lhs":{"dotRemainingTime":{"spellId":{"spellId":2944}}},"rhs":{"const":{"val":"5s"}}}},{"gcdIsReady":{}}]}},"castSpell":{"spellId":{"spellId":87153}}}}, - {"action":{"castSpell":{"spellId":{"spellId":34433}}}}, - {"action":{"condition":{"and":{"vals":[{"spellCanCast":{"spellId":{"spellId":32379}}},{"isExecutePhase":{"threshold":"E25"}}]}},"castSpell":{"spellId":{"spellId":32379}}}}, - {"action":{"channelSpell":{"spellId":{"spellId":15407},"interruptIf":{"cmp":{"op":"OpLe","lhs":{"gcdTimeToReady":{}},"rhs":{"channelClipDelay":{}}}}}}} + {"action":{"autocastOtherCooldowns":{}}}, + {"action":{"condition":{"and":{"vals":[{"isExecutePhase":{"threshold":"E20"}}]}},"castSpell":{"spellId":{"itemId":76093}}}}, + {"action":{"condition":{"spellIsKnown":{"spellId":{"spellId":123040}}},"castSpell":{"spellId":{"spellId":123040}}}}, + {"action":{"condition":{"spellIsKnown":{"spellId":{"spellId":34433}}},"castSpell":{"spellId":{"spellId":34433}}}}, + {"action":{"condition":{"spellIsKnown":{"spellId":{"spellId":10060}}},"castSpell":{"spellId":{"spellId":10060}}}}, + {"action":{"condition":{"and":{"vals":[{"cmp":{"op":"OpLt","lhs":{"currentTime":{}},"rhs":{"const":{"val":"10s"}}}},{"not":{"val":{"dotIsActive":{"spellId":{"spellId":589}}}}}]}},"castSpell":{"spellId":{"spellId":589}}}}, + {"action":{"condition":{"and":{"vals":[{"cmp":{"op":"OpLt","lhs":{"currentTime":{}},"rhs":{"const":{"val":"10s"}}}},{"not":{"val":{"dotIsActive":{"spellId":{"spellId":34914}}}}}]}},"castSpell":{"spellId":{"spellId":34914}}}}, + {"action":{"condition":{"and":{"vals":[{"cmp":{"op":"OpEq","lhs":{"currentGenericResource":{}},"rhs":{"const":{"val":"3"}}}}]}},"castSpell":{"spellId":{"spellId":2944}}}}, + {"action":{"castSpell":{"spellId":{"spellId":8092}}}}, + {"action":{"castSpell":{"spellId":{"spellId":32379}}}}, + {"action":{"condition":{"and":{"vals":[{"cmp":{"op":"OpLe","lhs":{"dotRemainingTime":{"spellId":{"spellId":2944}}},"rhs":{"dotTickFrequency":{"spellId":{"spellId":2944}}}}},{"dotIsActive":{"spellId":{"spellId":2944}}}]}},"castSpell":{"spellId":{"spellId":15407}}}}, + {"action":{"condition":{"dotIsActive":{"spellId":{"spellId":2944}}},"channelSpell":{"spellId":{"spellId":15407},"interruptIf":{"gcdIsReady":{}}}}}, + {"action":{"condition":{"and":{"vals":[{"cmp":{"op":"OpEq","lhs":{"currentGenericResource":{}},"rhs":{"const":{"val":"2"}}}},{"cmp":{"op":"OpLt","lhs":{"spellTimeToReady":{"spellId":{"spellId":8092}}},"rhs":{"const":{"val":"1.5s"}}}},{"cmp":{"op":"OpLe","lhs":{"dotRemainingTime":{"spellId":{"spellId":589}}},"rhs":{"const":{"val":"8s"}}}}]}},"castSpell":{"spellId":{"spellId":589}}}}, + {"action":{"condition":{"and":{"vals":[{"cmp":{"op":"OpEq","lhs":{"currentGenericResource":{}},"rhs":{"const":{"val":"2"}}}},{"cmp":{"op":"OpLt","lhs":{"spellTimeToReady":{"spellId":{"spellId":8092}}},"rhs":{"const":{"val":"1.5s"}}}},{"cmp":{"op":"OpLt","lhs":{"spellTimeToReady":{"spellId":{"spellId":32379}}},"rhs":{"const":{"val":"1.5s"}}}},{"cmp":{"op":"OpLe","lhs":{"dotRemainingTime":{"spellId":{"spellId":589}}},"rhs":{"const":{"val":"8s"}}}},{"isExecutePhase":{"threshold":"E20"}}]}},"castSpell":{"spellId":{"spellId":589}}}}, + {"action":{"condition":{"and":{"vals":[{"cmp":{"op":"OpEq","lhs":{"currentGenericResource":{}},"rhs":{"const":{"val":"2"}}}},{"cmp":{"op":"OpLt","lhs":{"spellTimeToReady":{"spellId":{"spellId":8092}}},"rhs":{"const":{"val":"1.5s"}}}},{"cmp":{"op":"OpLe","lhs":{"dotRemainingTime":{"spellId":{"spellId":34914}}},"rhs":{"const":{"val":"8s"}}}}]}},"castSpell":{"spellId":{"spellId":34914}}}}, + {"action":{"condition":{"and":{"vals":[{"cmp":{"op":"OpEq","lhs":{"currentGenericResource":{}},"rhs":{"const":{"val":"2"}}}},{"cmp":{"op":"OpLt","lhs":{"spellTimeToReady":{"spellId":{"spellId":8092}}},"rhs":{"const":{"val":"1.5s"}}}},{"cmp":{"op":"OpLt","lhs":{"spellTimeToReady":{"spellId":{"spellId":32379}}},"rhs":{"const":{"val":"1.5s"}}}},{"cmp":{"op":"OpLe","lhs":{"dotRemainingTime":{"spellId":{"spellId":34914}}},"rhs":{"const":{"val":"8s"}}}},{"isExecutePhase":{"threshold":"E20"}}]}},"castSpell":{"spellId":{"spellId":34914}}}}, + {"action":{"condition":{"cmp":{"op":"OpLe","lhs":{"dotRemainingTime":{"spellId":{"spellId":589}}},"rhs":{"dotTickFrequency":{"spellId":{"spellId":589}}}}},"castSpell":{"spellId":{"spellId":589}}}}, + {"action":{"condition":{"cmp":{"op":"OpLe","lhs":{"dotRemainingTime":{"spellId":{"spellId":34914}}},"rhs":{"dotTickFrequency":{"spellId":{"spellId":34914}}}}},"castSpell":{"spellId":{"spellId":34914}}}}, + {"action":{"condition":{"and":{"vals":[{"cmp":{"op":"OpEq","lhs":{"currentGenericResource":{}},"rhs":{"const":{"val":"3"}}}},{"cmp":{"op":"OpLe","lhs":{"dotRemainingTime":{"spellId":{"spellId":2944}}},"rhs":{"dotTickFrequency":{"spellId":{"spellId":2944}}}}}]}},"castSpell":{"spellId":{"spellId":2944}}}}, + {"action":{"condition":{"auraIsActive":{"auraId":{"spellId":87160}}},"castSpell":{"spellId":{"spellId":73510}}}}, + {"action":{"castSpell":{"spellId":{"spellId":120696}}}}, + {"action":{"castSpell":{"spellId":{"spellId":127632}}}}, + {"action":{"castSpell":{"spellId":{"spellId":122128}}}}, + {"action":{"channelSpell":{"spellId":{"spellId":15407},"interruptIf":{"gcdIsReady":{}}}}} ] } \ No newline at end of file diff --git a/ui/priest/shadow/apls/p4.apl.json b/ui/priest/shadow/apls/p4.apl.json deleted file mode 100644 index 4aa06f4563..0000000000 --- a/ui/priest/shadow/apls/p4.apl.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "type": "TypeAPL", - "prepullActions": [ - {"action":{"itemSwap":{"swapSet":"Swap1"}},"doAtValue":{"const":{"val":"-160s"}}}, - {"action":{"activateAllStatBuffProcAuras":{"swapSet":"Swap1","statType1":7,"statType2":14,"statType3":11}},"doAtValue":{"const":{"val":"-5s"}}}, - {"action":{"itemSwap":{"swapSet":"Main"}},"doAtValue":{"const":{"val":"-1s"}}}, - {"action":{"castSpell":{"spellId":{"otherId":"OtherActionPotion"}}},"doAtValue":{"const":{"val":"-1s"}}}, - {"action":{"castSpell":{"spellId":{"spellId":73510}}},"doAtValue":{"const":{"val":"-1s"}}} - ], - "priorityList": [ - {"action":{"condition":{"cmp":{"op":"OpGt","lhs":{"gcdTimeToReady":{}},"rhs":{"const":{"val":"200ms"}}}},"moveDuration":{"duration":{"gcdTimeToReady":{}}}}}, - {"action":{"castSpell":{"spellId":{"spellId":2825,"tag":-1}}}}, - {"action":{"condition":{"and":{"vals":[{"cmp":{"op":"OpEq","lhs":{"auraNumStacks":{"auraId":{"spellId":87118}}},"rhs":{"const":{"val":"5"}}}},{"spellIsReady":{"spellId":{"spellId":87153}}}]}},"sequence":{"name":"fiend","actions":[{"castSpell":{"spellId":{"spellId":34433}}}]}}}, - {"action":{"condition":{"and":{"vals":[{"auraIsActive":{"auraId":{"spellId":34433}}},{"gcdIsReady":{}}]}},"sequence":{"name":"archangel","actions":[{"castSpell":{"spellId":{"spellId":87153}}}]}}}, - {"action":{"condition":{"and":{"vals":[{"isExecutePhase":{"threshold":"E25"}},{"or":{"vals":[{"auraIsActive":{"auraId":{"spellId":34433}}},{"and":{"vals":[{"cmp":{"op":"OpLe","lhs":{"remainingTime":{}},"rhs":{"const":{"val":"90s"}}}},{"cmp":{"op":"OpGt","lhs":{"spellTimeToReady":{"spellId":{"spellId":34433}}},"rhs":{"const":{"val":"90s"}}}}]}}]}}]}},"castSpell":{"spellId":{"itemId":58091}}}}, - {"action":{"condition":{"or":{"vals":[{"auraIsActive":{"auraId":{"spellId":34433}}},{"and":{"vals":[{"cmp":{"op":"OpLe","lhs":{"remainingTime":{}},"rhs":{"const":{"val":"90s"}}}},{"cmp":{"op":"OpGt","lhs":{"spellTimeToReady":{"spellId":{"spellId":34433}}},"rhs":{"const":{"val":"90s"}}}}]}}]}},"castAllStatBuffCooldowns":{"statType1":11,"statType2":3,"statType3":14}}}, - {"action":{"condition":{"or":{"vals":[{"cmp":{"op":"OpLt","lhs":{"remainingTime":{}},"rhs":{"spellTimeToReady":{"spellId":{"spellId":34433}}}}},{"and":{"vals":[{"cmp":{"op":"OpEq","lhs":{"numStatBuffCooldowns":{"statType1":14,"statType2":3,"statType3":11}},"rhs":{"const":{"val":"0"}}}},{"or":{"vals":[{"auraIsActive":{"auraId":{"spellId":34433}}},{"not":{"val":{"spellIsReady":{"spellId":{"spellId":34433}}}}}]}}]}},{"and":{"vals":[{"auraIsKnown":{"auraId":{"itemId":77114}}},{"cmp":{"op":"OpGt","lhs":{"spellTimeToReady":{"spellId":{"itemId":77114}}},"rhs":{"const":{"val":"10s"}}}}]}},{"and":{"vals":[{"not":{"val":{"spellIsReady":{"spellId":{"spellId":34433}}}}},{"not":{"val":{"auraIsKnown":{"auraId":{"itemId":77114}}}}},{"cmp":{"op":"OpGt","lhs":{"numStatBuffCooldowns":{"statType1":14,"statType2":3,"statType3":11}},"rhs":{"const":{"val":"0"}}}}]}}]}},"castSpell":{"spellId":{"spellId":82174}}}}, - {"action":{"condition":{"and":{"vals":[{"not":{"val":{"auraIsActive":{"auraId":{"spellId":34433}}}}},{"cmp":{"op":"OpLt","lhs":{"auraNumStacks":{"auraId":{"spellId":87118}}},"rhs":{"const":{"val":"5"}}}}]}},"channelSpell":{"spellId":{"spellId":15407},"interruptIf":{"cmp":{"op":"OpEq","lhs":{"auraNumStacks":{"auraId":{"spellId":87118}}},"rhs":{"const":{"val":"5"}}}}}}}, - {"action":{"condition":{"and":{"vals":[{"sequenceIsComplete":{"sequenceName":"fiend"}},{"spellIsReady":{"spellId":{"spellId":34433}}}]}},"resetSequence":{"sequenceName":"fiend"}}}, - {"action":{"condition":{"and":{"vals":[{"sequenceIsComplete":{"sequenceName":"archangel"}},{"spellIsReady":{"spellId":{"spellId":87153}}}]}},"resetSequence":{"sequenceName":"archangel"}}}, - {"action":{"condition":{"and":{"vals":[{"auraIsActive":{"auraId":{"spellId":87153}}},{"cmp":{"op":"OpEq","lhs":{"auraNumStacks":{"auraId":{"spellId":77487}}},"rhs":{"const":{"val":"3"}}}}]}},"castSpell":{"spellId":{"spellId":8092}}}}, - {"action":{"condition":{"and":{"vals":[{"spellCanCast":{"spellId":{"spellId":32379}}},{"isExecutePhase":{"threshold":"E25"}}]}},"castSpell":{"spellId":{"spellId":32379}}}}, - {"action":{"condition":{"and":{"vals":[{"auraIsActive":{"auraId":{"spellId":34433}}},{"cmp":{"op":"OpGe","lhs":{"spellTimeToReady":{"spellId":{"spellId":8092}}},"rhs":{"math":{"op":"OpMul","lhs":{"dotTickFrequency":{"spellId":{"spellId":15407}}},"rhs":{"const":{"val":"2"}}}}}}]}},"castSpell":{"spellId":{"spellId":73510}}}}, - {"action":{"condition":{"and":{"vals":[{"auraIsActive":{"auraId":{"spellId":34433}}},{"not":{"val":{"cmp":{"op":"OpLe","lhs":{"spellIsReady":{"spellId":{"spellId":8092}}},"rhs":{"gcdTimeToReady":{}}}}}}]}},"channelSpell":{"spellId":{"spellId":15407},"interruptIf":{"spellIsReady":{"spellId":{"spellId":8092}}}}}}, - {"action":{"condition":{"or":{"vals":[{"cmp":{"op":"OpLt","lhs":{"remainingTime":{}},"rhs":{"const":{"val":"25s"}}}},{"and":{"vals":[{"cmp":{"op":"OpGt","lhs":{"spellTimeToReady":{"spellId":{"spellId":87153}}},"rhs":{"const":{"val":"10s"}}}},{"not":{"val":{"sequenceIsReady":{"sequenceName":"archangel"}}}}]}}]}},"castSpell":{"spellId":{"spellId":26297}}}}, - {"action":{"condition":{"or":{"vals":[{"cmp":{"op":"OpLt","lhs":{"remainingTime":{}},"rhs":{"const":{"val":"25s"}}}},{"and":{"vals":[{"cmp":{"op":"OpGt","lhs":{"spellTimeToReady":{"spellId":{"spellId":87153}}},"rhs":{"const":{"val":"15s"}}}},{"not":{"val":{"sequenceIsReady":{"sequenceName":"archangel"}}}},{"not":{"val":{"auraIsActive":{"auraId":{"spellId":2825,"tag":-1}}}}}]}}]}},"castSpell":{"spellId":{"spellId":10060,"tag":-1}}}}, - {"action":{"condition":{"and":{"vals":[{"and":{"vals":[{"not":{"val":{"sequenceIsReady":{"sequenceName":"archangel"}}}}]}},{"not":{"val":{"dotIsActive":{"spellId":{"spellId":589}}}}}]}},"castSpell":{"spellId":{"spellId":589}}}}, - {"action":{"condition":{"and":{"vals":[{"and":{"vals":[{"not":{"val":{"sequenceIsReady":{"sequenceName":"archangel"}}}}]}},{"cmp":{"op":"OpLt","lhs":{"dotRemainingTime":{"spellId":{"spellId":34914}}},"rhs":{"math":{"op":"OpAdd","lhs":{"spellCastTime":{"spellId":{"spellId":34914}}},"rhs":{"dotTickFrequency":{"spellId":{"spellId":34914}}}}}}}]}},"castSpell":{"spellId":{"spellId":34914}}}}, - {"action":{"condition":{"and":{"vals":[{"and":{"vals":[{"not":{"val":{"sequenceIsReady":{"sequenceName":"archangel"}}}}]}},{"cmp":{"op":"OpLt","lhs":{"dotRemainingTime":{"spellId":{"spellId":2944}}},"rhs":{"dotTickFrequency":{"spellId":{"spellId":2944}}}}}]}},"castSpell":{"spellId":{"spellId":2944}}}}, - {"action":{"condition":{"and":{"vals":[{"auraIsActive":{"auraId":{"spellId":77487}}}]}},"castSpell":{"spellId":{"spellId":8092}}}}, - {"action":{"condition":{"and":{"vals":[{"spellCanCast":{"spellId":{"spellId":32379}}},{"isExecutePhase":{"threshold":"E25"}}]}},"castSpell":{"spellId":{"spellId":32379}}}}, - {"action":{"channelSpell":{"spellId":{"spellId":15407},"interruptIf":{"cmp":{"op":"OpLe","lhs":{"gcdTimeToReady":{}},"rhs":{"channelClipDelay":{}}}}}}}, - {"action":{"condition":{"const":{"val":"false"}},"castSpell":{"spellId":{"spellId":47585}}}} - ] -} diff --git a/ui/priest/shadow/gear_sets/p1.gear.json b/ui/priest/shadow/gear_sets/p1.gear.json index 99d82eadfb..be0e16419c 100644 --- a/ui/priest/shadow/gear_sets/p1.gear.json +++ b/ui/priest/shadow/gear_sets/p1.gear.json @@ -1,21 +1,21 @@ { "items": [ - {"id":60237,"enchant":4207,"gems":[68780,52236],"reforging":141}, - {"id":69882,"randomSuffix":-131}, - {"id":65238,"enchant":4200,"gems":[52207]}, - {"id":60232,"enchant":4115,"gems":[52208],"reforging":162}, - {"id":65237,"enchant":4102,"gems":[52207,52236],"reforging":141}, - {"id":60238,"enchant":4257,"gems":[52208,0],"reforging":167}, - {"id":65234,"enchant":4068,"gems":[52207,0],"reforging":145}, - {"id":65376,"randomSuffix":-231,"gems":[52208,52207]}, - {"id":65236,"enchant":4110,"gems":[52208,52207]}, - {"id":65069,"enchant":4104,"gems":[52207],"reforging":162}, - {"id":65373,"randomSuffix":-131}, - {"id":65123,"reforging":162}, - {"id":62047,"reforging":167}, - {"id":65053,"reforging":145}, - {"id":65041,"enchant":4097,"reforging":162}, - {"id":65133,"enchant":4091,"reforging":134}, - {"id":65064,"reforging":167} - ] + {"id":87120,"gems":[95347,76634]}, + {"id":86976}, + {"id":87123,"enchant":4806,"gems":[76634]}, + {"id":90512,"enchant":4423}, + {"id":87122,"enchant":4419,"gems":[76672,76672]}, + {"id":90510,"enchant":4414,"gems":[0]}, + {"id":87119,"enchant":4433,"gems":[0]}, + {"id":86981,"gems":[76672,76672,76672]}, + {"id":87174,"enchant":4825,"gems":[76672,76634]}, + {"id":86959,"enchant":4429,"gems":[76660]}, + {"id":90511}, + {"id":86949}, + {"id":98075}, + {"id":98019}, + {"id":90513,"enchant":4442,"gems":[76672]}, + {"id":86960,"enchant":4434}, + {} + ] } \ No newline at end of file diff --git a/ui/priest/shadow/gear_sets/p3.gear.json b/ui/priest/shadow/gear_sets/p3.gear.json deleted file mode 100644 index 97763f4e8c..0000000000 --- a/ui/priest/shadow/gear_sets/p3.gear.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "items": [ - { "id": 71533, "enchant": 4207, "gems": [68780, 52207], "reforging": 162 }, - { "id": 71472, "gems": [52207], "reforging": 162 }, - { "id": 71536, "enchant": 4200, "gems": [52208], "reforging": 141 }, - { "id": 71434, "enchant": 4115, "reforging": 145 }, - { "id": 71535, "enchant": 4102, "gems": [52208, 52236] }, - { "id": 71471, "enchant": 4257, "gems": [0], "reforging": 141 }, - { "id": 71614, "enchant": 4068, "gems": [52207, 0], "reforging": 141 }, - { "id": 71613, "gems": [52207, 52207], "reforging": 162 }, - { "id": 71534, "enchant": 4110, "gems": [52207, 52207], "reforging": 167 }, - { "id": 71447, "enchant": 4104, "gems": [52207], "reforging": 141 }, - { "id": 71217, "gems": [52207], "reforging": 134 }, - { "id": 71449, "reforging": 162 }, - { "id": 69110 }, - { "id": 62047, "reforging": 162 }, - { "id": 71086, "enchant": 4097, "gems": [52207, 52207, 52207], "reforging": 134 }, - {}, - { "id": 71575, "reforging": 148 } - ] -} diff --git a/ui/priest/shadow/gear_sets/p4.gear.json b/ui/priest/shadow/gear_sets/p4.gear.json deleted file mode 100644 index 1c6fd6236a..0000000000 --- a/ui/priest/shadow/gear_sets/p4.gear.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "items": [ - { "id": 78703, "enchant": 4207, "gems": [68780, 71881], "reforging": 147 }, - { "id": 78364, "reforging": 151 }, - { "id": 78750, "enchant": 4200, "gems": [71881, 71881], "reforging": 141 }, - { "id": 77098, "enchant": 4115, "gems": [71881], "reforging": 134 }, - { "id": 78731, "enchant": 4102, "gems": [71881, 71881, 71854], "reforging": 154 }, - { "id": 78417, "enchant": 4257, "gems": [71881, 0], "reforging": 148 }, - { "id": 78461, "enchant": 4107, "gems": [71881, 71881, 0], "reforging": 147 }, - { "id": 78391, "gems": [71881, 71881, 71881], "reforging": 147 }, - { "id": 78722, "enchant": 4110, "gems": [71881, 71881, 71854], "reforging": 148 }, - { "id": 78449, "enchant": 4104, "gems": [71881, 71881], "reforging": 147 }, - { "id": 78419, "gems": [71881], "reforging": 148 }, - { "id": 78491, "gems": [71881], "reforging": 154 }, - { "id": 77995 }, - { "id": 78000 }, - { "id": 71086, "enchant": 4097, "gems": [71881, 71881, 71881], "reforging": 154 }, - {}, - { "id": 78392, "reforging": 162 } - ] -} diff --git a/ui/priest/shadow/gear_sets/p4_item_swap.gear.json b/ui/priest/shadow/gear_sets/p4_item_swap.gear.json deleted file mode 100644 index 800b3ddf3e..0000000000 --- a/ui/priest/shadow/gear_sets/p4_item_swap.gear.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "items": [ - {}, - {}, - {}, - {}, - {}, - {}, - {}, - {}, - {}, - {}, - {}, - {}, - { - "id": 72898 - }, - { - "id": 73497 - }, - {}, - {}, - {} - ] -} diff --git a/ui/priest/shadow/gear_sets/pre_raid.gear.json b/ui/priest/shadow/gear_sets/pre_raid.gear.json new file mode 100644 index 0000000000..d3e32b6a2d --- /dev/null +++ b/ui/priest/shadow/gear_sets/pre_raid.gear.json @@ -0,0 +1,20 @@ +{ + "items": [ + {"id":77533,"gems":[76885,77546,77542]}, + {"id":81095,"reforging":145}, + {"id":81235,"enchant":4806,"gems":[76694]}, + {"id":81084,"enchant":4892}, + {"id":82439,"enchant":4419,"gems":[76686,76668],"reforging":167}, + {"id":81276,"enchant":4414,"gems":[0],"reforging":116}, + {"id":82438,"enchant":4430,"gems":[76686,0],"reforging":167}, + {"id":82861,"gems":[76694],"reforging":167}, + {"id":81106,"enchant":4826,"gems":[76668],"reforging":166}, + {"id":81127,"enchant":4429,"gems":[76668],"reforging":167}, + {"id":81182,"reforging":167}, + {"id":81232,"reforging":116}, + {"id":79331}, + {"id":81133,"reforging":117}, + {"id":81094,"enchant":4442,"reforging":116}, + {"id":79335,"enchant":4434,"reforging":167} + ] +} \ No newline at end of file diff --git a/ui/priest/shadow/gear_sets/preraid.gear.json b/ui/priest/shadow/gear_sets/preraid.gear.json deleted file mode 100644 index 1de520d4e5..0000000000 --- a/ui/priest/shadow/gear_sets/preraid.gear.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "items": [ - { "id": 60237, "enchant": 4207, "gems": [68780, 52236], "reforging": 147 }, - { "id": 71213, "reforging": 145 }, - { "id": 60253, "enchant": 4200, "gems": [52207] }, - { "id": 60232, "enchant": 4115, "gems": [52208], "reforging": 162 }, - { "id": 71279, "enchant": 4102, "gems": [52208, 52236] }, - { "id": 71266, "enchant": 4257, "gems": [0] }, - { "id": 71276, "enchant": 4068, "gems": [52236, 0], "reforging": 119 }, - { "id": 65376, "randomSuffix": -231, "gems": [52208, 52207], "reforging": 119 }, - { "id": 71273, "enchant": 4110, "gems": [52207, 52208], "reforging": 117 }, - { "id": 58486, "enchant": 4104, "gems": [52207] }, - { "id": 65373, "randomSuffix": -131, "reforging": 119 }, - { "id": 71210, "reforging": 138 }, - { "id": 65053, "reforging": 145 }, - { "id": 62047, "reforging": 167 }, - { "id": 70157, "enchant": 4097, "reforging": 117 }, - { "id": 70112, "enchant": 4091, "reforging": 145 }, - { "id": 71151, "reforging": 134 } - ] -} diff --git a/ui/priest/shadow/presets.ts b/ui/priest/shadow/presets.ts index 18520ea165..aa0d40f203 100644 --- a/ui/priest/shadow/presets.ts +++ b/ui/priest/shadow/presets.ts @@ -4,28 +4,18 @@ import { PriestMajorGlyph as MajorGlyph, PriestMinorGlyph as MinorGlyph, PriestO import { SavedTalents } from '../../core/proto/ui'; import { Stats, UnitStat, UnitStatPresets } from '../../core/proto_utils/stats'; import DefaultApl from './apls/default.apl.json'; -import P4T134PCApl from './apls/p4.apl.json'; +import PreRaidGear from './gear_sets/pre_raid.gear.json'; import P1Gear from './gear_sets/p1.gear.json'; -import P3Gear from './gear_sets/p3.gear.json'; -import P4Gear from './gear_sets/p4.gear.json'; -import ItemSwapP4 from './gear_sets/p4_item_swap.gear.json'; -import PreRaidGear from './gear_sets/preraid.gear.json'; // Preset options for this spec. // Eventually we will import these values for the raid sim too, so its good to // keep them in a separate file. -export const PRE_RAID = PresetUtils.makePresetGear('Pre Raid', PreRaidGear); +export const PRE_RAID_PRESET = PresetUtils.makePresetGear('Pre Raid Preset', PreRaidGear); export const P1_PRESET = PresetUtils.makePresetGear('P1 Preset', P1Gear); -export const P3_PRESET = PresetUtils.makePresetGear('P3 Preset', P3Gear); -export const P4_PRESET = PresetUtils.makePresetGear('P4 Preset', P4Gear); - -export const P4_ITEM_SWAP = PresetUtils.makePresetItemSwapGear('P4', ItemSwapP4); - export const ROTATION_PRESET_DEFAULT = PresetUtils.makePresetAPLRotation('Default', DefaultApl); -export const P4_T13_4PC_PRESET_DEFAULT = PresetUtils.makePresetAPLRotation('T13 - 4PC', P4T134PCApl); // Preset options for EP weights -export const P3_EP_PRESET = PresetUtils.makePresetEpWeights( +export const P1_EP_PRESET = PresetUtils.makePresetEpWeights( 'Default', Stats.fromMap({ [Stat.StatIntellect]: 1.0, @@ -122,11 +112,8 @@ export const SHADOW_BREAKPOINTS: UnitStatPresets[] = [ export const StandardTalents = { name: 'Standard', data: SavedTalents.create({ - talentsString: '', - glyphs: Glyphs.create({ - major1: MajorGlyph.GlyphOfFade, - major2: MajorGlyph.GlyphOfInnerFire, - }), + talentsString: '223113', + glyphs: Glyphs.create({}), }), }; @@ -144,18 +131,22 @@ export const DefaultConsumables = ConsumesSpec.create({ tinkerId: 82174, // Synapse Springs }); -export const DefaultRaidBuffs = RaidBuffs.create({}); +export const DefaultRaidBuffs = RaidBuffs.create({ + arcaneBrilliance: true, + blessingOfKings: true, + mindQuickening: true, + leaderOfThePack: true, + blessingOfMight: true, + unholyAura: true, + bloodlust: true, + skullBannerCount: 2, + stormlashTotemCount: 4, +}); export const DefaultIndividualBuffs = IndividualBuffs.create({}); export const DefaultDebuffs = Debuffs.create({ - // bloodFrenzy: true, - // sunderArmor: true, - // ebonPlaguebringer: true, - // mangle: true, - // criticalMass: true, - // demoralizingShout: true, - // frostFever: true, + curseOfElements: true }); export const OtherDefaults = { @@ -164,15 +155,3 @@ export const OtherDefaults = { profession1: Profession.Engineering, profession2: Profession.Tailoring, }; - -export const P3_PRESET_BUILD = PresetUtils.makePresetBuild('P3 - Default', { - race: Race.RaceTroll, - gear: P3_PRESET, - rotation: ROTATION_PRESET_DEFAULT, -}); - -export const P4_PRESET_BUILD = PresetUtils.makePresetBuild('P4 - Default', { - race: Race.RaceTroll, - gear: P4_PRESET, - rotation: P4_T13_4PC_PRESET_DEFAULT, -}); diff --git a/ui/priest/shadow/sim.ts b/ui/priest/shadow/sim.ts index 1d98946765..40d078839a 100644 --- a/ui/priest/shadow/sim.ts +++ b/ui/priest/shadow/sim.ts @@ -23,7 +23,7 @@ const SPEC_CONFIG = registerSpecConfig(Spec.SpecShadowPriest, { cssClass: 'shadow-priest-sim-ui', cssScheme: PlayerClasses.getCssClass(PlayerClasses.Priest), // List any known bugs / issues here and they'll be shown on the site. - knownIssues: ['Some items may display and use stats a litle higher than their original value.'], + knownIssues: ['Some items may display and use stats a litle higher than their original value.', 'Procs from Weapons, Trinkets and other Items are not yet supported'], // All stats for which EP should be calculated. epStats: [Stat.StatIntellect, Stat.StatSpirit, Stat.StatSpellPower, Stat.StatHitRating, Stat.StatCritRating, Stat.StatHasteRating, Stat.StatMasteryRating], @@ -50,9 +50,9 @@ const SPEC_CONFIG = registerSpecConfig(Spec.SpecShadowPriest, { defaults: { // Default equipped gear. - gear: Presets.P4_PRESET.gear, + gear: Presets.PRE_RAID_PRESET.gear, // Default EP weights for sorting gear in the gear picker. - epWeights: Presets.P3_EP_PRESET.epWeights, + epWeights: Presets.P1_EP_PRESET.epWeights, statCaps: (() => { return new Stats().withPseudoStat(PseudoStat.PseudoStatSpellHitPercent, 17); })(), @@ -77,7 +77,7 @@ const SPEC_CONFIG = registerSpecConfig(Spec.SpecShadowPriest, { // IconInputs to include in the 'Player' section on the settings tab. playerIconInputs: [PriestInputs.ArmorInput()], // Buff and Debuff inputs to include/exclude, overriding the EP-based defaults. - includeBuffDebuffInputs: [], + includeBuffDebuffInputs: [BuffDebuffInputs.AttackSpeedBuff], excludeBuffDebuffInputs: [], // Inputs to include in the 'Other' section on the settings tab. otherInputs: { @@ -90,20 +90,17 @@ const SPEC_CONFIG = registerSpecConfig(Spec.SpecShadowPriest, { }, presets: { - epWeights: [Presets.P3_EP_PRESET], + epWeights: [Presets.P1_EP_PRESET], // Preset talents that the user can quickly select. talents: [Presets.StandardTalents], - rotations: [Presets.ROTATION_PRESET_DEFAULT, Presets.P4_T13_4PC_PRESET_DEFAULT], + rotations: [Presets.ROTATION_PRESET_DEFAULT], // Preset gear configurations that the user can quickly select. - gear: [Presets.PRE_RAID, Presets.P1_PRESET, Presets.P3_PRESET, Presets.P4_PRESET], - itemSwaps: [Presets.P4_ITEM_SWAP], - builds: [Presets.P3_PRESET_BUILD, Presets.P4_PRESET_BUILD], + gear: [Presets.PRE_RAID_PRESET, Presets.P1_PRESET], + itemSwaps: [], + builds: [], }, autoRotation: (player: Player): APLRotation => { - if (hasT134(player)) { - return Presets.P4_T13_4PC_PRESET_DEFAULT.rotation.rotation!; - } return Presets.ROTATION_PRESET_DEFAULT.rotation.rotation!; }, @@ -122,12 +119,12 @@ const SPEC_CONFIG = registerSpecConfig(Spec.SpecShadowPriest, { defaultGear: { [Faction.Unknown]: {}, [Faction.Alliance]: { - 0: Presets.PRE_RAID.gear, - 1: Presets.P3_PRESET.gear, + 1: Presets.PRE_RAID_PRESET.gear, + 2: Presets.P1_PRESET.gear, }, [Faction.Horde]: { - 0: Presets.PRE_RAID.gear, - 1: Presets.P3_PRESET.gear, + 1: Presets.PRE_RAID_PRESET.gear, + 2: Presets.P1_PRESET.gear, }, }, }, @@ -137,21 +134,10 @@ const SPEC_CONFIG = registerSpecConfig(Spec.SpecShadowPriest, { export class ShadowPriestSimUI extends IndividualSimUI { constructor(parentElem: HTMLElement, player: Player) { super(parentElem, player, SPEC_CONFIG); - player.sim.waitForInit().then(() => { new ReforgeOptimizer(this, { statSelectionPresets: Presets.SHADOW_BREAKPOINTS, updateSoftCaps: softCaps => { - if (hasT134(player)) { - softCaps.push( - StatCap.fromPseudoStat(PseudoStat.PseudoStatSpellHastePercent, { - breakpoints: [hasteBreakpoints.get('7-tick - VT')!], - capType: StatCapType.TypeSoftCap, - postCapEPs: [(Presets.P3_EP_PRESET.epWeights.getStat(Stat.StatCritRating) + 0.02) * Mechanics.HASTE_RATING_PER_HASTE_PERCENT], - }), - ); - } - return softCaps; }, });