Skip to content

Commit

Permalink
Hunting more corner cases
Browse files Browse the repository at this point in the history
MoM with ES when there is ES bypass was not capping effective ES.
MoM with life loss prevention was not accounting for the multiplied life effectiveness when reducing pools.
Max hit smoothing (for conversion + armour) now uses pool reduction to effectively home in on a precise max hit.
  • Loading branch information
Edvinas-Smita committed Jan 26, 2025
1 parent ee62c5d commit e850827
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 44 deletions.
90 changes: 46 additions & 44 deletions src/Modules/CalcDefence.lua
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,12 @@ function calcs.takenHitFromDamage(rawDamage, damageType, actor)
return receivedDamageSum, damages
end

local function calcLifeHitPoolWithLossPrevention(life, maxLife, lifeLossPrevented, lifeLossBelowHalfPrevented)
local halfLife = maxLife * 0.5
local aboveLow = m_max(life - halfLife, 0)
return aboveLow / (1 - lifeLossPrevented / 100) + m_min(life, halfLife) / (1 - lifeLossBelowHalfPrevented / 100) / (1 - lifeLossPrevented / 100)
end

---Helper function that reduces pools according to damage taken
---@param poolTable table special pool values to use. Can be nil. Values from actor output are used if this is not provided or a value for some key in this is nil.
---@param damageTable table damage table after all the relevant reductions
Expand Down Expand Up @@ -400,6 +406,7 @@ function calcs.reducePoolsByDamage(poolTable, damageTable, actor)
local energyShield = poolTbl.EnergyShield or output.EnergyShieldRecoveryCap
local mana = poolTbl.Mana or output.ManaUnreserved or 0
local life = poolTbl.Life or output.LifeRecoverable or 0
local lifeLossBelowHalfPrevented = modDB:Sum("BASE", nil, "LifeLossBelowHalfPrevented")
local LifeLossLostOverTime = poolTbl.LifeLossLostOverTime or 0
local LifeBelowHalfLossLostOverTime = poolTbl.LifeBelowHalfLossLostOverTime or 0
local overkillDamage = 0
Expand Down Expand Up @@ -458,27 +465,28 @@ function calcs.reducePoolsByDamage(poolTable, damageTable, actor)
local damageRemainder = damageRemaindersBeforeES[damageType]
if damageRemainder then
if life > 0 then
local esBypass = output[damageType.."EnergyShieldBypass"] or 0
if energyShield > 0 and (not modDB:Flag(nil, "EnergyShieldProtectsMana")) and (esBypass) < 100 then
local esDamageTypeMultiplier = damageType == "Chaos" and 2 or 1
local tempDamage = m_min(damageRemainder * (1 - esBypass / 100), energyShield / esDamageTypeMultiplier)
local esDamageTypeMultiplier = damageType == "Chaos" and 2 or 1
local esBypass = output[damageType.."EnergyShieldBypass"] / 100 or 0
if energyShield > 0 and (not modDB:Flag(nil, "EnergyShieldProtectsMana")) and (esBypass) < 1 then
local tempDamage = m_min(damageRemainder * (1 - esBypass), energyShield / esDamageTypeMultiplier)
energyShield = energyShield - tempDamage * esDamageTypeMultiplier
damageRemainder = damageRemainder - tempDamage
end
if (output.sharedMindOverMatter + output[damageType.."MindOverMatter"]) > 0 then
local lifeHitPool = calcLifeHitPoolWithLossPrevention(life, output.Life, output.preventedLifeLoss, lifeLossBelowHalfPrevented)
local MoMEffect = m_min(output.sharedMindOverMatter + output[damageType.."MindOverMatter"], 100) / 100
local MoMPool = MoMEffect < 1 and m_min(lifeHitPool / (1 - MoMEffect) - lifeHitPool, mana) or mana
local MoMDamage = damageRemainder * MoMEffect
if modDB:Flag(nil, "EnergyShieldProtectsMana") and energyShield > 0 and esBypass < 100 then
local esDamageTypeMultiplier = damageType == "Chaos" and 2 or 1
local tempDamage = m_min(MoMDamage * (1 - esBypass / 100), energyShield / esDamageTypeMultiplier)
if modDB:Flag(nil, "EnergyShieldProtectsMana") and energyShield > 0 and esBypass < 1 then
local MoMEBPool = esBypass > 0 and m_min(MoMPool / esBypass - MoMPool, energyShield) or energyShield
local tempDamage = m_min(MoMDamage * (1 - esBypass), MoMEBPool)
energyShield = energyShield - tempDamage * esDamageTypeMultiplier
MoMDamage = MoMDamage - tempDamage
local tempDamage2 = m_min(MoMDamage, mana)
local tempDamage2 = m_min(MoMDamage, MoMPool)
mana = mana - tempDamage2
damageRemainder = damageRemainder - tempDamage - tempDamage2
elseif mana > 0 then
local manaPool = MoMEffect < 1 and m_min(life / (1 - MoMEffect) - life, mana) or mana
local tempDamage = m_min(MoMDamage, manaPool)
local tempDamage = m_min(MoMDamage, MoMPool)
mana = mana - tempDamage
damageRemainder = damageRemainder - tempDamage
end
Expand All @@ -488,7 +496,7 @@ function calcs.reducePoolsByDamage(poolTable, damageTable, actor)
local lifeOverHalfLife = m_max(life - halfLife, 0)
local preventPercent = output.preventedLifeLoss / 100
local poolAboveLow = lifeOverHalfLife / (1 - preventPercent)
local preventBelowHalfPercent = modDB:Sum("BASE", nil, "LifeLossBelowHalfPrevented") / 100
local preventBelowHalfPercent = lifeLossBelowHalfPrevented / 100
local damageThatLifeCanStillTake = poolAboveLow + m_max(m_min(life, halfLife), 0) / (1 - preventBelowHalfPercent) / (1 - output.preventedLifeLoss / 100)
if damageThatLifeCanStillTake < damageRemainder then
overkillDamage = overkillDamage + damageRemainder - damageThatLifeCanStillTake
Expand Down Expand Up @@ -2280,22 +2288,20 @@ function calcs.buildDefenceEstimations(env, actor)

