Files
PathOfBuilding/Modules/CalcOffence-3_0.lua
Openarl d3de752a43 Release 1.4.99
- Skill changes for 3.4
2018-09-01 00:25:41 +10:00

2038 lines
89 KiB
Lua

-- Path of Building
--
-- Module: Calc Offence
-- Performs offence calculations.
--
local calcs = ...
local pairs = pairs
local ipairs = ipairs
local unpack = unpack
local t_insert = table.insert
local m_floor = math.floor
local m_modf = math.modf
local m_min = math.min
local m_max = math.max
local m_sqrt = math.sqrt
local m_pi = math.pi
local bor = bit.bor
local band = bit.band
local bnot = bit.bnot
local s_format = string.format
local tempTable1 = { }
local tempTable2 = { }
local tempTable3 = { }
local isElemental = { Fire = true, Cold = true, Lightning = true }
-- List of all damage types, ordered according to the conversion sequence
local dmgTypeList = {"Physical", "Lightning", "Cold", "Fire", "Chaos"}
local dmgTypeFlags = {
Physical = 0x01,
Lightning = 0x02,
Cold = 0x04,
Fire = 0x08,
Elemental = 0x0E,
Chaos = 0x10,
}
-- Magic table for caching the modifier name sets used in calcDamage()
local damageStatsForTypes = setmetatable({ }, { __index = function(t, k)
local modNames = { "Damage" }
for type, flag in pairs(dmgTypeFlags) do
if band(k, flag) ~= 0 then
t_insert(modNames, type.."Damage")
end
end
t[k] = modNames
return modNames
end })
-- Calculate min/max damage for the given damage type
local function calcDamage(actor, output, cfg, breakdown, damageType, typeFlags, convDst)
local modDB = actor.modDB
typeFlags = bor(typeFlags, dmgTypeFlags[damageType])
-- Calculate conversions
local addMin, addMax = 0, 0
local conversionTable = actor.conversionTable
for _, otherType in ipairs(dmgTypeList) do
if otherType == damageType then
-- Damage can only be converted from damage types that preceed this one in the conversion sequence, so stop here
break
end
local convMult = conversionTable[otherType][damageType]
if convMult > 0 then
-- Damage is being converted/gained from the other damage type
local min, max = calcDamage(actor, output, cfg, breakdown, otherType, typeFlags, damageType)
addMin = addMin + min * convMult
addMax = addMax + max * convMult
end
end
if addMin ~= 0 and addMax ~= 0 then
addMin = round(addMin)
addMax = round(addMax)
end
local baseMin = output[damageType.."MinBase"]
local baseMax = output[damageType.."MaxBase"]
if baseMin == 0 and baseMax == 0 then
-- No base damage for this type, don't need to calculate modifiers
if breakdown and (addMin ~= 0 or addMax ~= 0) then
t_insert(breakdown.damageTypes, {
source = damageType,
convSrc = (addMin ~= 0 or addMax ~= 0) and (addMin .. " to " .. addMax),
total = addMin .. " to " .. addMax,
convDst = convDst and s_format("%d%% to %s", conversionTable[damageType][convDst] * 100, convDst),
})
end
return addMin, addMax
end
-- Combine modifiers
local modNames = damageStatsForTypes[typeFlags]
local inc = 1 + modDB:Sum("INC", cfg, unpack(modNames)) / 100
local more = m_floor(modDB:Sum("MORE", cfg, unpack(modNames)) * 100 + 0.50000001) / 100
if breakdown then
t_insert(breakdown.damageTypes, {
source = damageType,
base = baseMin .. " to " .. baseMax,
inc = (inc ~= 1 and "x "..inc),
more = (more ~= 1 and "x "..more),
convSrc = (addMin ~= 0 or addMax ~= 0) and (addMin .. " to " .. addMax),
total = (round(baseMin * inc * more) + addMin) .. " to " .. (round(baseMax * inc * more) + addMax),
convDst = convDst and conversionTable[damageType][convDst] > 0 and s_format("%d%% to %s", conversionTable[damageType][convDst] * 100, convDst),
})
end
return (round(baseMin * inc * more) + addMin),
(round(baseMax * inc * more) + addMax)
end
local function calcAilmentSourceDamage(actor, output, cfg, breakdown, damageType, typeFlags)
local min, max = calcDamage(actor, output, cfg, breakdown, damageType, typeFlags)
local convMult = actor.conversionTable[damageType].mult
if breakdown and convMult ~= 1 then
t_insert(breakdown, "Source damage:")
t_insert(breakdown, s_format("%d to %d ^8(total damage)", min, max))
t_insert(breakdown, s_format("x %g ^8(%g%% converted to other damage types)", convMult, (1-convMult)*100))
t_insert(breakdown, s_format("= %d to %d", min * convMult, max * convMult))
end
return min * convMult, max * convMult
end
-- Performs all offensive calculations
function calcs.offence(env, actor)
local modDB = actor.modDB
local enemyDB = actor.enemy.modDB
local output = actor.output
local breakdown = actor.breakdown
local mainSkill = actor.mainSkill
local skillData = mainSkill.skillData
local skillFlags = mainSkill.skillFlags
local skillCfg = mainSkill.skillCfg
if skillData.showAverage then
skillFlags.showAverage = true
else
skillFlags.notAverage = true
end
if skillFlags.disable then
-- Skill is disabled
output.CombinedDPS = 0
return
end
-- Merge main skill mods
modDB:AddList(mainSkill.skillModList)
-- Update skill data
for _, value in ipairs(modDB:Sum("LIST", skillCfg, "SkillData")) do
if value.merge == "MAX" then
skillData[value.key] = m_max(value.value, skillData[value.key] or 0)
else
skillData[value.key] = value.value
end
end
skillCfg.skillCond["SkillIsTriggered"] = skillData.triggered
-- Add addition stat bonuses
if modDB:Sum("FLAG", nil, "IronGrip") then
modDB:NewMod("PhysicalDamage", "INC", actor.strDmgBonus, "Strength", bor(ModFlag.Attack, ModFlag.Projectile))
end
if modDB:Sum("FLAG", nil, "IronWill") then
modDB:NewMod("Damage", "INC", actor.strDmgBonus, "Strength", ModFlag.Spell)
end
if modDB:Sum("FLAG", nil, "MinionDamageAppliesToPlayer") then
-- Minion Damage conversion from The Scourge
for _, value in ipairs(modDB:Sum("LIST", env.player.mainSkill.skillCfg, "MinionModifier")) do
if value.mod.name == "Damage" and value.mod.type == "INC" then
modDB:AddMod(value.mod)
end
end
end
if modDB:Sum("FLAG", nil, "MinionAttackSpeedAppliesToPlayer") then
-- Minion Attack Speed conversion from Spiritual Command
for _, value in ipairs(modDB:Sum("LIST", env.player.mainSkill.skillCfg, "MinionModifier")) do
if value.mod.name == "Speed" and value.mod.type == "INC" and band(value.mod.flags, ModFlag.Attack) ~= 0 then
modDB:AddMod(value.mod)
end
end
end
if modDB:Sum("FLAG", nil, "SpellDamageAppliesToAttacks") then
-- Spell Damage conversion from Crown of Eyes
for i, mod in ipairs(modDB.mods.Damage or { }) do
if mod.type == "INC" and band(mod.flags, ModFlag.Spell) ~= 0 then
modDB:NewMod("Damage", "INC", mod.value, mod.source, bor(band(mod.flags, bnot(ModFlag.Spell)), ModFlag.Attack), mod.keywordFlags, unpack(mod))
end
end
end
if modDB:Sum("FLAG", nil, "ClawDamageAppliesToUnarmed") then
-- Claw Damage conversion from Rigwald's Curse
for i, mod in ipairs(modDB.mods.PhysicalDamage or { }) do
if band(mod.flags, ModFlag.Claw) ~= 0 then
modDB:NewMod("PhysicalDamage", mod.type, mod.value, mod.source, bor(band(mod.flags, bnot(ModFlag.Claw)), ModFlag.Unarmed), mod.keywordFlags, unpack(mod))
end
end
end
if modDB:Sum("FLAG", nil, "ClawAttackSpeedAppliesToUnarmed") then
-- Claw Attack Speed conversion from Rigwald's Curse
for i, mod in ipairs(modDB.mods.Speed or { }) do
if band(mod.flags, ModFlag.Claw) ~= 0 and band(mod.flags, ModFlag.Attack) ~= 0 then
modDB:NewMod("Speed", mod.type, mod.value, mod.source, bor(band(mod.flags, bnot(ModFlag.Claw)), ModFlag.Unarmed), mod.keywordFlags, unpack(mod))
end
end
end
if modDB:Sum("FLAG", nil, "ClawCritChanceAppliesToUnarmed") then
-- Claw Crit Chance conversion from Rigwald's Curse
for i, mod in ipairs(modDB.mods.CritChance or { }) do
if band(mod.flags, ModFlag.Claw) ~= 0 then
modDB:NewMod("CritChance", mod.type, mod.value, mod.source, bor(band(mod.flags, bnot(ModFlag.Claw)), ModFlag.Unarmed), mod.keywordFlags, unpack(mod))
end
end
end
if modDB:Sum("FLAG", nil, "LightRadiusAppliesToAccuracy") then
-- Light Radius conversion from Corona Solaris
for i, mod in ipairs(modDB.mods.LightRadius or { }) do
if mod.type == "INC" then
modDB:NewMod("Accuracy", "INC", mod.value, mod.source, mod.flags, mod.keywordFlags, unpack(mod))
end
end
end
if modDB:Sum("FLAG", nil, "CastSpeedAppliesToTrapThrowingSpeed") then
-- Cast Speed conversion from Slavedriver's Hand
for i, mod in ipairs(modDB.mods.Speed or { }) do
if mod.type == "INC" and (mod.flags == 0 or band(mod.flags, ModFlag.Cast) ~= 0) then
modDB:NewMod("TrapThrowingSpeed", "INC", mod.value, mod.source, band(mod.flags, bnot(ModFlag.Cast), bnot(ModFlag.Attack)), mod.keywordFlags, unpack(mod))
end
end
end
local isAttack = skillFlags.attack
-- Calculate skill type stats
if skillFlags.minion then
if mainSkill.minion and mainSkill.minion.minionData.limit then
output.ActiveMinionLimit = m_floor(calcLib.val(modDB, mainSkill.minion.minionData.limit, skillCfg))
end
end
if skillFlags.chaining then
output.ChainMax = modDB:Sum("BASE", skillCfg, "ChainCountMax")
output.Chain = m_min(output.ChainMax, modDB:Sum("BASE", skillCfg, "ChainCount"))
output.ChainRemaining = m_max(0, output.ChainMax - output.Chain)
end
if skillFlags.projectile then
if modDB:Sum("FLAG", nil, "PointBlank") then
modDB:NewMod("Damage", "MORE", 50, "Point Blank", bor(ModFlag.Attack, ModFlag.Projectile), { type = "DistanceRamp", ramp = {{10,1},{35,0},{150,-1}} })
end
output.ProjectileCount = modDB:Sum("BASE", skillCfg, "ProjectileCount")
if modDB:Sum("FLAG", skillCfg, "PierceAllTargets") or enemyDB:Sum("FLAG", nil, "AlwaysPierceSelf") then
output.PierceCount = 100
output.PierceCountString = "All targets"
else
output.PierceCount = modDB:Sum("BASE", skillCfg, "PierceCount")
output.PierceCountString = output.PierceCount
end
output.ProjectileSpeedMod = calcLib.mod(modDB, skillCfg, "ProjectileSpeed")
if breakdown then
breakdown.ProjectileSpeedMod = breakdown.mod(skillCfg, "ProjectileSpeed")
end
end
if skillFlags.melee then
if skillFlags.weapon1Attack then
actor.weaponRange1 = (actor.weaponData1.range and actor.weaponData1.range + modDB:Sum("BASE", skillCfg, "MeleeWeaponRange")) or (env.data.weaponTypeInfo["None"].range + modDB:Sum("BASE", skillCfg, "UnarmedRange"))
end
if skillFlags.weapon2Attack then
actor.weaponRange2 = (actor.weaponData2.range and actor.weaponData2.range + modDB:Sum("BASE", skillCfg, "MeleeWeaponRange")) or (env.data.weaponTypeInfo["None"].range + modDB:Sum("BASE", skillCfg, "UnarmedRange"))
end
if mainSkill.skillTypes[SkillType.MeleeSingleTarget] then
local range = 100
if skillFlags.weapon1Attack then
range = m_min(range, actor.weaponRange1)
end
if skillFlags.weapon2Attack then
range = m_min(range, actor.weaponRange2)
end
output.WeaponRange = range + 2
if breakdown then
breakdown.WeaponRange = {
radius = output.WeaponRange
}
end
end
end
if skillFlags.area or skillData.radius then
output.AreaOfEffectMod = calcLib.mod(modDB, skillCfg, "AreaOfEffect")
if skillData.radiusIsWeaponRange then
local range = 0
if skillFlags.weapon1Attack then
range = m_max(range, actor.weaponRange1)
end
if skillFlags.weapon2Attack then
range = m_max(range, actor.weaponRange2)
end
skillData.radius = range + 2
end
if skillData.radius then
skillFlags.area = true
local baseRadius = skillData.radius + (skillData.radiusExtra or 0) + modDB:Sum("BASE", skillCfg, "AreaOfEffect")
output.AreaOfEffectRadius = m_floor(baseRadius * m_sqrt(output.AreaOfEffectMod))
if breakdown then
breakdown.AreaOfEffectRadius = breakdown.area(baseRadius, output.AreaOfEffectMod, output.AreaOfEffectRadius)
end
if skillData.radiusSecondary then
output.AreaOfEffectModSecondary = calcLib.mod(modDB, skillCfg, "AreaOfEffect", "AreaOfEffectSecondary")
baseRadius = skillData.radiusSecondary + (skillData.radiusExtra or 0)
output.AreaOfEffectRadiusSecondary = m_floor(baseRadius * m_sqrt(output.AreaOfEffectModSecondary))
if breakdown then
breakdown.AreaOfEffectRadiusSecondary = breakdown.area(baseRadius, output.AreaOfEffectModSecondary, output.AreaOfEffectRadiusSecondary)
end
end
end
if breakdown then
breakdown.AreaOfEffectMod = breakdown.mod(skillCfg, "AreaOfEffect")
end
end
if skillFlags.trap then
local baseSpeed = 1 / modDB:Sum("BASE", skillCfg, "TrapThrowingTime")
output.TrapThrowingSpeed = baseSpeed * calcLib.mod(modDB, skillCfg, "TrapThrowingSpeed") * output.ActionSpeedMod
output.TrapThrowingTime = 1 / output.TrapThrowingSpeed
if breakdown then
breakdown.TrapThrowingTime = { }
breakdown.multiChain(breakdown.TrapThrowingTime, {
label = "Throwing speed:",
base = s_format("%.2f ^8(base throwing speed)", baseSpeed),
{ "%.2f ^8(increased/reduced throwing speed)", 1 + modDB:Sum("INC", skillCfg, "TrapThrowingSpeed") / 100 },
{ "%.2f ^8(more/less throwing speed)", modDB:Sum("MORE", skillCfg, "TrapThrowingSpeed") },
{ "%.2f ^8(action speed modifier)", output.ActionSpeedMod },
total = s_format("= %.2f ^8per second", output.TrapThrowingSpeed),
})
end
output.ActiveTrapLimit = modDB:Sum("BASE", skillCfg, "ActiveTrapLimit")
local baseCooldown = skillData.trapCooldown or skillData.cooldown
if baseCooldown then
output.TrapCooldown = baseCooldown / calcLib.mod(modDB, skillCfg, "CooldownRecovery")
if breakdown then
breakdown.TrapCooldown = {
s_format("%.2fs ^8(base)", skillData.trapCooldown or skillData.cooldown or 4),
s_format("/ %.2f ^8(increased/reduced cooldown recovery)", 1 + modDB:Sum("INC", skillCfg, "CooldownRecovery") / 100),
s_format("= %.2fs", output.TrapCooldown)
}
end
end
local areaMod = calcLib.mod(modDB, skillCfg, "TrapTriggerAreaOfEffect")
output.TrapTriggerRadius = 10 * m_sqrt(areaMod)
if breakdown then
breakdown.TrapTriggerRadius = breakdown.area(10, areaMod, output.TrapTriggerRadius)
end
elseif skillData.cooldown then
output.Cooldown = skillData.cooldown / calcLib.mod(modDB, skillCfg, "CooldownRecovery")
if breakdown then
breakdown.Cooldown = {
s_format("%.2fs ^8(base)", skillData.cooldown),
s_format("/ %.2f ^8(increased/reduced cooldown recovery)", 1 + modDB:Sum("INC", skillCfg, "CooldownRecovery") / 100),
s_format("= %.2fs", output.Cooldown)
}
end
end
if skillFlags.mine then
local baseSpeed = 1 / modDB:Sum("BASE", skillCfg, "MineLayingTime")
output.MineLayingSpeed = baseSpeed * calcLib.mod(modDB, skillCfg, "MineLayingSpeed") * output.ActionSpeedMod
output.MineLayingTime = 1 / output.MineLayingSpeed
if breakdown then
breakdown.MineLayingTime = { }
breakdown.multiChain(breakdown.MineLayingTime, {
label = "Laying speed:",
base = s_format("%.2f ^8(base laying speed)", baseSpeed),
{ "%.2f ^8(increased/reduced laying speed)", 1 + modDB:Sum("INC", skillCfg, "MineLayingSpeed") / 100 },
{ "%.2f ^8(more/less laying speed)", modDB:Sum("MORE", skillCfg, "MineLayingSpeed") },
{ "%.2f ^8(action speed modifier)", output.ActionSpeedMod },
total = s_format("= %.2f ^8per second", output.MineLayingSpeed),
})
end
output.ActiveMineLimit = modDB:Sum("BASE", skillCfg, "ActiveMineLimit")
local areaMod = calcLib.mod(modDB, skillCfg, "MineDetonationAreaOfEffect")
output.MineDetonationRadius = 60 * m_sqrt(areaMod)
if breakdown then
breakdown.MineDetonationRadius = breakdown.area(60, areaMod, output.MineDetonationRadius)
end
end
if skillFlags.totem then
local baseSpeed = 1 / modDB:Sum("BASE", skillCfg, "TotemPlacementTime")
output.TotemPlacementSpeed = baseSpeed * calcLib.mod(modDB, skillCfg, "TotemPlacementSpeed") * output.ActionSpeedMod
output.TotemPlacementTime = 1 / output.TotemPlacementSpeed
if breakdown then
breakdown.TotemPlacementTime = { }
breakdown.multiChain(breakdown.TotemPlacementTime, {
label = "Placement speed:",
base = s_format("%.2f ^8(base placement speed)", baseSpeed),
{ "%.2f ^8(increased/reduced placement speed)", 1 + modDB:Sum("INC", skillCfg, "TotemPlacementSpeed") / 100 },
{ "%.2f ^8(more/less placement speed)", modDB:Sum("MORE", skillCfg, "TotemPlacementSpeed") },
{ "%.2f ^8(action speed modifier)", output.ActionSpeedMod },
total = s_format("= %.2f ^8per second", output.TotemPlacementSpeed),
})
end
output.ActiveTotemLimit = modDB:Sum("BASE", skillCfg, "ActiveTotemLimit")
output.TotemLifeMod = calcLib.mod(modDB, skillCfg, "TotemLife")
output.TotemLife = round(m_floor(env.data.monsterAllyLifeTable[skillData.totemLevel] * env.data.totemLifeMult[mainSkill.skillTotemId]) * output.TotemLifeMod)
if breakdown then
breakdown.TotemLifeMod = breakdown.mod(skillCfg, "TotemLife")
breakdown.TotemLife = {
"Totem level: "..skillData.totemLevel,
env.data.monsterAllyLifeTable[skillData.totemLevel].." ^8(base life for a level "..skillData.totemLevel.." monster)",
"x "..env.data.totemLifeMult[mainSkill.skillTotemId].." ^8(life multiplier for this totem type)",
"x "..output.TotemLifeMod.." ^8(totem life modifier)",
"= "..output.TotemLife,
}
end
end
-- Skill duration
local debuffDurationMult
if env.mode_effective then
debuffDurationMult = 1 / calcLib.mod(enemyDB, skillCfg, "BuffExpireFaster")
else
debuffDurationMult = 1
end
do
output.DurationMod = calcLib.mod(modDB, skillCfg, "Duration", "PrimaryDuration", "SkillAndDamagingAilmentDuration")
if breakdown then
breakdown.DurationMod = breakdown.mod(skillCfg, "Duration", "PrimaryDuration", "SkillAndDamagingAilmentDuration")
end
local durationBase = (skillData.duration or 0) + modDB:Sum("BASE", skillCfg, "Duration", "PrimaryDuration")
if durationBase > 0 then
output.Duration = durationBase * output.DurationMod
if skillData.debuff then
output.Duration = output.Duration * debuffDurationMult
end
if breakdown and output.Duration ~= durationBase then
breakdown.Duration = {
s_format("%.2fs ^8(base)", durationBase),
}
if output.DurationMod ~= 1 then
t_insert(breakdown.Duration, s_format("x %.2f ^8(duration modifier)", output.DurationMod))
end
if skillData.debuff and debuffDurationMult ~= 1 then
t_insert(breakdown.Duration, s_format("/ %.2f ^8(debuff expires slower/faster)", 1 / debuffDurationMult))
end
t_insert(breakdown.Duration, s_format("= %.2fs", output.Duration))
end
end
durationBase = (skillData.durationSecondary or 0) + modDB:Sum("BASE", skillCfg, "Duration", "SecondaryDuration")
if durationBase > 0 then
local durationMod = calcLib.mod(modDB, skillCfg, "Duration", "SecondaryDuration", "SkillAndDamagingAilmentDuration")
output.DurationSecondary = durationBase * durationMod
if skillData.debuffSecondary then
output.DurationSecondary = output.DurationSecondary * debuffDurationMult
end
if breakdown and output.DurationSecondary ~= durationBase then
breakdown.DurationSecondary = {
s_format("%.2fs ^8(base)", durationBase),
}
if output.DurationMod ~= 1 then
t_insert(breakdown.DurationSecondary, s_format("x %.2f ^8(duration modifier)", durationMod))
end
if skillData.debuffSecondary and debuffDurationMult ~= 1 then
t_insert(breakdown.DurationSecondary, s_format("/ %.2f ^8(debuff expires slower/faster)", 1 / debuffDurationMult))
end
t_insert(breakdown.DurationSecondary, s_format("= %.2fs", output.DurationSecondary))
end
end
end
-- Run skill setup function
do
local setupFunc = mainSkill.activeEffect.grantedEffect.setupFunc
if setupFunc then
setupFunc(actor, output)
end
end
-- Handle corpse explosions
if skillData.explodeCorpse and skillData.corpseLife then
local damageType = skillData.corpseExplosionDamageType or "Fire"
skillData[damageType.."BonusMin"] = skillData.corpseLife * skillData.corpseExplosionLifeMultiplier
skillData[damageType.."BonusMax"] = skillData.corpseLife * skillData.corpseExplosionLifeMultiplier
end
-- Cache global damage disabling flags
local canDeal = { }
for _, damageType in pairs(dmgTypeList) do
canDeal[damageType] = not modDB:Sum("FLAG", skillCfg, "DealNo"..damageType)
end
-- Calculate damage conversion percentages
actor.conversionTable = wipeTable(actor.conversionTable)
for damageTypeIndex = 1, 4 do
local damageType = dmgTypeList[damageTypeIndex]
local globalConv = wipeTable(tempTable1)
local skillConv = wipeTable(tempTable2)
local add = wipeTable(tempTable3)
local globalTotal, skillTotal = 0, 0
for otherTypeIndex = damageTypeIndex + 1, 5 do
-- For all possible destination types, check for global and skill conversions
otherType = dmgTypeList[otherTypeIndex]
globalConv[otherType] = modDB:Sum("BASE", skillCfg, damageType.."DamageConvertTo"..otherType, isElemental[damageType] and "ElementalDamageConvertTo"..otherType or nil, damageType ~= "Chaos" and "NonChaosDamageConvertTo"..otherType or nil)
globalTotal = globalTotal + globalConv[otherType]
skillConv[otherType] = modDB:Sum("BASE", skillCfg, "Skill"..damageType.."DamageConvertTo"..otherType)
skillTotal = skillTotal + skillConv[otherType]
add[otherType] = modDB:Sum("BASE", skillCfg, damageType.."DamageGainAs"..otherType, isElemental[damageType] and "ElementalDamageGainAs"..otherType or nil, damageType ~= "Chaos" and "NonChaosDamageGainAs"..otherType or nil)
end
if skillTotal > 100 then
-- Skill conversion exceeds 100%, scale it down and remove non-skill conversions
local factor = 100 / skillTotal
for type, val in pairs(skillConv) do
-- Overconversion is fixed in 3.0, so I finally get to uncomment this line!
skillConv[type] = val * factor
end
for type, val in pairs(globalConv) do
globalConv[type] = 0
end
elseif globalTotal + skillTotal > 100 then
-- Conversion exceeds 100%, scale down non-skill conversions
local factor = (100 - skillTotal) / globalTotal
for type, val in pairs(globalConv) do
globalConv[type] = val * factor
end
globalTotal = globalTotal * factor
end
local dmgTable = { }
for type, val in pairs(globalConv) do
dmgTable[type] = (globalConv[type] + skillConv[type] + add[type]) / 100
end
dmgTable.mult = 1 - m_min((globalTotal + skillTotal) / 100, 1)
actor.conversionTable[damageType] = dmgTable
end
actor.conversionTable["Chaos"] = { mult = 1 }
-- Calculate mana cost (may be slightly off due to rounding differences)
do
local more = m_floor(modDB:Sum("MORE", skillCfg, "ManaCost") * 100 + 0.0001) / 100
local inc = modDB:Sum("INC", skillCfg, "ManaCost")
local base = modDB:Sum("BASE", skillCfg, "ManaCost")
output.ManaCost = m_floor(m_max(0, (skillData.manaCost or 0) * more * (1 + inc / 100) + base))
if mainSkill.skillTypes[SkillType.ManaCostPercent] and skillFlags.totem then
output.ManaCost = m_floor(output.Mana * output.ManaCost / 100)
end
if breakdown and output.ManaCost ~= (skillData.manaCost or 0) then
breakdown.ManaCost = {
s_format("%d ^8(base mana cost)", skillData.manaCost or 0)
}
if more ~= 1 then
t_insert(breakdown.ManaCost, s_format("x %.2f ^8(mana cost multiplier)", more))
end
if inc ~= 0 then
t_insert(breakdown.ManaCost, s_format("x %.2f ^8(increased/reduced mana cost)", 1 + inc/100))
end
if base ~= 0 then
t_insert(breakdown.ManaCost, s_format("- %d ^8(- mana cost)", -base))
end
t_insert(breakdown.ManaCost, s_format("= %d", output.ManaCost))
end
end
-- Configure damage passes
local passList = { }
if isAttack then
output.MainHand = { }
output.OffHand = { }
if skillFlags.weapon1Attack then
if breakdown then
breakdown.MainHand = LoadModule(calcs.breakdownModule, modDB, output.MainHand)
end
mainSkill.weapon1Cfg.skillStats = output.MainHand
t_insert(passList, {
label = "Main Hand",
source = actor.weaponData1,
cfg = mainSkill.weapon1Cfg,
output = output.MainHand,
breakdown = breakdown and breakdown.MainHand,
})
end
if skillFlags.weapon2Attack then
if breakdown then
breakdown.OffHand = LoadModule(calcs.breakdownModule, modDB, output.OffHand)
end
mainSkill.weapon2Cfg.skillStats = output.OffHand
local source = copyTable(actor.weaponData2)
if skillData.setOffHandBaseCritChance then
source.CritChance = skillData.setOffHandBaseCritChance
end
if skillData.setOffHandPhysicalMin and skillData.setOffHandPhysicalMax then
source.PhysicalMin = skillData.setOffHandPhysicalMin
source.PhysicalMax = skillData.setOffHandPhysicalMax
end
if skillData.setOffHandAttackTime then
source.AttackRate = 1000 / skillData.setOffHandAttackTime
end
t_insert(passList, {
label = "Off Hand",
source = source,
cfg = mainSkill.weapon2Cfg,
output = output.OffHand,
breakdown = breakdown and breakdown.OffHand,
})
end
else
t_insert(passList, {
label = "Skill",
source = skillData,
cfg = skillCfg,
output = output,
breakdown = breakdown,
})
end
local function combineStat(stat, mode, ...)
-- Combine stats from Main Hand and Off Hand according to the mode
if mode == "OR" or not skillFlags.bothWeaponAttack then
output[stat] = output.MainHand[stat] or output.OffHand[stat]
elseif mode == "ADD" then
output[stat] = (output.MainHand[stat] or 0) + (output.OffHand[stat] or 0)
elseif mode == "AVERAGE" then
output[stat] = ((output.MainHand[stat] or 0) + (output.OffHand[stat] or 0)) / 2
elseif mode == "CHANCE" 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)
output[stat] = output.MainHand[stat] * mainPortion + output.OffHand[stat] * offPortion
if breakdown then
if not breakdown[stat] then
breakdown[stat] = { }
end
t_insert(breakdown[stat], "Contribution from Main Hand:")
t_insert(breakdown[stat], s_format("%.1f", output.MainHand[stat]))
t_insert(breakdown[stat], s_format("x %.3f ^8(portion of instances created by main hand)", mainPortion))
t_insert(breakdown[stat], s_format("= %.1f", output.MainHand[stat] * mainPortion))
t_insert(breakdown[stat], "Contribution from Off Hand:")
t_insert(breakdown[stat], s_format("%.1f", output.OffHand[stat]))
t_insert(breakdown[stat], s_format("x %.3f ^8(portion of instances created by off hand)", offPortion))
t_insert(breakdown[stat], s_format("= %.1f", output.OffHand[stat] * offPortion))
t_insert(breakdown[stat], "Total:")
t_insert(breakdown[stat], s_format("%.1f + %.1f", output.MainHand[stat] * mainPortion, output.OffHand[stat] * offPortion))
t_insert(breakdown[stat], s_format("= %.1f", output[stat]))
end
else
output[stat] = output.MainHand[stat] or output.OffHand[stat]
end
elseif mode == "DPS" then
output[stat] = (output.MainHand[stat] or 0) + (output.OffHand[stat] or 0)
if not skillData.doubleHitsWhenDualWielding then
output[stat] = output[stat] / 2
end
end
end
for _, pass in ipairs(passList) do
local globalOutput, globalBreakdown = output, breakdown
local source, output, cfg, breakdown = pass.source, pass.output, pass.cfg, pass.breakdown
-- Calculate hit chance
output.Accuracy = calcLib.val(modDB, "Accuracy", cfg)
if breakdown then
breakdown.Accuracy = breakdown.simple(nil, cfg, output.Accuracy, "Accuracy")
end
if not isAttack or modDB:Sum("FLAG", cfg, "CannotBeEvaded") or skillData.cannotBeEvaded or (env.mode_effective and enemyDB:Sum("FLAG", nil, "CannotEvade")) then
output.HitChance = 100
else
local enemyEvasion = round(calcLib.val(enemyDB, "Evasion"))
output.HitChance = calcLib.hitChance(enemyEvasion, output.Accuracy)
if breakdown then
breakdown.HitChance = {
"Enemy level: "..env.enemyLevel..(env.configInput.enemyLevel and " ^8(overridden from the Configuration tab" or " ^8(can be overridden in the Configuration tab)"),
"Average enemy evasion: "..enemyEvasion,
"Approximate hit chance: "..output.HitChance.."%",
}
end
end
-- Calculate attack/cast speed
if mainSkill.skillTypes[SkillType.Instant] then
output.Time = 0
output.Speed = 0
elseif skillData.timeOverride then
output.Time = skillData.timeOverride
output.Speed = 1 / output.Time
else
local baseSpeed
if isAttack then
if skillData.castTimeOverridesAttackTime then
-- Skill is overriding weapon attack speed
baseSpeed = 1 / skillData.castTime * (1 + (source.AttackSpeedInc or 0) / 100)
else
baseSpeed = source.AttackRate or 1
end
else
baseSpeed = 1 / (skillData.castTime or 1)
end
local inc = modDB:Sum("INC", cfg, "Speed")
local more = modDB:Sum("MORE", cfg, "Speed")
output.Speed = baseSpeed * round((1 + inc/100) * more, 2)
if skillData.attackRateCap then
output.Speed = m_min(output.Speed, skillData.attackRateCap)
end
if skillFlags.selfCast then
-- Self-cast skill; apply action speed
output.Speed = output.Speed * globalOutput.ActionSpeedMod
end
if breakdown then
breakdown.Speed = { }
breakdown.multiChain(breakdown.Speed, {
base = s_format("%.2f ^8(base)", baseSpeed),
{ "%.2f ^8(increased/reduced)", 1 + inc/100 },
{ "%.2f ^8(more/less)", more },
{ "%.2f ^8(action speed modifier)", skillFlags.selfCast and globalOutput.ActionSpeedMod or 1 },
total = s_format("= %.2f ^8per second", output.Speed)
})
end
if output.Speed == 0 then
output.Time = 0
else
output.Time = 1 / output.Speed
end
end
if skillData.hitTimeOverride then
output.HitTime = skillData.hitTimeOverride
output.HitSpeed = 1 / output.HitTime
end
end
if isAttack then
-- Combine hit chance and attack speed
combineStat("HitChance", "AVERAGE")
combineStat("Speed", "AVERAGE")
if output.Speed == 0 then
output.Time = 0
else
output.Time = 1 / output.Speed
end
if skillFlags.bothWeaponAttack then
if breakdown then
breakdown.Speed = {
"Both weapons:",
s_format("(%.2f + %.2f) / 2", output.MainHand.Speed, output.OffHand.Speed),
s_format("= %.2f", output.Speed),
}
end
end
end
for _, pass in ipairs(passList) do
local globalOutput, globalBreakdown = output, breakdown
local source, output, cfg, breakdown = pass.source, pass.output, pass.cfg, pass.breakdown
-- Calculate crit chance, crit multiplier, and their combined effect
if modDB:Sum("FLAG", nil, "NeverCrit") then
output.PreEffectiveCritChance = 0
output.CritChance = 0
output.CritMultiplier = 0
output.CritDegenMultiplier = 0
output.CritEffect = 1
else
local baseCrit = source.CritChance or 0
if baseCrit == 100 then
output.PreEffectiveCritChance = 100
output.CritChance = 100
else
local base = modDB:Sum("BASE", cfg, "CritChance")
local inc = modDB:Sum("INC", cfg, "CritChance")
local more = modDB:Sum("MORE", cfg, "CritChance")
local enemyExtra = env.mode_effective and enemyDB:Sum("BASE", nil, "SelfExtraCritChance") or 0
output.CritChance = (baseCrit + base) * (1 + inc / 100) * more
local preCapCritChance = output.CritChance
output.CritChance = m_min(output.CritChance, 95)
if (baseCrit + base) > 0 then
output.CritChance = m_max(output.CritChance, 5)
end
output.PreEffectiveCritChance = output.CritChance
if enemyExtra ~= 0 then
output.CritChance = m_min(output.CritChance + enemyExtra, 100)
end
local preLuckyCritChance = output.CritChance
if env.mode_effective and modDB:Sum("FLAG", cfg, "CritChanceLucky") then
output.CritChance = (1 - (1 - output.CritChance / 100) ^ 2) * 100
end
local preHitCheckCritChance = output.CritChance
if env.mode_effective then
output.CritChance = output.CritChance * output.HitChance / 100
end
if breakdown and output.CritChance ~= baseCrit then
breakdown.CritChance = { }
if base ~= 0 then
t_insert(breakdown.CritChance, s_format("(%g + %g) ^8(base)", baseCrit, base))
else
t_insert(breakdown.CritChance, s_format("%g ^8(base)", baseCrit + base))
end
if inc ~= 0 then
t_insert(breakdown.CritChance, s_format("x %.2f", 1 + inc/100).." ^8(increased/reduced)")
end
if more ~= 1 then
t_insert(breakdown.CritChance, s_format("x %.2f", more).." ^8(more/less)")
end
t_insert(breakdown.CritChance, s_format("= %.2f%% ^8(crit chance)", output.PreEffectiveCritChance))
if preCapCritChance > 95 then
local overCap = preCapCritChance - 95
t_insert(breakdown.CritChance, s_format("Crit is overcapped by %.2f%% (%d%% increased Critical Strike Chance)", overCap, overCap / more / (baseCrit + base) * 100))
end
if enemyExtra ~= 0 then
t_insert(breakdown.CritChance, s_format("+ %g ^8(extra chance for enemy to be crit)", enemyExtra))
t_insert(breakdown.CritChance, s_format("= %.2f%% ^8(chance to crit against enemy)", preLuckyCritChance))
end
if env.mode_effective and modDB:Sum("FLAG", cfg, "CritChanceLucky") then
t_insert(breakdown.CritChance, "Crit Chance is Lucky:")
t_insert(breakdown.CritChance, s_format("1 - (1 - %.4f) x (1 - %.4f)", preLuckyCritChance / 100, preLuckyCritChance / 100))
t_insert(breakdown.CritChance, s_format("= %.2f%%", preHitCheckCritChance))
end
if env.mode_effective and output.HitChance < 100 then
t_insert(breakdown.CritChance, "Crit confirmation roll:")
t_insert(breakdown.CritChance, s_format("%.2f%%", preHitCheckCritChance))
t_insert(breakdown.CritChance, s_format("x %.2f ^8(chance to hit)", output.HitChance / 100))
t_insert(breakdown.CritChance, s_format("= %.2f%%", output.CritChance))
end
end
end
if modDB:Sum("FLAG", cfg, "NoCritMultiplier") then
output.CritMultiplier = 1
else
local extraDamage = modDB:Sum("BASE", cfg, "CritMultiplier") / 100
if env.mode_effective then
local enemyInc = 1 + enemyDB:Sum("INC", nil, "SelfCritMultiplier") / 100
extraDamage = round(extraDamage * enemyInc, 2)
if breakdown and enemyInc ~= 1 then
breakdown.CritMultiplier = {
s_format("%d%% ^8(additional extra damage)", modDB:Sum("BASE", cfg, "CritMultiplier") / 100),
s_format("x %.2f ^8(increased/reduced extra crit damage taken by enemy)", enemyInc),
s_format("= %d%% ^8(extra crit damage)", extraDamage * 100),
}
end
end
output.CritMultiplier = 1 + m_max(0, extraDamage)
end
if modDB:Sum("FLAG", cfg, "NoCritDegenMultiplier") then
output.CritDegenMultiplier = 1
else
output.CritDegenMultiplier = 1 + modDB:Sum("BASE", cfg, "CritDegenMultiplier") / 100 + (modDB:Sum("BASE", cfg, "CritMultiplier") - 50) * modDB:Sum("BASE", cfg, "CritMultiplierAppliesToDegen") / 10000
end
output.CritEffect = 1 - output.CritChance / 100 + output.CritChance / 100 * output.CritMultiplier
if breakdown and output.CritEffect ~= 1 then
breakdown.CritEffect = {
s_format("(1 - %.4f) ^8(portion of damage from non-crits)", output.CritChance/100),
s_format("+ (%.4f x %g) ^8(portion of damage from crits)", output.CritChance/100, output.CritMultiplier),
s_format("= %.3f", output.CritEffect),
}
end
end
-- Calculate Double Damage + Ruthless Blow chance/multipliers
output.DoubleDamageChance = m_min(modDB:Sum("BASE", cfg, "DoubleDamageChance"), 100)
output.DoubleDamageEffect = 1 + output.DoubleDamageChance / 100
output.RuthlessBlowMaxCount = modDB:Sum("BASE", cfg, "RuthlessBlowMaxCount")
if output.RuthlessBlowMaxCount > 0 then
output.RuthlessBlowChance = round(100 / output.RuthlessBlowMaxCount)
else
output.RuthlessBlowChance = 0
end
output.RuthlessBlowMultiplier = 1 + modDB:Sum("BASE", cfg, "RuthlessBlowMultiplier") / 100
output.RuthlessBlowEffect = 1 - output.RuthlessBlowChance / 100 + output.RuthlessBlowChance / 100 * output.RuthlessBlowMultiplier
-- Calculate base hit damage
for _, damageType in ipairs(dmgTypeList) do
local damageTypeMin = damageType.."Min"
local damageTypeMax = damageType.."Max"
local baseMultiplier = skillData.baseMultiplier or 1
local damageEffectiveness = skillData.damageEffectiveness or 1
local addedMin = modDB:Sum("BASE", cfg, damageTypeMin)
local addedMax = modDB:Sum("BASE", cfg, damageTypeMax)
local baseMin = ((source[damageTypeMin] or 0) + (source[damageType.."BonusMin"] or 0)) * baseMultiplier + addedMin * damageEffectiveness
local baseMax = ((source[damageTypeMax] or 0) + (source[damageType.."BonusMax"] or 0)) * baseMultiplier + addedMax * damageEffectiveness
output[damageTypeMin.."Base"] = baseMin
output[damageTypeMax.."Base"] = baseMax
if breakdown then
breakdown[damageType] = { damageTypes = { } }
if baseMin ~= 0 and baseMax ~= 0 then
t_insert(breakdown[damageType], "Base damage:")
local plus = ""
if (source[damageTypeMin] or 0) ~= 0 or (source[damageTypeMax] or 0) ~= 0 then
if baseMultiplier ~= 1 then
t_insert(breakdown[damageType], s_format("(%d to %d) x %.2f ^8(base damage from %s multiplied by base damage multiplier)", source[damageTypeMin], source[damageTypeMax], baseMultiplier, source.type and "weapon" or "skill"))
else
t_insert(breakdown[damageType], s_format("%d to %d ^8(base damage from %s)", source[damageTypeMin], source[damageTypeMax], source.type and "weapon" or "skill"))
end
plus = "+ "
end
if addedMin ~= 0 or addedMax ~= 0 then
if damageEffectiveness ~= 1 then
t_insert(breakdown[damageType], s_format("%s(%d to %d) x %.2f ^8(added damage multiplied by damage effectiveness)", plus, addedMin, addedMax, damageEffectiveness))
else
t_insert(breakdown[damageType], s_format("%s%d to %d ^8(added damage)", plus, addedMin, addedMax))
end
end
t_insert(breakdown[damageType], s_format("= %.1f to %.1f", baseMin, baseMax))
end
end
end
-- Calculate hit damage for each damage type
local totalHitMin, totalHitMax = 0, 0
local totalCritMin, totalCritMax = 0, 0
output.LifeLeech = 0
output.LifeLeechInstant = 0
output.ManaLeech = 0
output.ManaLeechInstant = 0
for pass = 1, 2 do
-- Pass 1 is critical strike damage, pass 2 is non-critical strike
cfg.skillCond["CriticalStrike"] = (pass == 1)
local lifeLeechTotal = 0
local manaLeechTotal = 0
local noLifeLeech = modDB:Sum("FLAG", cfg, "CannotLeechLife") or enemyDB:Sum("FLAG", nil, "CannotLeechLifeFromSelf")
local noManaLeech = modDB:Sum("FLAG", cfg, "CannotLeechMana") or enemyDB:Sum("FLAG", nil, "CannotLeechManaFromSelf")
for _, damageType in ipairs(dmgTypeList) do
local min, max
if skillFlags.hit and canDeal[damageType] then
min, max = calcDamage(actor, output, cfg, pass == 2 and breakdown and breakdown[damageType], damageType, 0)
local convMult = actor.conversionTable[damageType].mult
if pass == 2 and breakdown then
t_insert(breakdown[damageType], "Hit damage:")
t_insert(breakdown[damageType], s_format("%d to %d ^8(total damage)", min, max))
if convMult ~= 1 then
t_insert(breakdown[damageType], s_format("x %g ^8(%g%% converted to other damage types)", convMult, (1-convMult)*100))
end
if output.DoubleDamageEffect ~= 1 then
t_insert(breakdown[damageType], s_format("x %.2f ^8(chance to deal double damage)", output.DoubleDamageEffect))
end
if output.RuthlessBlowEffect ~= 1 then
t_insert(breakdown[damageType], s_format("x %.2f ^8(ruthless blow effect modifier)", output.RuthlessBlowEffect))
end
end
local allMult = convMult * output.DoubleDamageEffect * output.RuthlessBlowEffect
if pass == 1 then
-- Apply crit multiplier
allMult = allMult * output.CritMultiplier
end
min = min * allMult
max = max * allMult
if (min ~= 0 or max ~= 0) and env.mode_effective then
-- Apply enemy resistances and damage taken modifiers
local resist = 0
local pen = 0
local taken = enemyDB:Sum("INC", cfg, "DamageTaken", damageType.."DamageTaken")
if damageType == "Physical" then
resist = enemyDB:Sum("BASE", nil, "PhysicalDamageReduction")
else
resist = enemyDB:Sum("BASE", nil, damageType.."Resist")
if isElemental[damageType] then
resist = resist + enemyDB:Sum("BASE", nil, "ElementalResist")
pen = modDB:Sum("BASE", cfg, damageType.."Penetration", "ElementalPenetration")
taken = taken + enemyDB:Sum("INC", nil, "ElementalDamageTaken")
end
resist = m_min(resist, 75)
end
if skillFlags.projectile then
taken = taken + enemyDB:Sum("INC", nil, "ProjectileDamageTaken")
end
if skillFlags.trap or skillFlags.mine then
taken = taken + enemyDB:Sum("INC", nil, "TrapMineDamageTaken")
end
local effMult = (1 + taken / 100)
if not isElemental[damageType] or not (modDB:Sum("FLAG", cfg, "IgnoreElementalResistances", "Ignore"..damageType.."Resistance") or enemyDB:Sum("FLAG", nil, "SelfIgnore"..damageType.."Resistance")) then
effMult = effMult * (1 - (resist - pen) / 100)
end
min = min * effMult
max = max * effMult
if env.mode == "CALCS" then
output[damageType.."EffMult"] = effMult
end
if pass == 2 and breakdown and effMult ~= 1 then
t_insert(breakdown[damageType], s_format("x %.3f ^8(effective DPS modifier)", effMult))
breakdown[damageType.."EffMult"] = breakdown.effMult(damageType, resist, pen, taken, effMult)
end
end
if pass == 2 and breakdown then
t_insert(breakdown[damageType], s_format("= %d to %d", min, max))
end
if skillFlags.mine or skillFlags.trap or skillFlags.totem then
if not noLifeLeech then
local lifeLeech = modDB:Sum("BASE", cfg, "DamageLifeLeechToPlayer")
if lifeLeech > 0 then
lifeLeechTotal = lifeLeechTotal + (min + max) / 2 * lifeLeech / 100
end
end
else
if not noLifeLeech then
local lifeLeech
if modDB:Sum("FLAG", nil, "LifeLeechBasedOnChaosDamage") then
if damageType == "Chaos" then
lifeLeech = modDB:Sum("BASE", cfg, "DamageLeech", "DamageLifeLeech", "PhysicalDamageLifeLeech", "LightningDamageLifeLeech", "ColdDamageLifeLeech", "FireDamageLifeLeech", "ChaosDamageLifeLeech", "ElementalDamageLifeLeech") + enemyDB:Sum("BASE", nil, "SelfDamageLifeLeech") / 100
else
lifeLeech = 0
end
else
lifeLeech = modDB:Sum("BASE", cfg, "DamageLeech", "DamageLifeLeech", damageType.."DamageLifeLeech", isElemental[damageType] and "ElementalDamageLifeLeech" or nil) + enemyDB:Sum("BASE", nil, "SelfDamageLifeLeech") / 100
end
if lifeLeech > 0 then
lifeLeechTotal = lifeLeechTotal + (min + max) / 2 * lifeLeech / 100
end
end
if not noManaLeech then
local manaLeech = modDB:Sum("BASE", cfg, "DamageLeech", "DamageManaLeech", damageType.."DamageManaLeech", isElemental[damageType] and "ElementalDamageManaLeech" or nil) + enemyDB:Sum("BASE", nil, "SelfDamageManaLeech") / 100
if manaLeech > 0 then
manaLeechTotal = manaLeechTotal + (min + max) / 2 * manaLeech / 100
end
end
end
else
min, max = 0, 0
if breakdown then
breakdown[damageType] = {
"You can't deal "..damageType.." damage"
}
end
end
if pass == 1 then
output[damageType.."CritAverage"] = (min + max) / 2
totalCritMin = totalCritMin + min
totalCritMax = totalCritMax + max
else
if env.mode == "CALCS" then
output[damageType.."Min"] = min
output[damageType.."Max"] = max
end
output[damageType.."HitAverage"] = (min + max) / 2
totalHitMin = totalHitMin + min
totalHitMax = totalHitMax + max
end
end
if skillData.lifeLeechPerUse then
lifeLeechTotal = lifeLeechTotal + skillData.lifeLeechPerUse
end
if skillData.manaLeechPerUse then
manaLeechTotal = manaLeechTotal + skillData.manaLeechPerUse
end
local portion = (pass == 1) and (output.CritChance / 100) or (1 - output.CritChance / 100)
if modDB:Sum("FLAG", cfg, "InstantLifeLeech") and not modDB:Sum("FLAG", nil, "GhostReaver") then
output.LifeLeechInstant = output.LifeLeechInstant + lifeLeechTotal * portion
else
output.LifeLeech = output.LifeLeech + lifeLeechTotal * portion
end
if modDB:Sum("FLAG", cfg, "InstantManaLeech") then
output.ManaLeechInstant = output.ManaLeechInstant + manaLeechTotal * portion
else
output.ManaLeech = output.ManaLeech + manaLeechTotal * portion
end
end
output.TotalMin = totalHitMin
output.TotalMax = totalHitMax
if modDB:Sum("FLAG", skillCfg, "ElementalEquilibrium") and not env.configInput.EEIgnoreHitDamage and (output.FireHitAverage + output.ColdHitAverage + output.LightningHitAverage > 0) then
-- Update enemy hit-by-damage-type conditions
enemyDB.conditions.HitByFireDamage = output.FireHitAverage > 0
enemyDB.conditions.HitByColdDamage = output.ColdHitAverage > 0
enemyDB.conditions.HitByLightningDamage = output.LightningHitAverage > 0
end
if breakdown then
-- For each damage type, calculate percentage of total damage
for _, damageType in ipairs(dmgTypeList) do
if output[damageType.."HitAverage"] > 0 then
t_insert(breakdown[damageType], s_format("Portion of total damage: %d%%", output[damageType.."HitAverage"] / (totalHitMin + totalHitMax) * 200))
end
end
end
local hitRate = output.HitChance / 100 * (globalOutput.HitSpeed or globalOutput.Speed) * (skillData.dpsMultiplier or 1)
-- Calculate leech
local function getLeechInstances(amount, total)
if total == 0 then
return 0, 0
end
local duration = amount / total / 0.02
return duration, duration * hitRate
end
output.LifeLeechDuration, output.LifeLeechInstances = getLeechInstances(output.LifeLeech, modDB:Sum("FLAG", nil, "GhostReaver") and globalOutput.EnergyShield or globalOutput.Life)
output.LifeLeechInstantRate = output.LifeLeechInstant * hitRate
output.ManaLeechDuration, output.ManaLeechInstances = getLeechInstances(output.ManaLeech, globalOutput.Mana)
output.ManaLeechInstantRate = output.ManaLeechInstant * hitRate
-- Calculate gain on hit
if skillFlags.mine or skillFlags.trap or skillFlags.totem then
output.LifeOnHit = 0
output.EnergyShieldOnHit = 0
output.ManaOnHit = 0
else
output.LifeOnHit = (modDB:Sum("BASE", skillCfg, "LifeOnHit") + enemyDB:Sum("BASE", skillCfg, "SelfLifeOnHit")) * globalOutput.LifeRecoveryMod
output.EnergyShieldOnHit = (modDB:Sum("BASE", skillCfg, "EnergyShieldOnHit") + enemyDB:Sum("BASE", skillCfg, "SelfEnergyShieldOnHit")) * globalOutput.EnergyShieldRecoveryMod
output.ManaOnHit = (modDB:Sum("BASE", skillCfg, "ManaOnHit") + enemyDB:Sum("BASE", skillCfg, "SelfManaOnHit")) * globalOutput.ManaRecoveryMod
end
output.LifeOnHitRate = output.LifeOnHit * hitRate
output.EnergyShieldOnHitRate = output.EnergyShieldOnHit * hitRate
output.ManaOnHitRate = output.ManaOnHit * hitRate
-- Calculate average damage and final DPS
output.AverageHit = (totalHitMin + totalHitMax) / 2 * (1 - output.CritChance / 100) + (totalCritMin + totalCritMax) / 2 * output.CritChance / 100
output.AverageDamage = output.AverageHit * output.HitChance / 100
output.TotalDPS = output.AverageDamage * (globalOutput.HitSpeed or globalOutput.Speed) * (skillData.dpsMultiplier or 1)
if breakdown then
if output.CritEffect ~= 1 then
breakdown.AverageHit = {
s_format("%.1f x (1 - %.4f) ^8(damage from non-crits)", (totalHitMin + totalHitMax) / 2, output.CritChance / 100),
s_format("+ %.1f x %.4f ^8(damage from crits)", (totalCritMin + totalCritMax) / 2, output.CritChance / 100),
s_format("= %.1f", output.AverageHit),
}
end
if isAttack then
breakdown.AverageDamage = { }
t_insert(breakdown.AverageDamage, s_format("%s:", pass.label))
t_insert(breakdown.AverageDamage, s_format("%.1f ^8(average hit)", output.AverageHit))
t_insert(breakdown.AverageDamage, s_format("x %.2f ^8(chance to hit)", output.HitChance / 100))
t_insert(breakdown.AverageDamage, s_format("= %.1f", output.AverageDamage))
end
end
end
if isAttack then
-- Combine crit stats, average damage and DPS
combineStat("PreEffectiveCritChance", "AVERAGE")
combineStat("CritChance", "AVERAGE")
combineStat("CritMultiplier", "AVERAGE")
combineStat("AverageDamage", "DPS")
combineStat("TotalDPS", "DPS")
combineStat("LifeLeechDuration", "DPS")
combineStat("LifeLeechInstances", "DPS")
combineStat("LifeLeechInstant", "DPS")
combineStat("LifeLeechInstantRate", "DPS")
combineStat("ManaLeechDuration", "DPS")
combineStat("ManaLeechInstances", "DPS")
combineStat("ManaLeechInstant", "DPS")
combineStat("ManaLeechInstantRate", "DPS")
combineStat("LifeOnHit", "DPS")
combineStat("LifeOnHitRate", "DPS")
combineStat("EnergyShieldOnHit", "DPS")
combineStat("EnergyShieldOnHitRate", "DPS")
combineStat("ManaOnHit", "DPS")
combineStat("ManaOnHitRate", "DPS")
if skillFlags.bothWeaponAttack then
if breakdown then
breakdown.AverageDamage = { }
t_insert(breakdown.AverageDamage, "Both weapons:")
if skillData.doubleHitsWhenDualWielding then
t_insert(breakdown.AverageDamage, s_format("%.1f + %.1f ^8(skill hits with both weapons at once)", output.MainHand.AverageDamage, output.OffHand.AverageDamage))
else
t_insert(breakdown.AverageDamage, s_format("(%.1f + %.1f) / 2 ^8(skill alternates weapons)", output.MainHand.AverageDamage, output.OffHand.AverageDamage))
end
t_insert(breakdown.AverageDamage, s_format("= %.1f", output.AverageDamage))
end
end
end
if env.mode == "CALCS" then
if skillData.showAverage then
output.DisplayDamage = s_format("%.1f average damage", output.AverageDamage)
else
output.DisplayDamage = s_format("%.1f DPS", output.TotalDPS)
end
end
if breakdown then
if isAttack then
breakdown.TotalDPS = {
s_format("%.1f ^8(average damage)", output.AverageDamage),
output.HitSpeed and s_format("x %.2f ^8(hit rate)", output.HitSpeed) or s_format("x %.2f ^8(attack rate)", output.Speed),
}
else
breakdown.TotalDPS = {
s_format("%.1f ^8(average hit)", output.AverageDamage),
output.HitSpeed and s_format("x %.2f ^8(hit rate)", output.HitSpeed) or s_format("x %.2f ^8(cast rate)", output.Speed),
}
end
if skillData.dpsMultiplier then
t_insert(breakdown.TotalDPS, s_format("x %g ^8(DPS multiplier for this skill)", skillData.dpsMultiplier))
end
t_insert(breakdown.TotalDPS, s_format("= %.1f", output.TotalDPS))
end
-- Calculate leech rates
if modDB:Sum("FLAG", nil, "GhostReaver") then
output.LifeLeechRate = 0
output.LifeLeechPerHit = 0
output.EnergyShieldLeechInstanceRate = output.EnergyShield * 0.02 * calcLib.mod(modDB, skillCfg, "LifeLeechRate")
output.EnergyShieldLeechRate = output.LifeLeechInstantRate * output.EnergyShieldRecoveryMod + m_min(output.LifeLeechInstances * output.EnergyShieldLeechInstanceRate, output.MaxEnergyShieldLeechRate) * output.EnergyShieldRecoveryRateMod
output.EnergyShieldLeechPerHit = output.LifeLeechInstant * output.EnergyShieldRecoveryMod + m_min(output.EnergyShieldLeechInstanceRate, output.MaxEnergyShieldLeechRate) * output.LifeLeechDuration * output.EnergyShieldRecoveryRateMod
else
output.LifeLeechInstanceRate = output.Life * 0.02 * calcLib.mod(modDB, skillCfg, "LifeLeechRate")
output.LifeLeechRate = output.LifeLeechInstantRate * output.LifeRecoveryMod + m_min(output.LifeLeechInstances * output.LifeLeechInstanceRate, output.MaxLifeLeechRate) * output.LifeRecoveryRateMod
output.LifeLeechPerHit = output.LifeLeechInstant * output.LifeRecoveryMod + m_min(output.LifeLeechInstanceRate, output.MaxLifeLeechRate) * output.LifeLeechDuration * output.LifeRecoveryRateMod
output.EnergyShieldLeechRate = 0
output.EnergyShieldLeechPerHit = 0
end
do
output.ManaLeechInstanceRate = output.Mana * 0.02 * calcLib.mod(modDB, skillCfg, "ManaLeechRate")
output.ManaLeechRate = output.ManaLeechInstantRate * output.ManaRecoveryMod + m_min(output.ManaLeechInstances * output.ManaLeechInstanceRate, output.MaxManaLeechRate) * output.ManaRecoveryRateMod
output.ManaLeechPerHit = output.ManaLeechInstant * output.ManaRecoveryMod + m_min(output.ManaLeechInstanceRate, output.MaxManaLeechRate) * output.ManaLeechDuration * output.ManaRecoveryRateMod
end
skillFlags.leechES = output.EnergyShieldLeechRate > 0
skillFlags.leechLife = output.LifeLeechRate > 0
skillFlags.leechMana = output.ManaLeechRate > 0
if skillData.showAverage then
output.LifeLeechGainPerHit = output.LifeLeechPerHit + output.LifeOnHit
output.EnergyShieldLeechGainPerHit = output.EnergyShieldLeechPerHit + output.EnergyShieldOnHit
output.ManaLeechGainPerHit = output.ManaLeechPerHit + output.ManaOnHit
else
output.LifeLeechGainRate = output.LifeLeechRate + output.LifeOnHitRate
output.EnergyShieldLeechGainRate = output.EnergyShieldLeechRate + output.EnergyShieldOnHitRate
output.ManaLeechGainRate = output.ManaLeechRate + output.ManaOnHitRate
end
if breakdown then
if skillFlags.leechLife then
breakdown.LifeLeech = breakdown.leech(output.LifeLeechInstant, output.LifeLeechInstantRate, output.LifeLeechInstances, output.Life, "LifeLeechRate", output.MaxLifeLeechRate, output.LifeLeechDuration)
end
if skillFlags.leechES then
breakdown.EnergyShieldLeech = breakdown.leech(output.LifeLeechInstant, output.LifeLeechInstantRate, output.LifeLeechInstances, output.EnergyShield, "LifeLeechRate", output.MaxEnergyShieldLeechRate, output.LifeLeechDuration)
end
if skillFlags.leechMana then
breakdown.ManaLeech = breakdown.leech(output.ManaLeechInstant, output.ManaLeechInstantRate, output.ManaLeechInstances, output.Mana, "ManaLeechRate", output.MaxManaLeechRate, output.ManaLeechDuration)
end
end
-- Calculate skill DOT components
local dotCfg = {
skillName = skillCfg.skillName,
skillPart = skillCfg.skillPart,
skillTypes = skillCfg.skillTypes,
slotName = skillCfg.slotName,
flags = bor(ModFlag.Dot, skillData.dotIsSpell and ModFlag.Spell or 0, skillData.dotIsArea and ModFlag.Area or 0, skillData.dotIsProjectile and ModFlag.Projectile or 0),
keywordFlags = band(skillCfg.keywordFlags, bnot(KeywordFlag.Hit)),
}
mainSkill.dotCfg = dotCfg
output.TotalDot = 0
for _, damageType in ipairs(dmgTypeList) do
local baseVal
if canDeal[damageType] then
baseVal = skillData[damageType.."Dot"] or 0
else
baseVal = 0
end
if baseVal > 0 then
skillFlags.dot = true
if damageType == "Fire" then
dotCfg.keywordFlags = bor(dotCfg.keywordFlags, KeywordFlag.Burning)
else
dotCfg.keywordFlags = band(dotCfg.keywordFlags, bnot(KeywordFlag.Burning))
end
local effMult = 1
if env.mode_effective then
local resist = 0
local taken = enemyDB:Sum("INC", nil, "DamageTaken", "DamageTakenOverTime", damageType.."DamageTaken", damageType.."DamageTakenOverTime")
if damageType == "Physical" then
resist = enemyDB:Sum("BASE", nil, "PhysicalDamageReduction")
else
resist = enemyDB:Sum("BASE", nil, damageType.."Resist")
if isElemental[damageType] then
resist = resist + enemyDB:Sum("BASE", nil, "ElementalResist")
taken = taken + enemyDB:Sum("INC", nil, "ElementalDamageTaken")
end
resist = m_min(resist, 75)
end
effMult = (1 - resist / 100) * (1 + taken / 100)
output[damageType.."DotEffMult"] = effMult
if breakdown and effMult ~= 1 then
breakdown[damageType.."DotEffMult"] = breakdown.effMult(damageType, resist, 0, taken, effMult)
end
end
local inc = modDB:Sum("INC", dotCfg, "Damage", damageType.."Damage", isElemental[damageType] and "ElementalDamage" or nil)
local more = round(modDB:Sum("MORE", dotCfg, "Damage", damageType.."Damage", isElemental[damageType] and "ElementalDamage" or nil), 2)
local total = baseVal * (1 + inc/100) * more * effMult
if skillFlags.aura then
total = total * calcLib.mod(modDB, dotCfg, "AuraEffect")
end
output[damageType.."Dot"] = total
output.TotalDot = output.TotalDot + total
if breakdown then
breakdown[damageType.."Dot"] = { }
breakdown.dot(breakdown[damageType.."Dot"], baseVal, inc, more, nil, effMult, total)
end
end
end
skillFlags.bleed = false
skillFlags.poison = false
skillFlags.ignite = false
skillFlags.igniteCanStack = modDB:Sum("FLAG", skillCfg, "IgniteCanStack")
skillFlags.shock = false
skillFlags.freeze = false
for _, pass in ipairs(passList) do
local 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
cfg.skillCond["CriticalStrike"] = true
if not skillFlags.attack or modDB:Sum("FLAG", cfg, "CannotBleed") then
output.BleedChanceOnCrit = 0
else
output.BleedChanceOnCrit = m_min(100, modDB:Sum("BASE", cfg, "BleedChance") + enemyDB:Sum("BASE", nil, "SelfBleedChance"))
end
if not skillFlags.hit or modDB:Sum("FLAG", cfg, "CannotPoison") then
output.PoisonChanceOnCrit = 0
else
output.PoisonChanceOnCrit = m_min(100, modDB:Sum("BASE", cfg, "PoisonChance") + enemyDB:Sum("BASE", nil, "SelfPoisonChance"))
end
if not skillFlags.hit or modDB:Sum("FLAG", cfg, "CannotIgnite") then
output.IgniteChanceOnCrit = 0
else
output.IgniteChanceOnCrit = 100
end
if not skillFlags.hit or modDB:Sum("FLAG", cfg, "CannotShock") then
output.ShockChanceOnCrit = 0
else
output.ShockChanceOnCrit = 100
end
if not skillFlags.hit or modDB:Sum("FLAG", cfg, "CannotFreeze") then
output.FreezeChanceOnCrit = 0
else
output.FreezeChanceOnCrit = 100
end
if not skillFlags.hit or modDB:Sum("FLAG", cfg, "CannotKnockback") then
output.KnockbackChanceOnCrit = 0
else
output.KnockbackChanceOnCrit = modDB:Sum("BASE", cfg, "EnemyKnockbackChance")
end
cfg.skillCond["CriticalStrike"] = false
if not skillFlags.attack or modDB:Sum("FLAG", cfg, "CannotBleed") then
output.BleedChanceOnHit = 0
else
output.BleedChanceOnHit = m_min(100, modDB:Sum("BASE", cfg, "BleedChance") + enemyDB:Sum("BASE", nil, "SelfBleedChance"))
end
if not skillFlags.hit or modDB:Sum("FLAG", cfg, "CannotPoison") then
output.PoisonChanceOnHit = 0
output.ChaosPoisonChance = 0
else
output.PoisonChanceOnHit = m_min(100, modDB:Sum("BASE", cfg, "PoisonChance") + enemyDB:Sum("BASE", nil, "SelfPoisonChance"))
output.ChaosPoisonChance = m_min(100, modDB:Sum("BASE", cfg, "ChaosPoisonChance"))
end
if not skillFlags.hit or modDB:Sum("FLAG", cfg, "CannotIgnite") then
output.IgniteChanceOnHit = 0
else
output.IgniteChanceOnHit = m_min(100, modDB:Sum("BASE", cfg, "EnemyIgniteChance") + enemyDB:Sum("BASE", nil, "SelfIgniteChance"))
end
if not skillFlags.hit or modDB:Sum("FLAG", cfg, "CannotShock") then
output.ShockChanceOnHit = 0
else
output.ShockChanceOnHit = m_min(100, modDB:Sum("BASE", cfg, "EnemyShockChance") + enemyDB:Sum("BASE", nil, "SelfShockChance"))
end
if not skillFlags.hit or modDB:Sum("FLAG", cfg, "CannotFreeze") then
output.FreezeChanceOnHit = 0
else
output.FreezeChanceOnHit = m_min(100, modDB:Sum("BASE", cfg, "EnemyFreezeChance") + enemyDB:Sum("BASE", nil, "SelfFreezeChance"))
if modDB:Sum("FLAG", cfg, "CritsDontAlwaysFreeze") then
output.FreezeChanceOnCrit = output.FreezeChanceOnHit
end
end
if not skillFlags.hit or modDB:Sum("FLAG", cfg, "CannotKnockback") then
output.KnockbackChanceOnHit = 0
else
output.KnockbackChanceOnHit = modDB:Sum("BASE", cfg, "EnemyKnockbackChance")
end
if env.mode_effective then
local bleedMult = (1 - enemyDB:Sum("BASE", nil, "AvoidBleed") / 100)
output.BleedChanceOnHit = output.BleedChanceOnHit * bleedMult
output.BleedChanceOnCrit = output.BleedChanceOnCrit * bleedMult
local poisonMult = (1 - enemyDB:Sum("BASE", nil, "AvoidPoison") / 100)
output.PoisonChanceOnHit = output.PoisonChanceOnHit * poisonMult
output.PoisonChanceOnCrit = output.PoisonChanceOnCrit * poisonMult
output.ChaosPoisonChance = output.ChaosPoisonChance * poisonMult
local igniteMult = (1 - enemyDB:Sum("BASE", nil, "AvoidIgnite") / 100)
output.IgniteChanceOnHit = output.IgniteChanceOnHit * igniteMult
output.IgniteChanceOnCrit = output.IgniteChanceOnCrit * igniteMult
local shockMult = (1 - enemyDB:Sum("BASE", nil, "AvoidShock") / 100)
output.ShockChanceOnHit = output.ShockChanceOnHit * shockMult
output.ShockChanceOnCrit = output.ShockChanceOnCrit * shockMult
local freezeMult = (1 - enemyDB:Sum("BASE", nil, "AvoidFreeze") / 100)
output.FreezeChanceOnHit = output.FreezeChanceOnHit * freezeMult
output.FreezeChanceOnCrit = output.FreezeChanceOnCrit * freezeMult
end
local function calcAilmentDamage(type, sourceHitDmg, sourceCritDmg)
-- Calculate the inflict chance and base damage of a secondary effect (bleed/poison/ignite/shock/freeze)
local chanceOnHit, chanceOnCrit = output[type.."ChanceOnHit"], output[type.."ChanceOnCrit"]
local chanceFromHit = chanceOnHit * (1 - output.CritChance / 100)
local chanceFromCrit = chanceOnCrit * output.CritChance / 100
local chance = chanceFromHit + chanceFromCrit
output[type.."Chance"] = chance
local baseFromHit = sourceHitDmg * chanceFromHit / (chanceFromHit + chanceFromCrit)
local baseFromCrit = sourceCritDmg * chanceFromCrit / (chanceFromHit + chanceFromCrit)
local baseVal = baseFromHit + baseFromCrit
if breakdown and chance ~= 0 then
local breakdownChance = breakdown[type.."Chance"] or { }
breakdown[type.."Chance"] = breakdownChance
if breakdownChance[1] then
t_insert(breakdownChance, "")
end
if isAttack then
t_insert(breakdownChance, pass.label..":")
end
t_insert(breakdownChance, s_format("Chance on Non-crit: %d%%", chanceOnHit))
t_insert(breakdownChance, s_format("Chance on Crit: %d%%", chanceOnCrit))
if chanceOnHit ~= chanceOnCrit then
t_insert(breakdownChance, "Combined chance:")
t_insert(breakdownChance, s_format("%d x (1 - %.4f) ^8(chance from non-crits)", chanceOnHit, output.CritChance/100))
t_insert(breakdownChance, s_format("+ %d x %.4f ^8(chance from crits)", chanceOnCrit, output.CritChance/100))
t_insert(breakdownChance, s_format("= %.2f", chance))
end
end
if breakdown and baseVal > 0 then
local breakdownDPS = breakdown[type.."DPS"] or { }
breakdown[type.."DPS"] = breakdownDPS
if breakdownDPS[1] then
t_insert(breakdownDPS, "")
end
if isAttack then
t_insert(breakdownDPS, pass.label..":")
end
if sourceHitDmg == sourceCritDmg then
t_insert(breakdownDPS, "Total damage:")
t_insert(breakdownDPS, s_format("%.1f ^8(source damage)",sourceHitDmg))
else
if baseFromHit > 0 then
t_insert(breakdownDPS, "Damage from Non-crits:")
t_insert(breakdownDPS, s_format("%.1f ^8(source damage from non-crits)", sourceHitDmg))
t_insert(breakdownDPS, s_format("x %.3f ^8(portion of instances created by non-crits)", chanceFromHit / (chanceFromHit + chanceFromCrit)))
t_insert(breakdownDPS, s_format("= %.1f", baseFromHit))
end
if baseFromCrit > 0 then
t_insert(breakdownDPS, "Damage from Crits:")
t_insert(breakdownDPS, s_format("%.1f ^8(source damage from crits)", sourceCritDmg))
t_insert(breakdownDPS, s_format("x %.3f ^8(portion of instances created by crits)", chanceFromCrit / (chanceFromHit + chanceFromCrit)))
t_insert(breakdownDPS, s_format("= %.1f", baseFromCrit))
end
if baseFromHit > 0 and baseFromCrit > 0 then
t_insert(breakdownDPS, "Total damage:")
t_insert(breakdownDPS, s_format("%.1f + %.1f", baseFromHit, baseFromCrit))
t_insert(breakdownDPS, s_format("= %.1f", baseVal))
end
end
end
return baseVal
end
-- Calculate bleeding chance and damage
if canDeal.Physical and (output.BleedChanceOnHit + output.BleedChanceOnCrit) > 0 then
if not mainSkill.bleedCfg then
mainSkill.bleedCfg = {
skillName = skillCfg.skillName,
skillPart = skillCfg.skillPart,
skillTypes = skillCfg.skillTypes,
slotName = skillCfg.slotName,
flags = bor(ModFlag.Dot, ModFlag.Ailment),
keywordFlags = bor(band(skillCfg.keywordFlags, bnot(KeywordFlag.Hit)), KeywordFlag.Bleed, KeywordFlag.Ailment),
skillCond = { },
}
end
local dotCfg = mainSkill.bleedCfg
local sourceHitDmg, sourceCritDmg
if breakdown then
breakdown.BleedPhysical = { damageTypes = { } }
end
for pass = 1, 2 do
dotCfg.skillCond["CriticalStrike"] = (pass == 1)
local min, max = calcAilmentSourceDamage(actor, output, dotCfg, pass == 2 and breakdown and breakdown.BleedPhysical, "Physical", 0)
output.BleedPhysicalMin = min
output.BleedPhysicalMax = max
if pass == 1 then
sourceCritDmg = (min + max) / 2 * output.CritDegenMultiplier
else
sourceHitDmg = (min + max) / 2
end
end
local basePercent = skillData.bleedBasePercent or 70
local baseVal = calcAilmentDamage("Bleed", sourceHitDmg, sourceCritDmg) * basePercent / 100 * output.RuthlessBlowEffect
if baseVal > 0 then
skillFlags.bleed = true
skillFlags.duration = true
local effMult = 1
if env.mode_effective then
local resist = enemyDB:Sum("BASE", nil, "PhysicalDamageReduction")
local taken = enemyDB:Sum("INC", dotCfg, "DamageTaken", "DamageTakenOverTime", "PhysicalDamageTaken", "PhysicalDamageTakenOverTime")
effMult = (1 - resist / 100) * (1 + taken / 100)
globalOutput["BleedEffMult"] = effMult
if breakdown and effMult ~= 1 then
globalBreakdown.BleedEffMult = breakdown.effMult("Physical", resist, 0, taken, effMult)
end
end
local effectMod = calcLib.mod(modDB, dotCfg, "AilmentEffect")
output.BleedDPS = baseVal * effectMod * effMult
local durationBase
if skillData.bleedDurationIsSkillDuration then
durationBase = skillData.duration
else
durationBase = 5
end
local durationMod = calcLib.mod(modDB, dotCfg, "EnemyBleedDuration", "SkillAndDamagingAilmentDuration", skillData.bleedIsSkillEffect and "Duration" or nil) * calcLib.mod(enemyDB, nil, "SelfBleedDuration")
globalOutput.BleedDuration = durationBase * durationMod * debuffDurationMult
if breakdown then
t_insert(breakdown.BleedDPS, s_format("x %.2f ^8(bleed deals %d%% per second)", basePercent/100, basePercent))
if effectMod ~= 1 then
t_insert(breakdown.BleedDPS, s_format("x %.2f ^8(ailment effect modifier)", effectMod))
end
if output.RuthlessBlowEffect ~= 0 then
t_insert(breakdown.BleedDPS, s_format("x %.2f ^8(ruthless blow effect modifier)", output.RuthlessBlowEffect))
end
t_insert(breakdown.BleedDPS, s_format("= %.1f", baseVal))
breakdown.multiChain(breakdown.BleedDPS, {
label = "Bleed DPS:",
base = s_format("%.1f ^8(total damage per second)", baseVal),
{ "%.2f ^8(ailment effect modifier)", effectMod },
{ "%.3f ^8(effective DPS modifier)", effMult },
total = s_format("= %.1f ^8per second", output.BleedDPS),
})
if globalOutput.BleedDuration ~= durationBase then
globalBreakdown.BleedDuration = {
s_format("%.2fs ^8(base duration)", durationBase)
}
if durationMod ~= 1 then
t_insert(globalBreakdown.BleedDuration, s_format("x %.2f ^8(duration modifier)", durationMod))
end
if debuffDurationMult ~= 1 then
t_insert(globalBreakdown.BleedDuration, s_format("/ %.2f ^8(debuff expires slower/faster)", 1 / debuffDurationMult))
end
t_insert(globalBreakdown.BleedDuration, s_format("= %.2fs", globalOutput.BleedDuration))
end
end
end
end
-- Calculate poison chance and damage
if canDeal.Chaos and (output.PoisonChanceOnHit + output.PoisonChanceOnCrit + output.ChaosPoisonChance) > 0 then
if not mainSkill.poisonCfg then
mainSkill.poisonCfg = {
skillName = skillCfg.skillName,
skillPart = skillCfg.skillPart,
skillTypes = skillCfg.skillTypes,
slotName = skillCfg.slotName,
flags = bor(ModFlag.Dot, ModFlag.Ailment),
keywordFlags = bor(band(skillCfg.keywordFlags, bnot(KeywordFlag.Hit)), KeywordFlag.Poison, KeywordFlag.Ailment),
skillCond = { },
}
end
local dotCfg = mainSkill.poisonCfg
local sourceHitDmg, sourceCritDmg
if breakdown then
breakdown.PoisonPhysical = { damageTypes = { } }
breakdown.PoisonLightning = { damageTypes = { } }
breakdown.PoisonCold = { damageTypes = { } }
breakdown.PoisonFire = { damageTypes = { } }
breakdown.PoisonChaos = { damageTypes = { } }
end
for pass = 1, 2 do
dotCfg.skillCond["CriticalStrike"] = (pass == 1)
local totalMin, totalMax = 0, 0
do
local min, max = calcAilmentSourceDamage(actor, output, dotCfg, pass == 2 and breakdown and breakdown.PoisonChaos, "Chaos", 0)
output.PoisonChaosMin = min
output.PoisonChaosMax = max
totalMin = totalMin + min
totalMax = totalMax + max
end
local nonChaosMult = 1
if output.ChaosPoisonChance > 0 and output.PoisonChaosMax > 0 then
-- Additional chance for chaos
local chance = (pass == 1) and "PoisonChanceOnCrit" or "PoisonChanceOnHit"
local chaosChance = m_min(100, output[chance] + output.ChaosPoisonChance)
nonChaosMult = output[chance] / chaosChance
output[chance] = chaosChance
end
if canDeal.Lightning and modDB:Sum("FLAG", cfg, "LightningCanPoison") then
local min, max = calcAilmentSourceDamage(actor, output, dotCfg, pass == 2 and breakdown and breakdown.PoisonLightning, "Lightning", dmgTypeFlags.Chaos)
output.PoisonLightningMin = min
output.PoisonLightningMax = max
totalMin = totalMin + min * nonChaosMult
totalMax = totalMax + max * nonChaosMult
end
if canDeal.Cold and modDB:Sum("FLAG", cfg, "ColdCanPoison") then
local min, max = calcAilmentSourceDamage(actor, output, dotCfg, pass == 2 and breakdown and breakdown.PoisonCold, "Cold", dmgTypeFlags.Chaos)
output.PoisonColdMin = min
output.PoisonColdMax = max
totalMin = totalMin + min * nonChaosMult
totalMax = totalMax + max * nonChaosMult
end
if canDeal.Fire and modDB:Sum("FLAG", cfg, "FireCanPoison") then
local min, max = calcAilmentSourceDamage(actor, output, dotCfg, pass == 2 and breakdown and breakdown.PoisonFire, "Fire", dmgTypeFlags.Chaos)
output.PoisonFireMin = min
output.PoisonFireMax = max
totalMin = totalMin + min * nonChaosMult
totalMax = totalMax + max * nonChaosMult
end
if canDeal.Physical then
local min, max = calcAilmentSourceDamage(actor, output, dotCfg, pass == 2 and breakdown and breakdown.PoisonPhysical, "Physical", dmgTypeFlags.Chaos)
output.PoisonPhysicalMin = min
output.PoisonPhysicalMax = max
totalMin = totalMin + min * nonChaosMult
totalMax = totalMax + max * nonChaosMult
end
if pass == 1 then
sourceCritDmg = (totalMin + totalMax) / 2 * output.CritDegenMultiplier
else
sourceHitDmg = (totalMin + totalMax) / 2
end
end
local baseVal = calcAilmentDamage("Poison", sourceHitDmg, sourceCritDmg) * 0.20
if baseVal > 0 then
skillFlags.poison = true
skillFlags.duration = true
local effMult = 1
if env.mode_effective then
local resist = m_min(enemyDB:Sum("BASE", nil, "ChaosResist"), 75)
local taken = enemyDB:Sum("INC", nil, "DamageTaken", "DamageTakenOverTime", "ChaosDamageTaken", "ChaosDamageTakenOverTime")
effMult = (1 - resist / 100) * (1 + taken / 100)
globalOutput["PoisonEffMult"] = effMult
if breakdown and effMult ~= 1 then
globalBreakdown.PoisonEffMult = breakdown.effMult("Chaos", resist, 0, taken, effMult)
end
end
local effectMod = calcLib.mod(modDB, dotCfg, "AilmentEffect")
output.PoisonDPS = baseVal * effectMod * effMult
local durationBase
if skillData.poisonDurationIsSkillDuration then
durationBase = skillData.duration
else
durationBase = 2
end
local durationMod = calcLib.mod(modDB, dotCfg, "EnemyPoisonDuration", "SkillAndDamagingAilmentDuration", skillData.poisonIsSkillEffect and "Duration" or nil) * calcLib.mod(enemyDB, nil, "SelfPoisonDuration")
globalOutput.PoisonDuration = durationBase * durationMod * debuffDurationMult
output.PoisonDamage = output.PoisonDPS * globalOutput.PoisonDuration
if skillData.showAverage then
output.TotalPoisonAverageDamage = output.HitChance / 100 * output.PoisonChance / 100 * output.PoisonDamage
else
output.TotalPoisonStacks = output.HitChance / 100 * output.PoisonChance / 100 * globalOutput.PoisonDuration * (globalOutput.HitSpeed or globalOutput.Speed) * (skillData.dpsMultiplier or 1)
output.TotalPoisonDPS = output.PoisonDPS * output.TotalPoisonStacks
end
if breakdown then
t_insert(breakdown.PoisonDPS, "x 0.20 ^8(poison deals 20% per second)")
t_insert(breakdown.PoisonDPS, s_format("= %.1f", baseVal, 1))
breakdown.multiChain(breakdown.PoisonDPS, {
label = "Poison DPS:",
base = s_format("%.1f ^8(total damage per second)", baseVal),
{ "%.2f ^8(ailment effect modifier)", effectMod },
{ "%.3f ^8(effective DPS modifier)", effMult },
total = s_format("= %.1f ^8per second", output.PoisonDPS),
})
if globalOutput.PoisonDuration ~= 2 then
globalBreakdown.PoisonDuration = {
s_format("%.2fs ^8(base duration)", durationBase)
}
if durationMod ~= 1 then
t_insert(globalBreakdown.PoisonDuration, s_format("x %.2f ^8(duration modifier)", durationMod))
end
if debuffDurationMult ~= 1 then
t_insert(globalBreakdown.PoisonDuration, s_format("/ %.2f ^8(debuff expires slower/faster)", 1 / debuffDurationMult))
end
t_insert(globalBreakdown.PoisonDuration, s_format("= %.2fs", globalOutput.PoisonDuration))
end
breakdown.PoisonDamage = { }
if isAttack then
t_insert(breakdown.PoisonDamage, pass.label..":")
end
t_insert(breakdown.PoisonDamage, s_format("%.1f ^8(damage per second)", output.PoisonDPS))
t_insert(breakdown.PoisonDamage, s_format("x %.2fs ^8(poison duration)", globalOutput.PoisonDuration))
t_insert(breakdown.PoisonDamage, s_format("= %.1f ^8damage per poison stack", output.PoisonDamage))
if not skillData.showAverage then
breakdown.TotalPoisonStacks = { }
if isAttack then
t_insert(breakdown.TotalPoisonStacks, pass.label..":")
end
breakdown.multiChain(breakdown.TotalPoisonStacks, {
base = s_format("%.2fs ^8(poison duration)", globalOutput.PoisonDuration),
{ "%.2f ^8(poison chance)", output.PoisonChance / 100 },
{ "%.2f ^8(hit chance)", output.HitChance / 100 },
{ "%.2f ^8(hits per second)", globalOutput.HitSpeed or globalOutput.Speed },
{ "%g ^8(dps multiplier for this skill)", skillData.dpsMultiplier or 1 },
total = s_format("= %.1f", output.TotalPoisonStacks),
})
end
end
end
end
-- Calculate ignite chance and damage
if canDeal.Fire and (output.IgniteChanceOnHit + output.IgniteChanceOnCrit) > 0 then
if not mainSkill.igniteCfg then
mainSkill.igniteCfg = {
skillName = skillCfg.skillName,
skillPart = skillCfg.skillPart,
skillTypes = skillCfg.skillTypes,
slotName = skillCfg.slotName,
flags = bor(ModFlag.Dot, ModFlag.Ailment),
keywordFlags = bor(band(skillCfg.keywordFlags, bnot(KeywordFlag.Hit)), KeywordFlag.Ignite, KeywordFlag.Ailment, KeywordFlag.Burning),
skillCond = { },
}
end
local dotCfg = mainSkill.igniteCfg
local sourceHitDmg, sourceCritDmg
if breakdown then
breakdown.IgnitePhysical = { damageTypes = { } }
breakdown.IgniteLightning = { damageTypes = { } }
breakdown.IgniteCold = { damageTypes = { } }
breakdown.IgniteFire = { damageTypes = { } }
breakdown.IgniteChaos = { damageTypes = { } }
end
for pass = 1, 2 do
dotCfg.skillCond["CriticalStrike"] = (pass == 1)
local totalMin, totalMax = 0, 0
if canDeal.Physical and modDB:Sum("FLAG", cfg, "PhysicalCanIgnite") then
local min, max = calcAilmentSourceDamage(actor, output, dotCfg, pass == 2 and breakdown and breakdown.IgnitePhysical, "Physical", dmgTypeFlags.Fire)
output.IgnitePhysicalMin = min
output.IgnitePhysicalMax = max
totalMin = totalMin + min
totalMax = totalMax + max
end
if canDeal.Lightning and modDB:Sum("FLAG", cfg, "LightningCanIgnite") then
local min, max = calcAilmentSourceDamage(actor, output, dotCfg, pass == 2 and breakdown and breakdown.IgniteLightning, "Lightning", dmgTypeFlags.Fire)
output.IgniteLightningMin = min
output.IgniteLightningMax = max
totalMin = totalMin + min
totalMax = totalMax + max
end
if canDeal.Cold and modDB:Sum("FLAG", cfg, "ColdCanIgnite") then
local min, max = calcAilmentSourceDamage(actor, output, dotCfg, pass == 2 and breakdown and breakdown.IgniteCold, "Cold", dmgTypeFlags.Fire)
output.IgniteColdMin = min
output.IgniteColdMax = max
totalMin = totalMin + min
totalMax = totalMax + max
end
if canDeal.Fire and not modDB:Sum("FLAG", cfg, "FireCannotIgnite") then
local min, max = calcAilmentSourceDamage(actor, output, dotCfg, pass == 2 and breakdown and breakdown.IgniteFire, "Fire", 0)
output.IgniteFireMin = min
output.IgniteFireMax = max
totalMin = totalMin + min
totalMax = totalMax + max
end
if canDeal.Chaos and modDB:Sum("FLAG", cfg, "ChaosCanIgnite") then
local min, max = calcAilmentSourceDamage(actor, output, dotCfg, pass == 2 and breakdown and breakdown.IgniteChaos, "Chaos", dmgTypeFlags.Fire)
output.IgniteChaosMin = min
output.IgniteChaosMax = max
totalMin = totalMin + min
totalMax = totalMax + max
end
if pass == 1 then
sourceCritDmg = (totalMin + totalMax) / 2 * output.CritDegenMultiplier
else
sourceHitDmg = (totalMin + totalMax) / 2
end
end
local igniteMode = env.configInput.igniteMode or "AVERAGE"
if igniteMode == "CRIT" then
output.IgniteChanceOnHit = 0
end
if globalBreakdown then
globalBreakdown.IgniteDPS = {
s_format("Ignite mode: %s ^8(can be changed in the Configuration tab)", igniteMode == "CRIT" and "Crit Damage" or "Average Damage")
}
end
local baseVal = calcAilmentDamage("Ignite", sourceHitDmg, sourceCritDmg) * 0.5
if baseVal > 0 then
skillFlags.ignite = true
local effMult = 1
if env.mode_effective then
local resist = m_min(enemyDB:Sum("BASE", nil, "FireResist", "ElementalResist"), 75)
local taken = enemyDB:Sum("INC", dotCfg, "DamageTaken", "DamageTakenOverTime", "FireDamageTaken", "FireDamageTakenOverTime", "ElementalDamageTaken")
effMult = (1 - resist / 100) * (1 + taken / 100)
globalOutput["IgniteEffMult"] = effMult
if breakdown and effMult ~= 1 then
globalBreakdown.IgniteEffMult = breakdown.effMult("Fire", resist, 0, taken, effMult)
end
end
local effectMod = calcLib.mod(modDB, dotCfg, "AilmentEffect")
local burnRateMod = calcLib.mod(modDB, cfg, "IgniteBurnFaster") / calcLib.mod(modDB, cfg, "IgniteBurnSlower")
output.IgniteDPS = baseVal * effectMod * burnRateMod * effMult
local incDur = modDB:Sum("INC", dotCfg, "EnemyIgniteDuration", "SkillAndDamagingAilmentDuration") + enemyDB:Sum("INC", nil, "SelfIgniteDuration")
local moreDur = enemyDB:Sum("MORE", nil, "SelfIgniteDuration")
globalOutput.IgniteDuration = 4 * (1 + incDur / 100) * moreDur / burnRateMod * debuffDurationMult
if skillFlags.igniteCanStack then
output.IgniteDamage = output.IgniteDPS * globalOutput.IgniteDuration
if skillData.showAverage then
output.TotalIgniteAverageDamage = output.HitChance / 100 * output.IgniteChance / 100 * output.IgniteDamage
else
output.TotalIgniteStacks = output.HitChance / 100 * output.IgniteChance / 100 * globalOutput.IgniteDuration * (globalOutput.HitSpeed or globalOutput.Speed) * (skillData.dpsMultiplier or 1)
output.TotalIgniteDPS = output.IgniteDPS * output.TotalIgniteStacks
end
end
if breakdown then
t_insert(breakdown.IgniteDPS, "x 0.5 ^8(ignite deals 50% 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)", burnRateMod },
{ "%.3f ^8(effective DPS modifier)", effMult },
total = s_format("= %.1f ^8per second", output.IgniteDPS),
})
if skillFlags.igniteCanStack then
breakdown.IgniteDamage = { }
if isAttack then
t_insert(breakdown.IgniteDamage, pass.label..":")
end
t_insert(breakdown.IgniteDamage, s_format("%.1f ^8(damage per second)", output.IgniteDPS))
t_insert(breakdown.IgniteDamage, s_format("x %.2fs ^8(ignite duration)", globalOutput.IgniteDuration))
t_insert(breakdown.IgniteDamage, s_format("= %.1f ^8damage per ignite stack", output.IgniteDamage))
if not skillData.showAverage then
breakdown.TotalIgniteStacks = { }
if isAttack then
t_insert(breakdown.TotalIgniteStacks, pass.label..":")
end
breakdown.multiChain(breakdown.TotalIgniteStacks, {
base = s_format("%.2fs ^8(ignite duration)", globalOutput.IgniteDuration),
{ "%.2f ^8(ignite chance)", output.IgniteChance / 100 },
{ "%.2f ^8(hit chance)", output.HitChance / 100 },
{ "%.2f ^8(hits per second)", globalOutput.HitSpeed or globalOutput.Speed },
{ "%g ^8(dps multiplier for this skill)", skillData.dpsMultiplier or 1 },
total = s_format("= %.1f", output.TotalIgniteStacks),
})
end
end
if globalOutput.IgniteDuration ~= 4 then
globalBreakdown.IgniteDuration = {
s_format("4.00s ^8(base duration)", durationBase)
}
if incDur ~= 0 then
t_insert(globalBreakdown.IgniteDuration, s_format("x %.2f ^8(increased/reduced duration)", 1 + incDur/100))
end
if moreDur ~= 1 then
t_insert(globalBreakdown.IgniteDuration, s_format("x %.2f ^8(more/less duration)", moreDur))
end
if burnRateMod ~= 1 then
t_insert(globalBreakdown.IgniteDuration, s_format("/ %.2f ^8(burn rate modifier)", burnRateMod))
end
if debuffDurationMult ~= 1 then
t_insert(globalBreakdown.IgniteDuration, s_format("/ %.2f ^8(debuff expires slower/faster)", 1 / debuffDurationMult))
end
t_insert(globalBreakdown.IgniteDuration, s_format("= %.2fs", globalOutput.IgniteDuration))
end
end
end
end
-- Calculate shock and freeze chance + duration modifier
-- FIXME Completely fucking wrong now
if (output.ShockChanceOnHit + output.ShockChanceOnCrit) > 0 then
local sourceHitDmg = 0
local sourceCritDmg = 0
if canDeal.Physical and modDB:Sum("FLAG", cfg, "PhysicalCanShock") then
sourceHitDmg = sourceHitDmg + output.PhysicalHitAverage
sourceCritDmg = sourceCritDmg + output.PhysicalCritAverage
end
if canDeal.Lightning and not modDB:Sum("FLAG", cfg, "LightningCannotShock") then
sourceHitDmg = sourceHitDmg + output.LightningHitAverage
sourceCritDmg = sourceCritDmg + output.LightningCritAverage
end
if canDeal.Cold and modDB:Sum("FLAG", cfg, "ColdCanShock") then
sourceHitDmg = sourceHitDmg + output.ColdHitAverage
sourceCritDmg = sourceCritDmg + output.ColdCritAverage
end
if canDeal.Fire and modDB:Sum("FLAG", cfg, "FireCanShock") then
sourceHitDmg = sourceHitDmg + output.FireHitAverage
sourceCritDmg = sourceCritDmg + output.FireCritAverage
end
if canDeal.Chaos and modDB:Sum("FLAG", cfg, "ChaosCanShock") then
sourceHitDmg = sourceHitDmg + output.ChaosHitAverage
sourceCritDmg = sourceCritDmg + output.ChaosCritAverage
end
local baseVal = calcAilmentDamage("Shock", sourceHitDmg, sourceCritDmg)
if baseVal > 0 then
skillFlags.shock = true
output.ShockDurationMod = 1 + modDB:Sum("INC", cfg, "EnemyShockDuration") / 100 + enemyDB:Sum("INC", nil, "SelfShockDuration") / 100
if breakdown then
t_insert(breakdown.ShockDPS, s_format("For shock to apply, target must have no more than %d life.", baseVal * 20 * output.ShockDurationMod))
end
end
end
if (output.FreezeChanceOnHit + output.FreezeChanceOnCrit) > 0 then
local sourceHitDmg = 0
local sourceCritDmg = 0
if canDeal.Cold and not modDB:Sum("FLAG", cfg, "ColdCannotFreeze") then
sourceHitDmg = sourceHitDmg + output.ColdHitAverage
sourceCritDmg = sourceCritDmg + output.ColdCritAverage
end
if canDeal.Lightning and modDB:Sum("FLAG", cfg, "LightningCanFreeze") then
sourceHitDmg = sourceHitDmg + output.LightningHitAverage
sourceCritDmg = sourceCritDmg + output.LightningCritAverage
end
local baseVal = calcAilmentDamage("Freeze", sourceHitDmg, sourceCritDmg)
if baseVal > 0 then
skillFlags.freeze = true
output.FreezeDurationMod = 1 + modDB:Sum("INC", cfg, "EnemyFreezeDuration") / 100 + enemyDB:Sum("INC", nil, "SelfFreezeDuration") / 100
if breakdown then
t_insert(breakdown.FreezeDPS, s_format("For freeze to apply, target must have no more than %d life.", baseVal * 20 * output.FreezeDurationMod))
end
end
end
-- Calculate knockback chance/distance
output.KnockbackChance = m_min(100, output.KnockbackChanceOnHit * (1 - output.CritChance / 100) + output.KnockbackChanceOnCrit * output.CritChance / 100 + enemyDB:Sum("BASE", nil, "SelfKnockbackChance"))
if output.KnockbackChance > 0 then
output.KnockbackDistance = round(4 * calcLib.mod(modDB, cfg, "EnemyKnockbackDistance"))
if breakdown then
breakdown.KnockbackDistance = {
radius = output.KnockbackDistance,
}
end
end
-- Calculate enemy stun modifiers
local enemyStunThresholdRed = -modDB:Sum("INC", cfg, "EnemyStunThreshold")
if enemyStunThresholdRed > 75 then
output.EnemyStunThresholdMod = 1 - (75 + (enemyStunThresholdRed - 75) * 25 / (enemyStunThresholdRed - 50)) / 100
else
output.EnemyStunThresholdMod = 1 - enemyStunThresholdRed / 100
end
local base = skillData.baseStunDuration or 0.35
local incDur = modDB:Sum("INC", cfg, "EnemyStunDuration")
local incRecov = enemyDB:Sum("INC", nil, "StunRecovery")
output.EnemyStunDuration = base * (1 + incDur / 100) / (1 + incRecov / 100)
if breakdown then
if output.EnemyStunDuration ~= base then
breakdown.EnemyStunDuration = {
s_format("%.2fs ^8(base duration)", base),
}
if incDur ~= 0 then
t_insert(breakdown.EnemyStunDuration, s_format("x %.2f ^8(increased/reduced stun duration)", 1 + incDur/100))
end
if incRecov ~= 0 then
t_insert(breakdown.EnemyStunDuration, s_format("/ %.2f ^8(increased/reduced enemy stun recovery)", 1 + incRecov/100))
end
t_insert(breakdown.EnemyStunDuration, s_format("= %.2fs", output.EnemyStunDuration))
end
end
end
-- Combine secondary effect stats
if isAttack then
combineStat("BleedChance", "AVERAGE")
combineStat("BleedDPS", "CHANCE", "BleedChance")
combineStat("PoisonChance", "AVERAGE")
combineStat("PoisonDPS", "CHANCE", "PoisonChance")
combineStat("PoisonDamage", "CHANCE", "PoisonChance")
if skillData.showAverage then
combineStat("TotalPoisonAverageDamage", "DPS")
else
combineStat("TotalPoisonStacks", "DPS")
combineStat("TotalPoisonDPS", "DPS")
end
combineStat("IgniteChance", "AVERAGE")
combineStat("IgniteDPS", "CHANCE", "IgniteChance")
if skillFlags.igniteCanStack then
combineStat("IgniteDamage", "CHANCE", "IgniteChance")
if skillData.showAverage then
combineStat("TotalIgniteAverageDamage", "DPS")
else
combineStat("TotalIgniteStacks", "DPS")
combineStat("TotalIgniteDPS", "DPS")
end
end
combineStat("ShockChance", "AVERAGE")
combineStat("ShockDurationMod", "AVERAGE")
combineStat("FreezeChance", "AVERAGE")
combineStat("FreezeDurationMod", "AVERAGE")
end
if skillFlags.hit and skillData.decay then
-- Calculate DPS for Essence of Delirium's Decay effect
skillFlags.decay = true
mainSkill.decayCfg = {
skillName = skillCfg.skillName,
skillPart = skillCfg.skillPart,
skillTypes = skillCfg.skillTypes,
slotName = skillCfg.slotName,
flags = ModFlag.Dot,
keywordFlags = band(skillCfg.keywordFlags, bnot(KeywordFlag.Hit)),
}
local dotCfg = mainSkill.decayCfg
local effMult = 1
if env.mode_effective then
local resist = m_min(enemyDB:Sum("BASE", nil, "ChaosResist"), 75)
local taken = enemyDB:Sum("INC", nil, "DamageTaken", "DamageTakenOverTime", "ChaosDamageTaken", "ChaosDamageTakenOverTime")
effMult = (1 - resist / 100) * (1 + taken / 100)
output["DecayEffMult"] = effMult
if breakdown and effMult ~= 1 then
breakdown.DecayEffMult = breakdown.effMult("Chaos", resist, 0, taken, effMult)
end
end
local inc = modDB:Sum("INC", dotCfg, "Damage", "ChaosDamage")
local more = round(modDB:Sum("MORE", dotCfg, "Damage", "ChaosDamage"), 2)
output.DecayDPS = skillData.decay * (1 + inc/100) * more * effMult
local durationMod = calcLib.mod(modDB, dotCfg, "Duration", "SkillAndDamagingAilmentDuration")
output.DecayDuration = 10 * durationMod * debuffDurationMult
if breakdown then
breakdown.DecayDPS = { }
t_insert(breakdown.DecayDPS, "Decay DPS:")
breakdown.dot(breakdown.DecayDPS, skillData.decay, inc, more, nil, effMult, output.DecayDPS)
if output.DecayDuration ~= 2 then
breakdown.DecayDuration = {
s_format("%.2fs ^8(base duration)", 10)
}
if durationMod ~= 1 then
t_insert(breakdown.DecayDuration, s_format("x %.2f ^8(duration modifier)", durationMod))
end
if debuffDurationMult ~= 1 then
t_insert(breakdown.DecayDuration, s_format("/ %.2f ^8(debuff expires slower/faster)", 1 / debuffDurationMult))
end
t_insert(breakdown.DecayDuration, s_format("= %.2fs", output.DecayDuration))
end
end
end
-- Calculate combined DPS estimate, including DoTs
local baseDPS = output[(skillData.showAverage and "AverageDamage") or "TotalDPS"] + output.TotalDot
output.CombinedDPS = baseDPS
if skillData.showAverage then
output.CombinedDPS = output.CombinedDPS + (output.TotalPoisonAverageDamage or 0)
output.WithPoisonAverageDamage = baseDPS + (output.TotalPoisonAverageDamage or 0)
else
output.CombinedDPS = output.CombinedDPS + (output.TotalPoisonDPS or 0)
output.WithPoisonDPS = baseDPS + (output.TotalPoisonDPS or 0)
end
if skillFlags.ignite then
if skillFlags.igniteCanStack then
if skillData.showAverage then
output.CombinedDPS = output.CombinedDPS + output.TotalIgniteAverageDamage
output.WithIgniteAverageDamage = baseDPS + output.TotalIgniteAverageDamage
else
output.CombinedDPS = output.CombinedDPS + output.TotalIgniteDPS
output.WithIgniteDPS = baseDPS + output.TotalIgniteDPS
end
else
output.CombinedDPS = output.CombinedDPS + output.IgniteDPS
end
end
if skillFlags.bleed then
output.CombinedDPS = output.CombinedDPS + output.BleedDPS
end
if skillFlags.decay then
output.CombinedDPS = output.CombinedDPS + output.DecayDPS
end
end