Skip to content

Commit

Permalink
Merge pull request #3927 from Nostrademous/FixStackLimitedAilmentCalcs
Browse files Browse the repository at this point in the history
Updated Bleed/Ignite Ailment calculation to use weighted average
  • Loading branch information
Wires77 authored Feb 4, 2022
2 parents 389a06a + 15697e0 commit d4c323d
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 47 deletions.
209 changes: 168 additions & 41 deletions src/Modules/CalcOffence.lua
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ local m_ceil = math.ceil
local m_min = math.min
local m_max = math.max
local m_sqrt = math.sqrt
local m_pow = math.pow
local bor = bit.bor
local band = bit.band
local bnot = bit.bnot
Expand Down Expand Up @@ -54,6 +55,9 @@ local damageStatsForTypes = setmetatable({ }, { __index = function(t, k)
return modNames
end })

local globalOutput = nil
local globalBreakdown = nil

-- Calculate min/max damage for the given damage type
local function calcDamage(activeSkill, output, cfg, breakdown, damageType, typeFlags, convDst)
local skillModList = activeSkill.skillModList
Expand Down Expand Up @@ -1407,6 +1411,41 @@ function calcs.offence(env, actor, activeSkill)
else
output[stat] = output.MainHand[stat] or output.OffHand[stat]
end
elseif mode == "CHANCE_AILMENT" then
if output.MainHand[stat] and output.OffHand[stat] then
local mainChance = output.MainHand[...] * output.MainHand.HitChance
local offChance = output.OffHand[...] * output.OffHand.HitChance
local mainPortion = mainChance / (mainChance + offChance)
local offPortion = offChance / (mainChance + offChance)
local maxInstance = m_max(output.MainHand[stat], output.OffHand[stat])
local minInstance = m_min(output.MainHand[stat], output.OffHand[stat])
local stackName = stat:gsub("DPS","") .. "Stacks"
local maxInstanceStacks = m_min(1, (globalOutput[stackName] or 1) / (globalOutput[stackName.."Max"] or 1))
output[stat] = maxInstance * maxInstanceStacks + minInstance * (1 - maxInstanceStacks)
if breakdown then
if not breakdown[stat] then breakdown[stat] = { } end
t_insert(breakdown[stat], s_format(""))
t_insert(breakdown[stat], s_format("%.2f%% of ailment stacks use maximum damage", maxInstanceStacks * 100))
t_insert(breakdown[stat], s_format("Max Damage comes from %s", output.MainHand[stat] >= output.OffHand[stat] and "Main Hand" or "Off Hand"))
t_insert(breakdown[stat], s_format("= %.1f", maxInstance * maxInstanceStacks))
if maxInstanceStacks < 1 then
t_insert(breakdown[stat], s_format("%.2f%% of ailment stacks use non-maximum damage", (1-maxInstanceStacks) * 100))
t_insert(breakdown[stat], s_format("= %.1f", minInstance * (1 - maxInstanceStacks)))
end
t_insert(breakdown[stat], "")
t_insert(breakdown[stat], "Total:")
if maxInstanceStacks < 1 then
t_insert(breakdown[stat], s_format("%.1f + %.1f", maxInstance * maxInstanceStacks, minInstance * (1 - maxInstanceStacks)))
end
t_insert(breakdown[stat], s_format("= %.1f", output[stat]))
end
else
output[stat] = output.MainHand[stat] or output.OffHand[stat]
if breakdown then
if not breakdown[stat] then breakdown[stat] = { } end
t_insert(breakdown[stat], s_format("All ailment stacks comes from %s", output.MainHand[stat] and "Main Hand" or "Off Hand"))
end
end
elseif mode == "DPS" then
output[stat] = (output.MainHand[stat] or 0) + (output.OffHand[stat] or 0)
if not skillData.doubleHitsWhenDualWielding then
Expand All @@ -1417,7 +1456,7 @@ function calcs.offence(env, actor, activeSkill)

