diff --git a/spec/System/TestDefence_spec.lua b/spec/System/TestDefence_spec.lua index 4fb37d87f8..c12ffdb46a 100644 --- a/spec/System/TestDefence_spec.lua +++ b/spec/System/TestDefence_spec.lua @@ -1816,4 +1816,97 @@ describe("TestDefence", function() assert.is_true(higherResistEHP >= lowerResistEHP, string.format("EHP fell from %.1f to %.1f", lowerResistEHP, higherResistEHP)) end) + + describe("energy shield depleted by self degen", function() + local function calc(customMods, config) + newBuild() + build.skillsTab:PasteSocketGroup("Righteous Fire 20/0 1") + build.configTab.input.enemyIsBoss = "None" + for k, v in pairs(config or { }) do + build.configTab.input[k] = v + end + build.configTab.input.customMods = customMods + build.configTab:BuildModList() + runCallback("OnFrame") + return build.calcsTab.calcsOutput + end + local esMods = "+1000 to maximum Energy Shield\n" + + it("does not change energy shield without a self degen", function() + newBuild() + build.configTab.input.enemyIsBoss = "None" + build.configTab.input.customMods = esMods + build.configTab:BuildModList() + runCallback("OnFrame") + local output = build.calcsTab.calcsOutput + assert.is_false(output.EnergyShieldDepletedByDegen) + assert.are.equals(output.EnergyShield, output.EnergyShieldRecoveryCap) + end) + + it("excludes energy shield from the hit pools when Righteous Fire drains it", function() + local output = calc(esMods) + assert.is_true(output.FireBuildDegen > 0) + assert.is_true(output.EnergyShield > 0) + assert.is_true(output.EnergyShieldDepletedByDegen) + assert.are.equals(0, output.EnergyShieldRecoveryCap) + -- hit pools now match a build with no energy shield at all + local depletedPool, depletedEHP = output.FireTotalPool, output.TotalEHP + local noES = calc(esMods, { DisableEHPDegenEnergyShieldDepletion = true }) + assert.is_false(noES.EnergyShieldDepletedByDegen) + assert.are.equals(noES.EnergyShield, noES.EnergyShieldRecoveryCap) + assert.are.equals(depletedPool + noES.EnergyShield, noES.FireTotalPool) + assert.is_true(depletedEHP < noES.TotalEHP) + end) + + it("keeps energy shield when its regeneration sustains the degen", function() + local degen = calc(esMods).FireBuildDegen + local output = calc(esMods .. "Regenerate " .. math.ceil(degen) + 1 .. " Energy Shield per second") + assert.is_false(output.EnergyShieldDepletedByDegen) + assert.are.equals(output.EnergyShield, output.EnergyShieldRecoveryCap) + -- partial sustain still drains energy shield + output = calc(esMods .. "Regenerate " .. math.floor(degen / 2) .. " Energy Shield per second") + assert.is_true(output.EnergyShieldDepletedByDegen) + assert.are.equals(0, output.EnergyShieldRecoveryCap) + end) + + it("ignores the part of the degen that bypasses energy shield", function() + local output = calc(esMods .. "All Damage taken bypasses Energy Shield") + assert.is_false(output.EnergyShieldDepletedByDegen) + -- Corrupted Soul style partial bypass: energy shield only takes half of the degen + local degen = calc(esMods).FireBuildDegen + output = calc(esMods .. "50% of Non-Chaos Damage taken bypasses Energy Shield\nRegenerate " .. math.ceil(degen * 0.75) .. " Energy Shield per second") + assert.is_false(output.EnergyShieldDepletedByDegen) + output = calc(esMods .. "50% of Non-Chaos Damage taken bypasses Energy Shield\nRegenerate " .. math.floor(degen * 0.25) .. " Energy Shield per second") + assert.is_true(output.EnergyShieldDepletedByDegen) + end) + + it("uses life regeneration converted by Zealot's Oath as energy shield sustain", function() + local degen = calc(esMods .. "Zealot's Oath").FireBuildDegen + local output = calc(esMods .. "Zealot's Oath\nRegenerate " .. math.ceil(degen) + 1 .. " Life per second") + assert.is_true(output.EnergyShieldRegenRecovery > degen) + assert.is_false(output.EnergyShieldDepletedByDegen) + assert.are.equals(output.EnergyShield, output.EnergyShieldRecoveryCap) + output = calc(esMods .. "Zealot's Oath\nRegenerate " .. math.floor(degen / 2) .. " Life per second") + assert.is_true(output.EnergyShieldDepletedByDegen) + assert.are.equals(0, output.EnergyShieldRecoveryCap) + end) + + it("does not change energy shield with Chaos Inoculation", function() + local output = calc(esMods .. "Chaos Inoculation") + assert.are.equals(1, output.Life) + assert.is_nil(output.TotalBuildDegen) + assert.is_false(output.EnergyShieldDepletedByDegen) + assert.are.equals(output.EnergyShield, output.EnergyShieldRecoveryCap) + end) + + it("only drains energy shield through mana with Eldritch Battery", function() + local output = calc(esMods .. "Energy Shield protects Mana instead of Life") + assert.is_false(output.EnergyShieldDepletedByDegen) + assert.are.equals(output.EnergyShield, output.EnergyShieldRecoveryCap) + output = calc(esMods .. "Energy Shield protects Mana instead of Life\n40% of Damage is taken from Mana before Life") + assert.are.equals(40, output.sharedMindOverMatter) + assert.is_true(output.EnergyShieldDepletedByDegen) + assert.are.equals(0, output.EnergyShieldRecoveryCap) + end) + end) end) diff --git a/src/Modules/CalcDefence.lua b/src/Modules/CalcDefence.lua index 0157e85881..32327f21f0 100644 --- a/src/Modules/CalcDefence.lua +++ b/src/Modules/CalcDefence.lua @@ -2321,6 +2321,59 @@ function calcs.buildDefenceEstimations(env, actor) end end + -- Self-inflicted degens (eg. Righteous Fire). Calculated before the pools so energy shield depletion can be determined + output.TotalBuildDegen = 0 + for _, damageType in ipairs(dmgTypeList) do + local baseVal = modDB:Sum("BASE", nil, damageType.."Degen") + if baseVal > 0 then + for damageConvertedType, convertPercent in pairs(actor.damageOverTimeShiftTable[damageType]) do + if convertPercent > 0 then + local total = baseVal * (convertPercent / 100) * output[damageConvertedType.."TakenDotMult"] + output[damageConvertedType.."BuildDegen"] = (output[damageConvertedType.."BuildDegen"] or 0) + total + output.TotalBuildDegen = output.TotalBuildDegen + total + if breakdown then + breakdown.TotalBuildDegen = breakdown.TotalBuildDegen or { + rowList = { }, + colList = { + { label = "Base Type", key = "type" }, + { label = "Final Type", key = "type2" }, + { label = "Base", key = "base" }, + { label = "Taken As Percent", key = "conv" }, + { label = "Multiplier", key = "mult" }, + { label = "Total", key = "total" }, + } + } + t_insert(breakdown.TotalBuildDegen.rowList, { + type = damageType, + type2 = damageConvertedType, + base = s_format("%.1f", baseVal), + conv = s_format("x %.2f%%", convertPercent), + mult = s_format("x %.2f", output[damageConvertedType.."TakenDotMult"]), + total = s_format("%.1f", total), + }) + breakdown[damageConvertedType.."BuildDegen"] = breakdown[damageConvertedType.."BuildDegen"] or { + rowList = { }, + colList = { + { label = "Base Type", key = "type" }, + { label = "Base", key = "base" }, + { label = "Taken As Percent", key = "conv" }, + { label = "Multiplier", key = "mult" }, + { label = "Total", key = "total" }, + } + } + t_insert(breakdown[damageConvertedType.."BuildDegen"].rowList, { + type = damageType, + base = s_format("%.1f", baseVal), + conv = s_format("x %.2f%%", convertPercent), + mult = s_format("x %.2f", output[damageConvertedType.."TakenDotMult"]), + total = s_format("%.1f", total), + }) + end + end + end + end + end + -- Energy Shield bypass output.AnyBypass = false output.MinimumBypass = 100 @@ -2350,6 +2403,39 @@ function calcs.buildDefenceEstimations(env, actor) output.OnlySharedMindOverMatter = false output.AnySpecificMindOverMatter = false output["sharedMindOverMatter"] = m_min(modDB:Sum("BASE", nil, "DamageTakenFromManaBeforeLife"), 100) + + -- Energy Shield depleted by self-inflicted degens + -- Degens (eg. Righteous Fire) are taken from energy shield before life. If energy shield regeneration can't keep up, + -- energy shield is drained to 0 and stays there while the degen is active, so it can't absorb hits + output.EnergyShieldDepletedByDegen = false + if output.TotalBuildDegen > 0 and output.EnergyShieldRecoveryCap > 0 and not env.configInput.DisableEHPDegenEnergyShieldDepletion then + local energyShieldDegen = 0 + for _, damageType in ipairs(dmgTypeList) do + if output[damageType.."BuildDegen"] then + local energyShieldPortion = 1 - output[damageType.."EnergyShieldBypass"] / 100 + if modDB:Flag(nil, "EnergyShieldProtectsMana") then + -- Eldritch Battery: energy shield only takes the part of the degen taken from mana + local takenFromMana = m_min(modDB:Sum("BASE", nil, damageType.."DamageTakenFromManaBeforeLife"), 100 - output["sharedMindOverMatter"]) + output["sharedMindOverMatter"] + energyShieldPortion = energyShieldPortion * takenFromMana / 100 + end + energyShieldDegen = energyShieldDegen + output[damageType.."BuildDegen"] * energyShieldPortion + end + end + if energyShieldDegen > output.EnergyShieldRegenRecovery then + output.EnergyShieldDepletedByDegen = true + if breakdown then + breakdown.EnergyShieldRecoveryCap = { + s_format("%.1f ^8(energy shield degen)", energyShieldDegen), + s_format("> %.1f ^8(energy shield regen)", output.EnergyShieldRegenRecovery), + "Energy shield is depleted by degen, so it is not counted towards the hit pools", + "^8(this can be disabled in the Configuration tab)", + } + end + output.CappingES = true + output.EnergyShieldRecoveryCap = 0 + end + end + local function calcMoMEBPool(lifePool, MoMEffect, esBypass) local mana = m_max(output.ManaUnreserved or 0, 0) local maxMoMPool = MoMEffect < 1 and lifePool / (1 - MoMEffect) - lifePool or m_huge @@ -3384,57 +3470,6 @@ function calcs.buildDefenceEstimations(env, actor) -- degens do - output.TotalBuildDegen = 0 - for _, damageType in ipairs(dmgTypeList) do - local baseVal = modDB:Sum("BASE", nil, damageType.."Degen") - if baseVal > 0 then - for damageConvertedType, convertPercent in pairs(actor.damageOverTimeShiftTable[damageType]) do - if convertPercent > 0 then - local total = baseVal * (convertPercent / 100) * output[damageConvertedType.."TakenDotMult"] - output[damageConvertedType.."BuildDegen"] = (output[damageConvertedType.."BuildDegen"] or 0) + total - output.TotalBuildDegen = output.TotalBuildDegen + total - if breakdown then - breakdown.TotalBuildDegen = breakdown.TotalBuildDegen or { - rowList = { }, - colList = { - { label = "Base Type", key = "type" }, - { label = "Final Type", key = "type2" }, - { label = "Base", key = "base" }, - { label = "Taken As Percent", key = "conv" }, - { label = "Multiplier", key = "mult" }, - { label = "Total", key = "total" }, - } - } - t_insert(breakdown.TotalBuildDegen.rowList, { - type = damageType, - type2 = damageConvertedType, - base = s_format("%.1f", baseVal), - conv = s_format("x %.2f%%", convertPercent), - mult = s_format("x %.2f", output[damageConvertedType.."TakenDotMult"]), - total = s_format("%.1f", total), - }) - breakdown[damageConvertedType.."BuildDegen"] = breakdown[damageConvertedType.."BuildDegen"] or { - rowList = { }, - colList = { - { label = "Base Type", key = "type" }, - { label = "Base", key = "base" }, - { label = "Taken As Percent", key = "conv" }, - { label = "Multiplier", key = "mult" }, - { label = "Total", key = "total" }, - } - } - t_insert(breakdown[damageConvertedType.."BuildDegen"].rowList, { - type = damageType, - base = s_format("%.1f", baseVal), - conv = s_format("x %.2f%%", convertPercent), - mult = s_format("x %.2f", output[damageConvertedType.."TakenDotMult"]), - total = s_format("%.1f", total), - }) - end - end - end - end - end if output.TotalBuildDegen == 0 then output.TotalBuildDegen = nil else diff --git a/src/Modules/CalcSections.lua b/src/Modules/CalcSections.lua index 0f5b1c7f1c..8f3a0ea553 100644 --- a/src/Modules/CalcSections.lua +++ b/src/Modules/CalcSections.lua @@ -1582,7 +1582,7 @@ return { { label = "Total Increased", { format = "{0:mod:1}%", { modName = { "EnergyShield", "Defences" }, modType = "INC" }, }, }, { label = "Total More", { format = "{0:mod:1}%", { modName = { "EnergyShield", "Defences" }, modType = "MORE" }, }, }, { label = "Total", { format = "{0:output:EnergyShield}", { breakdown = "EnergyShield" }, }, }, - { label = "Total Recoverable", haveOutput = "CappingES", { format = "{0:output:EnergyShieldRecoveryCap}", { breakdown = "EnergyShield" }, }, }, + { label = "Total Recoverable", haveOutput = "CappingES", { format = "{0:output:EnergyShieldRecoveryCap}", { breakdown = "EnergyShieldRecoveryCap" }, { breakdown = "EnergyShield" }, }, }, { label = "Recharge Rate", haveOutput = "EnergyShieldRechargeAppliesToEnergyShield", { format = "{1:output:EnergyShieldRecharge}", { breakdown = "EnergyShieldRecharge" }, { modName = { "EnergyShieldRecharge", "EnergyShieldRecoveryRate", "NoEnergyShieldRecharge", "CannotGainEnergyShield" }, }, diff --git a/src/Modules/ConfigOptions.lua b/src/Modules/ConfigOptions.lua index 8ff3e6f8c7..449dd8e3ef 100644 --- a/src/Modules/ConfigOptions.lua +++ b/src/Modules/ConfigOptions.lua @@ -224,6 +224,7 @@ return { end end }, { var = "EHPUnluckyWorstOf", type = "list", label = "EHP calc unlucky:", tooltip = "Sets the EHP calc to pretend its unlucky and reduce the effects of random events such as\n\tBlock/Spell Block Chance\n\tDodge/Spell Dodge Chance\n\tSpell Suppression Chance\n\tAvoidance Chance", list = {{val=1,label="Average"},{val=2,label="Unlucky"},{val=4,label="Very Unlucky"}} }, + { var = "DisableEHPDegenEnergyShieldDepletion", type = "check", label = "Disable EHP ^x88FFFFES ^7depletion by self degen:", ifMod = {"PhysicalDegen", "LightningDegen", "ColdDegen", "FireDegen", "ChaosDegen"}, tooltip = "When a self-inflicted degen (eg. Righteous Fire) takes more ^x88FFFFEnergy Shield ^7per second than you regenerate,\n^x88FFFFEnergy Shield ^7is treated as depleted and not counted in EHP / Maximum Hit Taken.\nEnable this if something not included in ^x88FFFFEnergy Shield ^7regeneration (eg. leech) keeps it up."}, { var = "DisableEHPGainOnBlock", type = "check", label = "Disable EHP gain when hit:", ifMod = {"LifeOnBlock", "ManaOnBlock", "EnergyShieldOnBlock", "EnergyShieldOnSpellBlock", "LifeOnSuppress", "EnergyShieldOnSuppress", "MissingLifeBeforeEnemyHit", "MissingManaBeforeEnemyHit"}, tooltip = "Sets the EHP calc to not apply gain on block, suppress or Defiance of Destiny effects"}, { var = "armourCalculationMode", type = "list", label = "Armour calculation mode:", ifCond = { "ArmourMax", "ArmourAvg" }, tooltip = "Controls how Defending with Double Armour is calculated:\n\tMinimum: never Defend with Double Armour\n\tAverage: Damage Reduction from Defending with Double Armour is proportional to chance\n\tMaximum: always Defend with Double Armour\nThis setting has no effect if you have 100% chance to Defend with Double Armour.", list = {{val="MIN",label="Minimum"},{val="AVERAGE",label="Average"},{val="MAX",label="Maximum"}}, apply = function(val, modList, enemyModList) if val == "MAX" then