-- Prevented life loss taken over 4 seconds (and Petrified Blood)
do
local halfLife = output.Life * 0.5
local recoverable = output.LifeRecoverable
local aboveLow = m_max(recoverable - halfLife, 0)
local preventedLifeLoss = m_min(modDB:Sum("BASE", nil, "LifeLossPrevented"), 100)
output["preventedLifeLoss"] = preventedLifeLoss
local initialLifeLossBelowHalfPrevented = modDB:Sum("BASE", nil, "LifeLossBelowHalfPrevented")
output["preventedLifeLossBelowHalf"] = (1 - output["preventedLifeLoss"] / 100) * initialLifeLossBelowHalfPrevented
local portionLife = 1
if not env.configInput["conditionLowLife"] then
--portion of life that is lowlife
portionLife = m_min(halfLife / recoverable, 1)
portionLife = m_min(output.Life * 0.5 / recoverable, 1)
output["preventedLifeLossTotal"] = output["preventedLifeLoss"] + output["preventedLifeLossBelowHalf"] * portionLife
else
output["preventedLifeLossTotal"] = output["preventedLifeLoss"] + output["preventedLifeLossBelowHalf"]
end
output.LifeHitPool = aboveLow / (1 - preventedLifeLoss / 100) + m_min(recoverable, halfLife) / (1 - initialLifeLossBelowHalfPrevented / 100) / (1 - preventedLifeLoss / 100)
output.LifeHitPool = calcLifeHitPoolWithLossPrevention(recoverable, output.Life, preventedLifeLoss, initialLifeLossBelowHalfPrevented)