local storedMainHandAccuracy = nil
for _, pass in ipairs(passList) do
local globalOutput, globalBreakdown = output, breakdown
globalOutput, globalBreakdown = output, breakdown
local source, output, cfg, breakdown = pass.source, pass.output, pass.cfg, pass.breakdown

-- Calculate hit chance
Expand Down Expand Up @@ -1594,7 +1633,7 @@ function calcs.offence(env, actor, activeSkill)
end

for _, pass in ipairs(passList) do
local globalOutput, globalBreakdown = output, breakdown
globalOutput, globalBreakdown = output, breakdown
local source, output, cfg, breakdown = pass.source, pass.output, pass.cfg, pass.breakdown

-- Exerted Attack members
Expand Down Expand Up @@ -2604,7 +2643,7 @@ function calcs.offence(env, actor, activeSkill)
skillFlags.igniteToChaos = skillModList:Flag(skillCfg, "IgniteToChaos")
skillFlags.impale = false
for _, pass in ipairs(passList) do
local globalOutput, globalBreakdown = output, breakdown
globalOutput, globalBreakdown = output, breakdown
local source, output, cfg, breakdown = pass.source, pass.output, pass.cfg, pass.breakdown

-- Calculate chance to inflict secondary dots/status effects
Expand Down Expand Up @@ -2836,6 +2875,29 @@ function calcs.offence(env, actor, activeSkill)
if breakdown then
breakdown.BleedPhysical = { damageTypes = { } }
end

-- For bleeds we will be using a weighted average calculation
local configStacks = enemyDB:Sum("BASE", nil, "Multiplier:BleedStacks")
local maxStacks = skillModList:Override(cfg, "BleedStacksMax") or skillModList:Sum("BASE", cfg, "BleedStacksMax")
globalOutput.BleedStacksMax = maxStacks
local durationBase = skillData.bleedDurationIsSkillDuration and skillData.duration or data.misc.BleedDurationBase
local durationMod = calcLib.mod(skillModList, dotCfg, "EnemyBleedDuration", "SkillAndDamagingAilmentDuration", skillData.bleedIsSkillEffect and "Duration" or nil) * calcLib.mod(enemyDB, nil, "SelfBleedDuration") / calcLib.mod(enemyDB, dotCfg, "BleedExpireRate")
local rateMod = calcLib.mod(skillModList, cfg, "BleedFaster") + enemyDB:Sum("INC", nil, "SelfBleedFaster") / 100
globalOutput.BleedDuration = durationBase * durationMod / rateMod * debuffDurationMult
local bleedStacks = (output.HitChance / 100) * (globalOutput.BleedDuration / output.Time) / maxStacks
bleedStacks = configStacks > 0 and m_min(bleedStacks, configStacks / maxStacks) or bleedStacks
globalOutput.BleedStackPotential = bleedStacks
if globalBreakdown then
globalBreakdown.BleedStackPotential = {
s_format(colorCodes.CUSTOM.."NOTE: Calculation uses new Weighted Avg Ailment formula"),
s_format(""),
s_format("%.2f ^8(chance to hit)", output.HitChance / 100),
s_format("* (%.2f / %.2f) ^8(BleedDuration / Attack Time)", globalOutput.BleedDuration, output.Time),
s_format("/ %d ^8(max number of stacks)", maxStacks),
s_format("= %.2f", globalOutput.BleedStackPotential),
}
end

