diff --git a/sim/core/aura_helpers.go b/sim/core/aura_helpers.go index a16f47927e..22d90ec000 100644 --- a/sim/core/aura_helpers.go +++ b/sim/core/aura_helpers.go @@ -365,6 +365,10 @@ func CreateDamageAbsorptionAura(character *Character, auraLabel string, actionID FreshShieldStrengthCalculator: calculator, } + aura.ApplyOnExpire(func(_ *Aura, _ *Simulation) { + aura.ShieldStrength = 0 + }) + character.AddDynamicDamageTakenModifier(func(sim *Simulation, spell *Spell, result *SpellResult) { if aura.Aura.IsActive() && result.Damage > 0 && (extraSpellCheck == nil || extraSpellCheck(spell)) { absorbedDamage := min(aura.ShieldStrength, result.Damage) diff --git a/sim/core/health.go b/sim/core/health.go index b8eaf18e05..2b1c91f276 100644 --- a/sim/core/health.go +++ b/sim/core/health.go @@ -214,12 +214,14 @@ func (character *Character) applyHealingModel(healingModel *proto.HealingModel) // Use modeled HPS to scale heal per tick based on random cadence healPerTick = healingModel.Hps * (float64(timeToNextHeal) / float64(time.Second)) * character.PseudoStats.HealingTakenMultiplier * character.PseudoStats.ExternalHealingTakenMultiplier - // Execute the direct portion of the heal - character.GainHealth(sim, healPerTick * (1.0 - absorbFrac), healthMetrics) + if healPerTick > 0 { + // Execute the direct portion of the heal + character.GainHealth(sim, healPerTick * (1.0 - absorbFrac), healthMetrics) - // Turn the remainder into an absorb shield - if absorbShield != nil { - absorbShield.Activate(sim) + // Turn the remainder into an absorb shield + if absorbShield != nil { + absorbShield.Activate(sim) + } } // Might use this again in the future to track "absorb" metrics but currently disabled diff --git a/sim/encounters/firelands/baleroc_ai.go b/sim/encounters/firelands/baleroc_ai.go index e8891024fe..7520793227 100644 --- a/sim/encounters/firelands/baleroc_ai.go +++ b/sim/encounters/firelands/baleroc_ai.go @@ -136,6 +136,7 @@ func (ai *BalerocAI) Initialize(target *core.Target, config *proto.Target) { if ai.stackCountForFirstSwap <= 0 { target.CurrentTarget = ai.MainTank + target.SecondaryTarget = ai.OffTank } ai.initialHealerStackGain = config.TargetInputs[2].NumberValue @@ -205,6 +206,10 @@ func (ai *BalerocAI) registerBlazeOfGlory() { hpDepByStackCount[i] = tankUnit.NewDynamicMultiplyStat(stats.Health, 1.0 + 0.2*float64(i)) } + // Blaze of Glory applications also heal the player, just like + // most other temporary max health increases. + healthMetrics := tankUnit.NewHealthMetrics(blazeOfGloryActionID) + tankUnit.GetOrRegisterAura(core.Aura{ Label: "Blaze of Glory", ActionID: blazeOfGloryActionID, @@ -214,6 +219,9 @@ func (ai *BalerocAI) registerBlazeOfGlory() { OnStacksChange: func(aura *core.Aura, sim *core.Simulation, oldStacks int32, newStacks int32) { aura.Unit.PseudoStats.SchoolDamageTakenMultiplier[stats.SchoolIndexPhysical] *= (1.0 + 0.2*float64(newStacks)) / (1.0 + 0.2*float64(oldStacks)) + // Cache max HP prior to processing multipliers. + oldMaxHp := aura.Unit.MaxHealth() + if oldStacks > 0 { aura.Unit.DisableDynamicStatDep(sim, hpDepByStackCount[oldStacks]) } @@ -221,6 +229,12 @@ func (ai *BalerocAI) registerBlazeOfGlory() { if newStacks > 0 { aura.Unit.EnableDynamicStatDep(sim, hpDepByStackCount[newStacks]) } + + hpGain := aura.Unit.MaxHealth() - oldMaxHp + + if hpGain > 0 { + aura.Unit.GainHealth(sim, hpGain, healthMetrics) + } }, }) } @@ -254,37 +268,11 @@ func (ai *BalerocAI) registerBlazeOfGlory() { } func (ai *BalerocAI) registerBlades() { - // 0 - 10N, 1 - 25N, 2 - 10H, 3 - 25H - scalingIndex := core.TernaryInt(ai.raidSize == 10, core.TernaryInt(ai.isHeroic, 2, 0), core.TernaryInt(ai.isHeroic, 3, 1)) - - // https://wago.tools/db2/SpellEffect?build=4.4.1.57294&filter[SpellID]=99351&page=1&sort[SpellID]=asc - infernoStrikeBase := []float64{97499, 165749, 136499, 232049}[scalingIndex] - infernoStrikeVariance := []float64{5000, 8500, 7000, 11900}[scalingIndex] - - infernoStrike := ai.Target.RegisterSpell(core.SpellConfig{ - ActionID: core.ActionID{SpellID: 99351}, - SpellSchool: core.SpellSchoolFire, - ProcMask: core.ProcMaskSpellDamage, - Flags: core.SpellFlagMeleeMetrics, - DamageMultiplier: 1, - - ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { - damageRoll := infernoStrikeBase + infernoStrikeVariance*sim.RandomFloat("Inferno Strike Damage") - spell.CalcAndDealDamage(sim, target, damageRoll, spell.OutcomeEnemyMeleeWhite) - }, - }) - + // First register the blade auras and activation spells. const bladeDuration = time.Second * 15 const bladeCooldown = time.Second * 45 // very first one is special cased as 30s const bladeCastTime = time.Millisecond * 1500 - infernoBladeActionID := core.ActionID{SpellID: 99350} - infernoBladeAura := ai.Target.RegisterAura(core.Aura{ - Label: "Inferno Blade", - ActionID: infernoBladeActionID, - Duration: bladeDuration, - }) - sharedBladeCastHandler := func(sim *core.Simulation) { // First, schedule a swing timer reset to fire on cast completion. ai.Target.AutoAttacks.StopMeleeUntil(sim, sim.CurrentTime + bladeCastTime, true) @@ -299,6 +287,13 @@ func (ai *BalerocAI) registerBlades() { ai.blazeOfGlory.CD.Set(sim.CurrentTime + ai.blazeOfGlory.CD.Duration) } + infernoBladeActionID := core.ActionID{SpellID: 99350} + infernoBladeAura := ai.Target.RegisterAura(core.Aura{ + Label: "Inferno Blade", + ActionID: infernoBladeActionID, + Duration: bladeDuration, + }) + ai.infernoBlade = ai.Target.RegisterSpell(core.SpellConfig{ ActionID: infernoBladeActionID, ProcMask: core.ProcMaskEmpty, @@ -327,44 +322,6 @@ func (ai *BalerocAI) registerBlades() { }, }) - decimatingStrikeActionID := core.ActionID{SpellID: 99353} - decimatingStrikeDebuffConfig := core.Aura{ - Label: "Decimating Strike", - ActionID: decimatingStrikeActionID, - Duration: time.Second * 4, - - OnGain: func(aura *core.Aura, _ *core.Simulation) { - aura.Unit.PseudoStats.HealingDealtMultiplier *= 0.1 - }, - - OnExpire: func(aura *core.Aura, _ *core.Simulation) { - aura.Unit.PseudoStats.HealingDealtMultiplier /= 0.1 - }, - } - - for _, tankUnit := range []*core.Unit{ai.MainTank, ai.OffTank} { - if tankUnit != nil { - tankUnit.GetOrRegisterAura(decimatingStrikeDebuffConfig) - } - } - - decimatingStrike := ai.Target.RegisterSpell(core.SpellConfig{ - ActionID: decimatingStrikeActionID, - SpellSchool: core.SpellSchoolShadow, - ProcMask: core.ProcMaskSpellDamage, - Flags: core.SpellFlagMeleeMetrics | core.SpellFlagIgnoreModifiers | core.SpellFlagIgnoreResists, - DamageMultiplier: 1, - - ApplyEffects: func(sim *core.Simulation, tankTarget *core.Unit, spell *core.Spell) { - spell.CalcAndDealDamage(sim, tankTarget, max(0.9 * tankTarget.MaxHealth(), 250000), spell.OutcomeEnemyMeleeWhite) - debuffAura := tankTarget.GetAuraByID(decimatingStrikeActionID) - - if debuffAura != nil { - debuffAura.Activate(sim) - } - }, - }) - decimationBladeActionID := core.ActionID{SpellID: 99352} decimationBladeAura := ai.Target.RegisterAura(core.Aura{ Label: "Decimation Blade", @@ -372,7 +329,7 @@ func (ai *BalerocAI) registerBlades() { Duration: bladeDuration, OnExpire: func(_ *core.Aura, sim *core.Simulation) { - if ai.tankSwap { + if ai.tankSwap && (ai.Target.CurrentTarget == ai.OffTank) { ai.swapTargets(sim, ai.MainTank) } }, @@ -410,6 +367,73 @@ func (ai *BalerocAI) registerBlades() { }, }) + // Then register the strikes that replace boss melees during each blade. + // 0 - 10N, 1 - 25N, 2 - 10H, 3 - 25H + scalingIndex := core.TernaryInt(ai.raidSize == 10, core.TernaryInt(ai.isHeroic, 2, 0), core.TernaryInt(ai.isHeroic, 3, 1)) + + // https://wago.tools/db2/SpellEffect?build=4.4.1.57294&filter[SpellID]=99351&page=1&sort[SpellID]=asc + infernoStrikeBase := []float64{97499, 165749, 136499, 232049}[scalingIndex] + infernoStrikeVariance := []float64{5000, 8500, 7000, 11900}[scalingIndex] + + infernoStrike := ai.Target.RegisterSpell(core.SpellConfig{ + ActionID: core.ActionID{SpellID: 99351}, + SpellSchool: core.SpellSchoolFire, + ProcMask: core.ProcMaskSpellDamage, + Flags: core.SpellFlagMeleeMetrics, + DamageMultiplier: 1, + + ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { + damageRoll := infernoStrikeBase + infernoStrikeVariance*sim.RandomFloat("Inferno Strike Damage") + spell.CalcAndDealDamage(sim, target, damageRoll, spell.OutcomeEnemyMeleeWhite) + }, + }) + + decimatingStrikeActionID := core.ActionID{SpellID: 99353} + decimatingStrikeDebuffConfig := core.Aura{ + Label: "Decimating Strike", + ActionID: decimatingStrikeActionID, + Duration: time.Second * 4, + + OnGain: func(aura *core.Aura, _ *core.Simulation) { + aura.Unit.PseudoStats.HealingDealtMultiplier *= 0.1 + }, + + OnExpire: func(aura *core.Aura, _ *core.Simulation) { + aura.Unit.PseudoStats.HealingDealtMultiplier /= 0.1 + }, + } + + for _, tankUnit := range []*core.Unit{ai.MainTank, ai.OffTank} { + if tankUnit != nil { + tankUnit.GetOrRegisterAura(decimatingStrikeDebuffConfig) + } + } + + decimatingStrike := ai.Target.RegisterSpell(core.SpellConfig{ + ActionID: decimatingStrikeActionID, + SpellSchool: core.SpellSchoolShadow, + ProcMask: core.ProcMaskSpellDamage, + Flags: core.SpellFlagMeleeMetrics | core.SpellFlagIgnoreModifiers | core.SpellFlagIgnoreResists, + DamageMultiplier: 1, + + ApplyEffects: func(sim *core.Simulation, tankTarget *core.Unit, spell *core.Spell) { + result := spell.CalcAndDealDamage(sim, tankTarget, max(0.9 * tankTarget.MaxHealth(), 250000), spell.OutcomeEnemyMeleeWhite) + + if result.Landed() { + debuffAura := tankTarget.GetAuraByID(decimatingStrikeActionID) + + if debuffAura != nil { + debuffAura.Activate(sim) + } + } + + // MT should taunt as soon as the final Decimating Strike goes out in order to maximize their Blaze of Glory stack count. + if ai.tankSwap && (ai.stackCountForFirstSwap > 0) && (decimationBladeAura.ExpiresAt() < ai.Target.AutoAttacks.NextAttackAt()) { + ai.swapTargets(sim, ai.MainTank) + } + }, + }) + ai.Target.AutoAttacks.SetReplaceMHSwing(func(_ *core.Simulation, mhSwingSpell *core.Spell) *core.Spell { if infernoBladeAura.IsActive() { return infernoStrike diff --git a/ui/druid/guardian/presets.ts b/ui/druid/guardian/presets.ts index 343248b4b8..8fb0fbf43b 100644 --- a/ui/druid/guardian/presets.ts +++ b/ui/druid/guardian/presets.ts @@ -200,6 +200,6 @@ export const PRESET_BUILD_BALEROC_OT = PresetUtils.makePresetBuild("Baleroc OT", rotation: ROTATION_BALEROC_OT, encounter: PresetUtils.makePresetEncounter( "Baleroc OT", - 'http://localhost:5173/cata/druid/guardian/?i=cmxe#eJzVUr9PFEEUvtlDc4gmB5oIJJIHlV4QTyJGickuF4NHAvEixGDn3O7s3eRmZ8/ZWS5HRayMsTB00CiV0creWKuJJFQGGysKCk2sjHa+neUQMP4BvmIzO/O9H9/3vRNncwSIQ+pkhZCnhKxY5LVFtixS6s6Ta6RMtgmZzExaedKXGXywnj1eEbTNVK4nT0ZO5bLFgYq1aN0/hpmlzKtsz2B3b8bEmPPe6tq1vJ8W2cimV/ecx9lL3eY4+83pXV9L4pd9IT18ti+nh5f2QIrfteH4yLPucjbNKS45e7XPOAMp9IN9Pb3Zsod9E5/sqc2PSXy1x3dyzR8b2eWTJSqYCl0Yn4AyLPZ3FbZJ5j+MFe+dc+D3/MPKi6mjGCPHzpvTU2vkSu/VG2/vfLE7GKdMimSVPCF9QwtUNiBq0Sb4oYKbzOUB1TyUUBLUY4X6jA9unbkN5o2CrjOohlEELS4EVBloTGYeVNtgyoyDoohRCKTSoOcol+mbFysua381wFLSC1vRWD/O8yibI30wI7nmVMDthQSyzCD04ZYIVRsiTd1GVNi2cCgZyovLaOVop7WZiZtczUzzZow3sdRcmN+oib19jvOaOuCG+AY8Auq6cRALTPPGYJaqGjJYoiJm+CRE2DLZOI0OocaogiBUDGitplgU8SUm2tCqc4ENdDJCwJhOmCZJ4xPFBpQreEZsPRSeETl58bmK9L/UGIVqrCGgDQaC+yxt6HHf524s9H6NuYWOqjPSZ0qGRySFhTqy47IZG5q8JrGOB9w3vhnTkaEpHdGqQPbD6Xp8t1fJJkEvznW8uMs1fuebVDXQ46CZGM0Kz8n0IToBl7E2fnXkT3cm4nvyQp1RkQxMawy1N5b9UR+xOGZAZftQv9R1QI3oElOYiZunW4zJPdS0oAEDxfxEY2aASdOEY4fQkLNKXORTmNeMemaRsN3BJvtTJF4YcoV5tB43IfnZl1yxADfaw+sDJDtd8s5vNoJvHA==' + 'http://localhost:5173/cata/druid/guardian/?i=rcmxe#eJzVUk9oHFUY3zcz2c6+JGUztSQZsL7sQeKShG3aFA3F3QSJG0kxmBDizZeZN7uvOzuzzMxmSU6xHhqLhxIQbRG1J6EnCYIY0YMXFVpILyXtwYr00IMFQZD0In7vTSbdre3Bo++wO9/3/b4/v9/34X4dEVRCVbSB0GWENhT0pYJ2FTSdyaKXURntITSZmlSyyEiZl9NX1fS8S9dYoHdnUa5XVwqD88qy8k4XpE6nrqvdZqYvJd9Y6SdF21XUu0ri+bX4m/Jh14Ca6zYeu3AfTpsaVgb6ckdwF1ZHT4b4ONbPp7GmP9rXchkM7tHC2MnQ0ExlMmUMmv24N9eNM9tIYL66mDbF/8f31GeFPtlGceiShrGSVQxAb8ahHQgtm0u4bMzgnnFsQBS8ty4q2MwP6Mjo/QG1OU2YD6NTMqYZPefbQ1ANK6dDE4P9zZ+aiBgl81VcMMdwNncU92yjjISr+oV0UmGnzRdXOBNXuPMekhWGzBfwsbtKVqIU3TpsowtLQrCpSyqfvq8aGfOImFF/OHzo/vvdLmPOfAOfNSbjOTpYtqkF+bg/f1zvMjLXE0fc7lQ8E+Blw6Nmj7Rvb3ba39/TOuyEQ9V08LKx9B/0zZvD7ZN9va/JyTQjs5M4nqq3yBN11afX7WDRa8KpglQfTQjzgWLvK+iaGl/l26VL6osZ+Tn3sNR39Yp4j4ovxR93ioMx7EGRpHOfZcpqDC2slg6u+rnSYAz9ufhK7NktDjny3S5O3bwh3u/F8ft6469r6nrPNHVZ4FtkfIKUyfKAlt9Dqf/h27B/LLWZwxfmv5h6EiPluP/tsakr6HTfmbPfvfVLMcGUyqiAttAHyDixSL0aCVu0QRw/IK8xi9dpxH2PTLvUZvnqrEOsKrNqzB4hUZWRFT8MSYu7LllhJIJkZpOVNSLLjJOAAiYAIPUk+hzlXhyzmwH3Kv9qAKU822+FYwMwz6YKZ0pmPR5x6pI3FwVknRHfIa+7frBGwohatTC/p8BQnu+NrsMqR5LWciYucyMmmzea4Gl6EXelGTagt8NhXlmHWD7ECA8JtaxmvelCmj1G5mhQAQar1G0yCLmu35LZME3kkwqjAan7ASO0UglYGPJV5q6RVpW70CASI9QZiwRTkTQ+UaiR8jx8A7bqu7YUWUQcHoTRs9QYISvNiNRpjRGXOyxuaHPH4VbTjQ5rnFtMVJ31HBZ4/hOSksUqsONeoylp8ooHdWzCHbk3uXRgKEuHdMUF9kPxefxR3EI3Eezi+WQXSzyC34UGDWqw43pDLJrlP0czHXTq3GtGcl+J/PHNhPxAXlJl1BUD0woD7eXKHqsPWBizTr21jn7x1gloRFdZAJlweVGLMe8ANePSOiMBc4TGTAJFU8ExIXSitIUs4JNfiBi15SFBu/Ymh1OIXUhy+QVYPVyCMA4lD1gdLtoGdxvJpEu29A+pJMjo' ), });