if breakdown then
breakdown["preventedLifeLossTotal"] = {
Expand Down Expand Up @@ -2355,12 +2361,13 @@ function calcs.buildDefenceEstimations(env, actor)
local sourcePool = m_max(output.ManaUnreserved or 0, 0)
local sourceHitPool = sourcePool
local manatext = "unreserved mana"
if modDB:Flag(nil, "EnergyShieldProtectsMana") and output.MinimumBypass < 100 then
local esBypass = output.MinimumBypass / 100
if modDB:Flag(nil, "EnergyShieldProtectsMana") and esBypass < 1 then
manatext = manatext.." + non-bypassed energy shield"
if output.MinimumBypass > 0 then
local manaProtected = output.EnergyShieldRecoveryCap / (1 - output.MinimumBypass / 100) * (output.MinimumBypass / 100)
sourcePool = m_max(sourcePool - manaProtected, -output.LifeRecoverable) + m_min(sourcePool + output.LifeRecoverable, manaProtected) / (output.MinimumBypass / 100)
sourceHitPool = m_max(sourceHitPool - manaProtected, -output.LifeHitPool) + m_min(sourceHitPool + output.LifeHitPool, manaProtected) / (output.MinimumBypass / 100)
if esBypass > 0 then
local manaProtected = m_min(sourcePool / esBypass - sourcePool, output.EnergyShieldRecoveryCap)
sourcePool = m_max(sourcePool - manaProtected, -output.LifeRecoverable) + m_min(sourcePool + output.LifeRecoverable, manaProtected) / esBypass
sourceHitPool = m_max(sourceHitPool - manaProtected, -output.LifeHitPool) + m_min(sourceHitPool + output.LifeHitPool, manaProtected) / esBypass
else
sourcePool = sourcePool + output.EnergyShieldRecoveryCap
sourceHitPool = sourcePool
Expand Down Expand Up @@ -2686,8 +2693,9 @@ function calcs.buildDefenceEstimations(env, actor)
local VaalArcticArmourMultiplier = VaalArcticArmourHitsLeft > 0 and (( 1 - output["VaalArcticArmourMitigation"] * m_min(VaalArcticArmourHitsLeft / iterationMultiplier, 1))) or 1
VaalArcticArmourHitsLeft = VaalArcticArmourHitsLeft - iterationMultiplier
for _, damageType in ipairs(dmgTypeList) do
Damage[damageType] = DamageIn[damageType] * iterationMultiplier * VaalArcticArmourMultiplier
damageTotal = damageTotal + Damage[damageType]
local damage = DamageIn[damageType] or 0
Damage[damageType] = damage > 0 and damage * iterationMultiplier * VaalArcticArmourMultiplier or nil
damageTotal = damageTotal + damage
end
if DamageIn.GainWhenHit and (iterationMultiplier > 1 or DamageIn["cycles"] > 1) then
local gainMult = iterationMultiplier * DamageIn["cycles"]
Expand Down Expand Up @@ -3195,7 +3203,6 @@ function calcs.buildDefenceEstimations(env, actor)

for _, damageType in ipairs(dmgTypeList) do
local partMin = m_huge
local poolMax = 0
local useConversionSmoothing = false
for _, damageConvertedType in ipairs(dmgTypeList) do
local convertPercent = actor.damageShiftTable[damageType][damageConvertedType]
Expand Down Expand Up @@ -3250,7 +3257,6 @@ function calcs.buildDefenceEstimations(env, actor)
hitTaken = m_floor(m_max(m_min(RAW, maxDRMaxHit), noDRMaxHit))
useConversionSmoothing = useConversionSmoothing or convertPercent ~= 100
end
poolMax = m_max(poolMax, totalHitPool)
partMin = m_min(partMin, hitTaken)
end
end
Expand All @@ -3261,26 +3267,22 @@ function calcs.buildDefenceEstimations(env, actor)
if partMin == m_huge then
finalMaxHit = m_huge
elseif useConversionSmoothing then
-- this just reduces deviation from what the result should be
local noSmoothing = partMin
-- this sqrt pass could be repeated multiple times and each time it would produce a more accurate result.
local noSmoothingFullTaken, noSmoothingDamages = calcs.takenHitFromDamage(noSmoothing, damageType, actor)
local firstPassRatio = noSmoothingFullTaken / poolMax
for partType, partTaken in pairs(noSmoothingDamages) do
firstPassRatio = m_max(firstPassRatio, partTaken / output[partType.."TotalHitPool"])
end
local onePass = noSmoothing / m_sqrt(firstPassRatio)
-- this finishing pass is special because it:
-- 1) inverts the behaviour of misreporting - instead of over reporting it under reports, so players don't try to tank something they can't
-- 2) near the worst case scenarios of previous smoothing ratios this *magically* makes calculations near exact. In average case scenarios it still helps.
local onePassFullTaken, onePassDamages = calcs.takenHitFromDamage(onePass, damageType, actor)
local finalPassRatio = onePassFullTaken / poolMax
for partType, partTaken in pairs(onePassDamages) do
finalPassRatio = m_max(finalPassRatio, partTaken / output[partType.."TotalHitPool"])
end
local finalPass = onePass / finalPassRatio
local passIncomingDamage = partMin
for n = 1, data.misc.maxHitSmoothingPasses do
local _, passDamages = calcs.takenHitFromDamage(passIncomingDamage, damageType, actor)
local passPools = calcs.reducePoolsByDamage(nil, passDamages, actor)
local passOverkill = passPools.OverkillDamage - passPools.Life
local passRatio = 0
for partType, _ in pairs(passDamages) do
passRatio = m_max(passRatio, (passOverkill + output[partType.."TotalHitPool"]) / output[partType.."TotalHitPool"])
end
passIncomingDamage = (passIncomingDamage - passOverkill) / m_sqrt(passRatio)
if passOverkill < 1 and passOverkill > -1 then
break
end
end