for sub_pass = 1, 2 do
if skillModList:Flag(dotCfg, "AilmentsAreNeverFromCrit") or sub_pass == 1 then
dotCfg.skillCond["CriticalStrike"] = false
Expand All @@ -2847,16 +2909,40 @@ function calcs.offence(env, actor, activeSkill)
output.BleedPhysicalMax = max
if sub_pass == 2 then
output.CritBleedDotMulti = 1 + skillModList:Sum("BASE", dotCfg, "DotMultiplier", "PhysicalDotMultiplier") / 100
sourceCritDmg = (min + max) / 2 * output.CritBleedDotMulti
sourceCritDmg = (min + (max - min) / m_pow(2, 1 / (bleedStacks + 1))) * output.CritBleedDotMulti
else
output.BleedDotMulti = 1 + skillModList:Sum("BASE", dotCfg, "DotMultiplier", "PhysicalDotMultiplier") / 100
sourceHitDmg = (min + max) / 2 * output.BleedDotMulti
sourceHitDmg = (min + (max - min) / m_pow(2, 1 / (bleedStacks + 1))) * output.BleedDotMulti
end
end
if globalBreakdown then
globalBreakdown.BleedDPS = {
s_format("Ailment mode: %s ^8(can be changed in the Configuration tab)", igniteMode == "CRIT" and "Crits Only" or "Average Damage")
}
if sourceHitDmg == sourceCritDmg then
globalBreakdown.BleedDPS = {
s_format(colorCodes.CUSTOM.."NOTE: Calculation uses new Weighted Avg Ailment formula"),
s_format(""),
s_format("Dmg Derivation:"),
s_format("(%.2f + (%.2f - %.2f) ^8(min source physical + (max source physical - min source physical)", output.BleedPhysicalMin, output.BleedPhysicalMax, output.BleedPhysicalMin),
s_format("/ 2^(1 / (%.2f + 1))) ^8(/ 2^(1 / (stack potential + 1)))", bleedStacks),
s_format("* %.2f ^8(Bleed DoT Multi)", output.BleedDotMulti),
s_format("= %.2f", sourceHitDmg),
}
else
globalBreakdown.BleedDPS = {
s_format(colorCodes.CUSTOM.."NOTE: Calculation uses new Weighted Avg Ailment formula"),
s_format(""),
s_format("Non-Crit Dmg Derivation:"),
s_format("(%.2f + (%.2f - %.2f) ^8(min source physical + (max source physical - min source physical)", output.BleedPhysicalMin, output.BleedPhysicalMax, output.BleedPhysicalMin),
s_format("/ 2^(1 / (%.2f + 1))) ^8(/ 2^(1 / (stack potential + 1)))", bleedStacks),
s_format("* %.2f ^8(Bleed DoT Multi for Non-Crit)", output.BleedDotMulti),
s_format("= %.2f", sourceHitDmg),
s_format(""),
s_format("Crit Dmg Derivation:"),
s_format("(%.2f + (%.2f - %.2f) ^8(min source physical + (max source physical - min source physical)", output.BleedPhysicalMin, output.BleedPhysicalMax, output.BleedPhysicalMin),
s_format("/ 2^(1 / (%.2f + 1))) ^8(/ 2^(1 / (stack potential + 1)))", bleedStacks),
s_format("* %.2f ^8(Bleed DoT Multi for Crit)", output.CritBleedDotMulti),
s_format("= %.2f", sourceCritDmg),
}
end
end
local basePercent = skillData.bleedBasePercent or data.misc.BleedPercentBase
local baseVal = calcAilmentDamage("Bleed", sourceHitDmg, sourceCritDmg) * basePercent / 100 * output.RuthlessBlowEffect * output.FistOfWarAilmentEffect * globalOutput.AilmentWarcryEffect
Expand All @@ -2874,25 +2960,14 @@ function calcs.offence(env, actor, activeSkill)
globalBreakdown.BleedEffMult = breakdown.effMult("Physical", resist, 0, takenInc, effMult, takenMore)
end
end
local mult = skillModList:Sum("BASE", dotCfg, "PhysicalDotMultiplier", "BleedMultiplier")
local effectMod = calcLib.mod(skillModList, dotCfg, "AilmentEffect")
local rateMod = calcLib.mod(skillModList, cfg, "BleedFaster") + enemyDB:Sum("INC", nil, "SelfBleedFaster") / 100
local maxStacks = skillModList:Override(cfg, "BleedStacksMax") or skillModList:Sum("BASE", cfg, "BleedStacksMax")
local configStacks = enemyDB:Sum("BASE", nil, "Multiplier:BleedStacks")
local bleedStacks = configStacks > 0 and m_min(configStacks, maxStacks) or maxStacks
output.BaseBleedDPS = baseVal * effectMod * rateMod * effMult
output.BleedDPS = (baseVal * effectMod * rateMod * effMult) * bleedStacks
local durationBase
if skillData.bleedDurationIsSkillDuration then
durationBase = skillData.duration
else
durationBase = data.misc.BleedDurationBase
end
local durationMod = calcLib.mod(skillModList, dotCfg, "EnemyBleedDuration", "SkillAndDamagingAilmentDuration", skillData.bleedIsSkillEffect and "Duration" or nil) * calcLib.mod(enemyDB, nil, "SelfBleedDuration")
globalOutput.BleedDuration = durationBase * durationMod / rateMod * debuffDurationMult
globalOutput.BleedDamage = output.BaseBleedDPS * globalOutput.BleedDuration
globalOutput.BleedStacksMax = maxStacks
bleedStacks = m_min(maxStacks, (output.HitChance / 100) * globalOutput.BleedDuration / output.Time)
local chanceToHitInOneSecInterval = 1 - m_pow(1 - (output.HitChance / 100), output.Speed)
output.BleedDPS = (baseVal * effectMod * rateMod * effMult) * bleedStacks * chanceToHitInOneSecInterval
-- reset bleed stacks to actual number doing damage after weighted avg DPS calculation is done
globalOutput.BleedStacks = bleedStacks
globalOutput.BleedDamage = output.BaseBleedDPS * globalOutput.BleedDuration
if breakdown then
if output.CritBleedDotMulti and (output.CritBleedDotMulti ~= output.BleedDotMulti) then
local chanceFromHit = output.BleedChanceOnHit / 100 * (1 - globalOutput.CritChance / 100)
Expand Down Expand Up @@ -2922,6 +2997,8 @@ function calcs.offence(env, actor, activeSkill)
{ "%.2f ^8(ailment effect modifier)", effectMod },
{ "%.2f ^8(damage rate modifier)", rateMod },
{ "%.3f ^8(effective DPS modifier)", effMult },
{ "%d ^8(bleed stacks)", globalOutput.BleedStacks },
{ "%.3f ^8(bleed chance based on chance to hit each second)", chanceToHitInOneSecInterval },
total = s_format("= %.1f ^8per second", output.BleedDPS),
})
if globalOutput.BleedDuration ~= durationBase then
Expand Down Expand Up @@ -3143,6 +3220,31 @@ function calcs.offence(env, actor, activeSkill)
breakdown.IgniteFire = { damageTypes = { } }
breakdown.IgniteChaos = { damageTypes = { } }
end

