Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions spec/System/TestDefence_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
137 changes: 86 additions & 51 deletions src/Modules/CalcDefence.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/Modules/CalcSections.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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" }, },
Expand Down
1 change: 1 addition & 0 deletions src/Modules/ConfigOptions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down