finalMaxHit = round(finalPass / enemyDamageMult)
finalMaxHit = round(passIncomingDamage / enemyDamageMult)
else
finalMaxHit = round(partMin / enemyDamageMult)
end
Expand Down Expand Up @@ -3385,7 +3387,7 @@ function calcs.buildDefenceEstimations(env, actor)
t_insert(breakdown[maxHitCurType], s_format("\t%d "..colorCodes.LIFE.."Life ^7Loss Prevented", poolsRemaining.LifeLossLostOverTime + poolsRemaining.LifeBelowHalfLossLostOverTime))
end
t_insert(breakdown[maxHitCurType], s_format("\t%d "..colorCodes.LIFE.."Life ^7(%d remaining)", output.LifeRecoverable - poolsRemaining.Life, poolsRemaining.Life))
if poolsRemaining.OverkillDamage > 0 then
if poolsRemaining.OverkillDamage >= 1 then
t_insert(breakdown[maxHitCurType], s_format("\t%d Overkill damage", poolsRemaining.OverkillDamage))
end
end
Expand Down
2 changes: 2 additions & 0 deletions src/Modules/Data.lua
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ data.misc = { -- magic numbers
ehpCalcMaxDamage = 100000000,
-- max iterations can be increased for more accuracy this should be perfectly accurate unless it runs out of iterations and so high eHP values will be underestimated.
ehpCalcMaxIterationsToCalc = 50,
-- more iterations would reduce the cases where max hit would result in overkill damage or leave some life.
maxHitSmoothingPasses = 8,
-- maximum increase for stat weights, only used in trader for now.
maxStatIncrease = 2, -- 100% increased
-- PvP scaling used for hogm
Expand Down

0 comments on commit e850827

Please sign in to comment.