-- For ignites we will be using a weighted average calculation
local maxStacks = 1
if skillFlags.igniteCanStack then
maxStacks = maxStacks + skillModList:Sum("BASE", cfg, "IgniteStacks")
end
globalOutput.IgniteStacksMax = maxStacks

local rateMod = (calcLib.mod(skillModList, cfg, "IgniteBurnFaster") + enemyDB:Sum("INC", nil, "SelfIgniteBurnFaster") / 100) / calcLib.mod(skillModList, cfg, "IgniteBurnSlower")
local durationBase = data.misc.IgniteDurationBase
local durationMod = m_max(calcLib.mod(skillModList, dotCfg, "EnemyIgniteDuration", "SkillAndDamagingAilmentDuration") * calcLib.mod(enemyDB, nil, "SelfIgniteDuration"), 0)
globalOutput.IgniteDuration = durationBase * durationMod / rateMod * debuffDurationMult
globalOutput.IgniteDuration = globalOutput.IgniteDuration > data.misc.IgniteMinDuration and globalOutput.IgniteDuration or 0
local igniteStacks = (globalOutput.IgniteDuration / output.Time) / maxStacks
globalOutput.IgniteStackPotential = igniteStacks
if globalBreakdown then
globalBreakdown.IgniteStackPotential = {
s_format(colorCodes.CUSTOM.."NOTE: Calculation uses new Weighted Avg Ailment formula"),
s_format(""),
s_format("(%.2f / %.2f) ^8(IgniteDuration / Cast Time)", globalOutput.IgniteDuration, output.Time),
s_format("/ %d ^8(max number of stacks)", maxStacks),
s_format("= %.2f", globalOutput.IgniteStackPotential),
}
end

for sub_pass = 1, 2 do
if skillModList:Flag(dotCfg, "AilmentsAreNeverFromCrit") or sub_pass == 1 then
dotCfg.skillCond["CriticalStrike"] = false
Expand Down Expand Up @@ -3187,16 +3289,42 @@ function calcs.offence(env, actor, activeSkill)
end
if sub_pass == 2 then
output.CritIgniteDotMulti = 1 + skillModList:Sum("BASE", dotCfg, "DotMultiplier", "FireDotMultiplier") / 100
sourceCritDmg = (totalMin + totalMax) / 2 * output.CritIgniteDotMulti
sourceCritDmg = (totalMin + (totalMax - totalMin) / m_pow(2, 1 / (igniteStacks + 1))) * output.CritIgniteDotMulti
else
output.IgniteDotMulti = 1 + skillModList:Sum("BASE", dotCfg, "DotMultiplier", "FireDotMultiplier") / 100
sourceHitDmg = (totalMin + totalMax) / 2 * output.IgniteDotMulti
sourceHitDmg = (totalMin + (totalMax - totalMin) / m_pow(2, 1 / (igniteStacks + 1))) * output.IgniteDotMulti
end
output.IgniteTotalMin = totalMin
output.IgniteTotalMax = totalMax
end
if globalBreakdown then
globalBreakdown.IgniteDPS = {
s_format("Ailment mode: %s ^8(can be changed in the Configuration tab)", igniteMode == "CRIT" and "Crits Only" or "Average Damage")
}
if sourceHitDmg == sourceCritDmg then
globalBreakdown.IgniteDPS = {
s_format(colorCodes.CUSTOM.."NOTE: Calculation uses new Weighted Avg Ailment formula"),
s_format(""),
s_format("Dmg Derivation:"),
s_format("(%.2f + (%.2f - %.2f) ^8(min combined sources + (max combined sources - min combined sources)", output.IgniteTotalMin, output.IgniteTotalMax, output.IgniteTotalMin),
s_format("/ 2^(1 / (%.2f + 1))) ^8(/ 2^(1 / (stack potential + 1)))", igniteStacks),
s_format("* %.2f ^8(Ignite DoT Multi)", output.IgniteDotMulti),
s_format("= %.2f", sourceHitDmg),
}
else
globalBreakdown.IgniteDPS = {
s_format(colorCodes.CUSTOM.."NOTE: Calculation uses new Weighted Avg Ailment formula"),
s_format(""),
s_format("Non-Crit Dmg Derivation:"),
s_format("(%.2f + (%.2f - %.2f) ^8(min combined sources + (max combined sources - min combined sources)", output.IgniteTotalMin, output.IgniteTotalMax, output.IgniteTotalMin),
s_format("/ 2^(1 / (%.2f + 1))) ^8(/ 2^(1 / (stack potential + 1)))", igniteStacks),
s_format("* %.2f ^8(Ignite DoT Multi for Non-Crit)", output.IgniteDotMulti),
s_format("= %.2f", sourceHitDmg),
s_format(""),
s_format("Crit Dmg Derivation:"),
s_format("(%.2f + (%.2f - %.2f) ^8(min combined sources + (max combined sources - min combined sources)", output.IgniteTotalMin, output.IgniteTotalMax, output.IgniteTotalMin),
s_format("/ 2^(1 / (%.2f + 1))) ^8(/ 2^(1 / (stack potential + 1)))", igniteStacks),
s_format("* %.2f ^8(Ignite DoT Multi for Crit)", output.CritIgniteDotMulti),
s_format("= %.2f", sourceCritDmg),
}
end
end
local baseVal = calcAilmentDamage("Ignite", sourceHitDmg, sourceCritDmg) * data.misc.IgnitePercentBase * output.FistOfWarAilmentEffect * globalOutput.AilmentWarcryEffect
if baseVal > 0 then
Expand Down Expand Up @@ -3224,26 +3352,25 @@ function calcs.offence(env, actor, activeSkill)
end
end
local effectMod = calcLib.mod(skillModList, dotCfg, "AilmentEffect")
local rateMod = (calcLib.mod(skillModList, cfg, "IgniteBurnFaster") + enemyDB:Sum("INC", nil, "SelfIgniteBurnFaster") / 100) / calcLib.mod(skillModList, cfg, "IgniteBurnSlower")
output.IgniteDPS = baseVal * effectMod * rateMod * effMult
local durationBase = data.misc.IgniteDurationBase
local durationMod = calcLib.mod(skillModList, dotCfg, "EnemyIgniteDuration", "SkillAndDamagingAilmentDuration") * calcLib.mod(enemyDB, nil, "SelfIgniteDuration")
globalOutput.IgniteDuration = durationBase * durationMod / rateMod * debuffDurationMult
igniteStacks = m_min(maxStacks, (output.HitChance / 100) * globalOutput.IgniteDuration / output.Time)
output.IgniteDPS = baseVal * effectMod * rateMod * effMult * igniteStacks
globalOutput.IgniteDamage = output.IgniteDPS * globalOutput.IgniteDuration
if skillFlags.igniteCanStack then
output.IgniteDamage = output.IgniteDPS * globalOutput.IgniteDuration
output.TotalIgniteStacks = 1 + skillModList:Sum("BASE", cfg, "IgniteStacks")
output.TotalIgniteDPS = output.IgniteDPS * output.TotalIgniteStacks
output.IgniteStacksMax = maxStacks
output.TotalIgniteDPS = output.IgniteDPS
end

if breakdown then
t_insert(breakdown.IgniteDPS, "x 1.25 ^8(ignite deals 125% per second)")
t_insert(breakdown.IgniteDPS, "x 0.9 ^8(ignite deals 90% per second)")
t_insert(breakdown.IgniteDPS, s_format("= %.1f", baseVal, 1))
breakdown.multiChain(breakdown.IgniteDPS, {
label = "Ignite DPS:",
base = s_format("%.1f ^8(total damage per second)", baseVal),
{ "%.2f ^8(ailment effect modifier)", effectMod },
{ "%.2f ^8(burn rate modifier)", rateMod },
{ "%.3f ^8(effective DPS modifier)", effMult },
{ "%d ^8(ignite stacks)", output.IgniteStacksMax },
total = s_format("= %.1f ^8per second", output.IgniteDPS),
})
if output.CritIgniteDotMulti and (output.CritIgniteDotMulti ~= output.IgniteDotMulti) then
Expand Down Expand Up @@ -3526,7 +3653,7 @@ function calcs.offence(env, actor, activeSkill)
-- Combine secondary effect stats
if isAttack then
combineStat("BleedChance", "AVERAGE")
combineStat("BleedDPS", "CHANCE", "BleedChance")
combineStat("BleedDPS", "CHANCE_AILMENT", "BleedChance")
combineStat("PoisonChance", "AVERAGE")
combineStat("PoisonDPS", "CHANCE", "PoisonChance")
combineStat("TotalPoisonDPS", "DPS")
Expand All @@ -3537,15 +3664,15 @@ function calcs.offence(env, actor, activeSkill)
combineStat("TotalPoisonStacks", "DPS")
end
combineStat("IgniteChance", "AVERAGE")
combineStat("IgniteDPS", "CHANCE", "IgniteChance")
combineStat("IgniteDPS", "CHANCE_AILMENT", "IgniteChance")
if skillFlags.igniteCanStack then
combineStat("IgniteDamage", "CHANCE", "IgniteChance")
if skillData.showAverage then
combineStat("TotalIgniteAverageDamage", "DPS")
combineStat("TotalIgniteStacks", "DPS")
combineStat("IgniteStacksMax", "DPS")
combineStat("TotalIgniteDPS", "DPS")
else
combineStat("TotalIgniteStacks", "DPS")
combineStat("IgniteStacksMax", "DPS")
combineStat("TotalIgniteDPS", "DPS")
end
end
Expand Down
Loading

0 comments on commit d4c323d

Please sign in to comment.