Files
PathOfBuilding/Modules/CalcOffence-3_0.lua
Jack Lockwood 6e1e41ea34 Close combat and multi strike correct damage values
Adds pantheons from temmings' pull request
Adds partial Impale support from baranio's pull request
Adds updated uniques from PJacek's pull request and my own
Adds more tree highlighting options for node power from coldino's pull request
Adds support for fossil mods in the crafting window. Including correct parsing for some mods that previously didn't work
2019-09-04 21:53:06 +10:00

2180 lines
98 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(activeSkill, output, cfg, breakdown, damageType, typeFlags, convDst)
local skillModList = activeSkill.skillModList
typeFlags = bor(typeFlags, dmgTypeFlags[damageType])
-- Calculate conversions
local addMin, addMax = 0, 0
local conversionTable = activeSkill.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(activeSkill, 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 + skillModList:Sum("INC", cfg, unpack(modNames)) / 100
local more = m_floor(skillModList: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(activeSkill, output, cfg, breakdown, damageType, typeFlags)
local min, max = calcDamage(activeSkill, output, cfg, breakdown, damageType, typeFlags)
local convMult = activeSkill.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, activeSkill)
local enemyDB = actor.enemy.modDB
local output = actor.output
local breakdown = actor.breakdown
local skillModList = activeSkill.skillModList
local skillData = activeSkill.skillData
local skillFlags = activeSkill.skillFlags
local skillCfg = activeSkill.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
-- Update skill data
for _, value in ipairs(skillModList: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 skillModList:Flag(nil, "IronGrip") then
skillModList:NewMod("PhysicalDamage", "INC", actor.strDmgBonus, "Strength", bor(ModFlag.Attack, ModFlag.Projectile))
end
if skillModList:Flag(nil, "IronWill") then
skillModList:NewMod("Damage", "INC", actor.strDmgBonus, "Strength", ModFlag.Spell)
end
if skillModList:Flag(nil, "MinionDamageAppliesToPlayer") then
-- Minion Damage conversion from The Scourge
for _, value in ipairs(skillModList:List(skillCfg, "MinionModifier")) do
if value.mod.name == "Damage" and value.mod.type == "INC" then
skillModList:AddMod(value.mod)
end
end
end
if skillModList:Flag(nil, "MinionAttackSpeedAppliesToPlayer") then
-- Minion Attack Speed conversion from Spiritual Command
for _, value in ipairs(skillModList:List(skillCfg, "MinionModifier")) do
if value.mod.name == "Speed" and value.mod.type == "INC" and (value.mod.flags == 0 or band(value.mod.flags, ModFlag.Attack) ~= 0) then
skillModList:NewMod("Speed", "INC", value.mod.value, value.mod.source, ModFlag.Attack, value.mod.keywordFlags, unpack(value.mod))
end
end
end
if skillModList:Flag(nil, "SpellDamageAppliesToAttacks") then
-- Spell Damage conversion from Crown of Eyes
for i, value in ipairs(skillModList:Tabulate("INC", { flags = ModFlag.Spell }, "Damage")) do
local mod = value.mod
if band(mod.flags, ModFlag.Spell) ~= 0 then
skillModList:NewMod("Damage", "INC", mod.value, mod.source, bor(band(mod.flags, bnot(ModFlag.Spell)), ModFlag.Attack), mod.keywordFlags, unpack(mod))
end
end
end
if skillModList:Flag(nil, "ClawDamageAppliesToUnarmed") then
-- Claw Damage conversion from Rigwald's Curse
for i, value in ipairs(skillModList:Tabulate("INC", { flags = ModFlag.Claw }, "Damage")) do
local mod = value.mod
if band(mod.flags, ModFlag.Claw) ~= 0 then
skillModList:NewMod("Damage", mod.type, mod.value, mod.source, bor(band(mod.flags, bnot(ModFlag.Claw)), ModFlag.Unarmed), mod.keywordFlags, unpack(mod))
end
end
end
if skillModList:Flag(nil, "ClawAttackSpeedAppliesToUnarmed") then
-- Claw Attack Speed conversion from Rigwald's Curse
for i, value in ipairs(skillModList:Tabulate("INC", { flags = bor(ModFlag.Claw, ModFlag.Attack) }, "Speed")) do
local mod = value.mod
if band(mod.flags, ModFlag.Claw) ~= 0 and band(mod.flags, ModFlag.Attack) ~= 0 then
skillModList: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 skillModList:Flag(nil, "ClawCritChanceAppliesToUnarmed") then
-- Claw Crit Chance conversion from Rigwald's Curse
for i, value in ipairs(skillModList:Tabulate("INC", { flags = ModFlag.Claw }, "CritChance")) do
local mod = value.mod
if band(mod.flags, ModFlag.Claw) ~= 0 then
skillModList: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 skillModList:Flag(nil, "LightRadiusAppliesToAccuracy") then
-- Light Radius conversion from Corona Solaris
for i, value in ipairs(skillModList:Tabulate("INC", { }, "LightRadius")) do
local mod = value.mod
skillModList:NewMod("Accuracy", "INC", mod.value, mod.source, mod.flags, mod.keywordFlags, unpack(mod))
end
end
if skillModList:Flag(nil, "LightRadiusAppliesToAreaOfEffect") then
-- Light Radius conversion from Wreath of Phrecia
for i, value in ipairs(skillModList:Tabulate("INC", { }, "LightRadius")) do
local mod = value.mod
skillModList:NewMod("AreaOfEffect", "INC", math.floor(mod.value / 2), mod.source, mod.flags, mod.keywordFlags, unpack(mod))
end
end
if skillModList:Flag(nil, "LightRadiusAppliesToDamage") then
-- Light Radius conversion from Wreath of Phrecia
for i, value in ipairs(skillModList:Tabulate("INC", { }, "LightRadius")) do
local mod = value.mod
skillModList:NewMod("Damage", "INC", mod.value, mod.source, mod.flags, mod.keywordFlags, unpack(mod))
end
end
if skillModList:Flag(nil, "CastSpeedAppliesToTrapThrowingSpeed") then
-- Cast Speed conversion from Slavedriver's Hand
for i, value in ipairs(skillModList:Tabulate("INC", { flags = ModFlag.Cast }, "Speed")) do
local mod = value.mod
if (mod.flags == 0 or band(mod.flags, ModFlag.Cast) ~= 0) then
skillModList: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 activeSkill.minion and activeSkill.minion.minionData.limit then
output.ActiveMinionLimit = m_floor(calcLib.val(skillModList, activeSkill.minion.minionData.limit, skillCfg))
end
end
if skillFlags.chaining then
if skillModList:Flag(skillCfg, "CannotChain") then
output.ChainMaxString = "Cannot chain"
else
output.ChainMax = skillModList:Sum("BASE", skillCfg, "ChainCountMax")
output.ChainMaxString = output.ChainMax
output.Chain = m_min(output.ChainMax, skillModList:Sum("BASE", skillCfg, "ChainCount"))
output.ChainRemaining = m_max(0, output.ChainMax - output.Chain)
end
end
if skillFlags.projectile then
if skillModList:Flag(nil, "PointBlank") then
skillModList:NewMod("Damage", "MORE", 50, "Point Blank", bor(ModFlag.Attack, ModFlag.Projectile), { type = "DistanceRamp", ramp = {{10,1},{35,0},{150,-1}} })
end
if skillModList:Flag(nil, "FarShot") then
skillModList:NewMod("Damage", "MORE", 30, "Far Shot", bor(ModFlag.Attack, ModFlag.Projectile), { type = "DistanceRamp", ramp = {{35,0},{70,1}} })
end
local projBase = skillModList:Sum("BASE", skillCfg, "ProjectileCount")
local projMore = skillModList:More(skillCfg, "ProjectileCount")
output.ProjectileCount = round((projBase - 1) * projMore + 1)
if skillModList:Flag(skillCfg, "CannotPierce") then
output.PierceCountString = "Cannot pierce"
else
if skillModList:Flag(skillCfg, "PierceAllTargets") or enemyDB:Flag(nil, "AlwaysPierceSelf") then
output.PierceCount = 100
output.PierceCountString = "All targets"
else
output.PierceCount = skillModList:Sum("BASE", skillCfg, "PierceCount")
output.PierceCountString = output.PierceCount
end
end
output.ProjectileSpeedMod = calcLib.mod(skillModList, 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 + skillModList:Sum("BASE", skillCfg, "MeleeWeaponRange")) or (6 + skillModList:Sum("BASE", skillCfg, "UnarmedRange"))
end
if skillFlags.weapon2Attack then
actor.weaponRange2 = (actor.weaponData2.range and actor.weaponData2.range + skillModList:Sum("BASE", skillCfg, "MeleeWeaponRange")) or (6 + skillModList:Sum("BASE", skillCfg, "UnarmedRange"))
end
if activeSkill.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(skillModList, 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) + skillModList: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(skillModList, 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 / skillModList:Sum("BASE", skillCfg, "TrapThrowingTime")
output.TrapThrowingSpeed = baseSpeed * calcLib.mod(skillModList, 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 + skillModList:Sum("INC", skillCfg, "TrapThrowingSpeed") / 100 },
{ "%.2f ^8(more/less throwing speed)", skillModList:More(skillCfg, "TrapThrowingSpeed") },
{ "%.2f ^8(action speed modifier)", output.ActionSpeedMod },
total = s_format("= %.2f ^8per second", output.TrapThrowingSpeed),
})
end
output.ActiveTrapLimit = skillModList:Sum("BASE", skillCfg, "ActiveTrapLimit")
local baseCooldown = skillData.trapCooldown or skillData.cooldown
if baseCooldown then
output.TrapCooldown = baseCooldown / calcLib.mod(skillModList, 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 + skillModList:Sum("INC", skillCfg, "CooldownRecovery") / 100),
s_format("= %.2fs", output.TrapCooldown)
}
end
end
local areaMod = calcLib.mod(skillModList, skillCfg, "TrapTriggerAreaOfEffect")
output.TrapTriggerRadius = 10 * m_sqrt(areaMod)
if breakdown then
breakdown.TrapTriggerRadius = breakdown.area(10, areaMod, output.TrapTriggerRadius)
end
elseif skillData.cooldown then
local cooldownOverride = skillModList:Override(skillCfg, "CooldownRecovery")
output.Cooldown = cooldownOverride or skillData.cooldown / calcLib.mod(skillModList, skillCfg, "CooldownRecovery")
if breakdown then
breakdown.Cooldown = {
s_format("%.2fs ^8(base)", skillData.cooldown),
s_format("/ %.2f ^8(increased/reduced cooldown recovery)", 1 + skillModList:Sum("INC", skillCfg, "CooldownRecovery") / 100),
s_format("= %.2fs", output.Cooldown)
}
end
end
if skillFlags.mine then
local baseSpeed = 1 / skillModList:Sum("BASE", skillCfg, "MineLayingTime")
output.MineLayingSpeed = baseSpeed * calcLib.mod(skillModList, 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 + skillModList:Sum("INC", skillCfg, "MineLayingSpeed") / 100 },
{ "%.2f ^8(more/less laying speed)", skillModList:More(skillCfg, "MineLayingSpeed") },
{ "%.2f ^8(action speed modifier)", output.ActionSpeedMod },
total = s_format("= %.2f ^8per second", output.MineLayingSpeed),
})
end
output.ActiveMineLimit = skillModList:Sum("BASE", skillCfg, "ActiveMineLimit")
local areaMod = calcLib.mod(skillModList, 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 / skillModList:Sum("BASE", skillCfg, "TotemPlacementTime")
output.TotemPlacementSpeed = baseSpeed * calcLib.mod(skillModList, 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 + skillModList:Sum("INC", skillCfg, "TotemPlacementSpeed") / 100 },
{ "%.2f ^8(more/less placement speed)", skillModList:More(skillCfg, "TotemPlacementSpeed") },
{ "%.2f ^8(action speed modifier)", output.ActionSpeedMod },
total = s_format("= %.2f ^8per second", output.TotemPlacementSpeed),
})
end
output.ActiveTotemLimit = skillModList:Sum("BASE", skillCfg, "ActiveTotemLimit")
output.TotemLifeMod = calcLib.mod(skillModList, skillCfg, "TotemLife")
output.TotemLife = round(m_floor(env.data.monsterAllyLifeTable[skillData.totemLevel] * env.data.totemLifeMult[activeSkill.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[activeSkill.skillTotemId].." ^8(life multiplier for this totem type)",
"x "..output.TotemLifeMod.." ^8(totem life modifier)",
"= "..output.TotemLife,
}
end
end
-- Run skill setup function
do
local setupFunc = activeSkill.activeEffect.grantedEffect.setupFunc
if setupFunc then
setupFunc(activeSkill, output)
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(skillModList, skillCfg, "Duration", "PrimaryDuration", "SkillAndDamagingAilmentDuration")
if breakdown then
breakdown.DurationMod = breakdown.mod(skillCfg, "Duration", "PrimaryDuration", "SkillAndDamagingAilmentDuration")
end
local durationBase = (skillData.duration or 0) + skillModList: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) + skillModList:Sum("BASE", skillCfg, "Duration", "SecondaryDuration")
if durationBase > 0 then
local durationMod = calcLib.mod(skillModList, 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
durationBase = (skillData.auraDuration or 0)
if durationBase > 0 then
local durationMod = calcLib.mod(skillModList, skillCfg, "Duration", "SkillAndDamagingAilmentDuration")
output.AuraDuration = durationBase * durationMod
if breakdown and output.AuraDuration ~= durationBase then
breakdown.AuraDuration = {
s_format("%.2fs ^8(base)", durationBase),
s_format("x %.2f ^8(duration modifier)", durationMod),
s_format("= %.2fs", output.AuraDuration),
}
end
end
durationBase = (skillData.reserveDuration or 0)
if durationBase > 0 then
local durationMod = calcLib.mod(skillModList, skillCfg, "Duration", "SkillAndDamagingAilmentDuration")
output.ReserveDuration = durationBase * durationMod
if breakdown and output.ReserveDuration ~= durationBase then
breakdown.ReserveDuration = {
s_format("%.2fs ^8(base)", durationBase),
s_format("x %.2f ^8(duration modifier)", durationMod),
s_format("= %.2fs", output.ReserveDuration),
}
end
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 skillModList:Flag(skillCfg, "DealNo"..damageType)
end
-- Calculate damage conversion percentages
activeSkill.conversionTable = wipeTable(activeSkill.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] = skillModList: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] = skillModList:Sum("BASE", skillCfg, "Skill"..damageType.."DamageConvertTo"..otherType)
skillTotal = skillTotal + skillConv[otherType]
add[otherType] = skillModList: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)
activeSkill.conversionTable[damageType] = dmgTable
end
activeSkill.conversionTable["Chaos"] = { mult = 1 }
-- Calculate mana cost (may be slightly off due to rounding differences)
do
local more = m_floor(skillModList:More(skillCfg, "ManaCost") * 100 + 0.0001) / 100
local inc = skillModList:Sum("INC", skillCfg, "ManaCost")
local base = skillModList:Sum("BASE", skillCfg, "ManaCost")
local manaCost = activeSkill.activeEffect.grantedEffectLevel.manaCost or 0
output.ManaCost = m_floor(m_max(0, manaCost * more * (1 + inc / 100) + base))
if activeSkill.skillTypes[SkillType.ManaCostPercent] and skillFlags.totem then
output.ManaCost = m_floor(output.Mana * output.ManaCost / 100)
end
if breakdown and output.ManaCost ~= manaCost then
breakdown.ManaCost = {
s_format("%d ^8(base mana cost)", manaCost)
}
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 = { }
local critOverride = skillModList:Override(cfg, "WeaponBaseCritChance")
if skillFlags.weapon1Attack then
if breakdown then
breakdown.MainHand = LoadModule(calcs.breakdownModule, skillModList, output.MainHand)
end
activeSkill.weapon1Cfg.skillStats = output.MainHand
local source = copyTable(actor.weaponData1)
if critOverride and source.type and source.type ~= "None" then
source.CritChance = critOverride
end
t_insert(passList, {
label = "Main Hand",
source = source,
cfg = activeSkill.weapon1Cfg,
output = output.MainHand,
breakdown = breakdown and breakdown.MainHand,
})
end
if skillFlags.weapon2Attack then
if breakdown then
breakdown.OffHand = LoadModule(calcs.breakdownModule, skillModList, output.OffHand)
end
activeSkill.weapon2Cfg.skillStats = output.OffHand
local source = copyTable(actor.weaponData2)
if critOverride and source.type and source.type ~= "None" then
source.CritChance = critOverride
end
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 = activeSkill.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(skillModList, "Accuracy", cfg)
if breakdown then
breakdown.Accuracy = breakdown.simple(nil, cfg, output.Accuracy, "Accuracy")
end
if not isAttack or skillModList:Flag(cfg, "CannotBeEvaded") or skillData.cannotBeEvaded or (env.mode_effective and enemyDB:Flag(nil, "CannotEvade")) then
output.HitChance = 100
else
local enemyEvasion = round(calcLib.val(enemyDB, "Evasion"))
output.HitChance = calcs.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 activeSkill.activeEffect.grantedEffect.castTime == 0 and not skillData.castTimeOverride then
output.Time = 0
output.Speed = 0
elseif skillData.timeOverride then
output.Time = skillData.timeOverride
output.Speed = 1 / output.Time
elseif skillData.fixedCastTime then
output.Time = activeSkill.activeEffect.grantedEffect.castTime
output.Speed = 1 / output.Time
else
local baseTime
if isAttack then
if skillData.castTimeOverridesAttackTime then
-- Skill is overriding weapon attack speed
baseTime = activeSkill.activeEffect.grantedEffect.castTime / (1 + (source.AttackSpeedInc or 0) / 100)
else
baseTime = 1 / ( source.AttackRate or 1 ) + skillModList:Sum("BASE", cfg, "Speed")
end
else
baseTime = skillData.castTimeOverride or activeSkill.activeEffect.grantedEffect.castTime or 1
end
local inc = skillModList:Sum("INC", cfg, "Speed")
local more = skillModList:More(cfg, "Speed")
output.Speed = 1 / baseTime * 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)", 1 / baseTime),
{ "%.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")
combineStat("HitSpeed", "OR")
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 skillModList: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 = skillModList:Sum("BASE", cfg, "CritChance")
local inc = skillModList:Sum("INC", cfg, "CritChance") + (env.mode_effective and enemyDB:Sum("INC", nil, "SelfCritChance") or 0)
local more = skillModList:More(cfg, "CritChance")
output.CritChance = (baseCrit + base) * (1 + inc / 100) * more
local preCapCritChance = output.CritChance
output.CritChance = m_min(output.CritChance, 100)
if (baseCrit + base) > 0 then
output.CritChance = m_max(output.CritChance, 0)
end
output.PreEffectiveCritChance = output.CritChance
local preLuckyCritChance = output.CritChance
if env.mode_effective and skillModList: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 > 100 then
local overCap = preCapCritChance - 100
t_insert(breakdown.CritChance, s_format("Crit is overcapped by %.2f%% (%d%% increased Critical Strike Chance)", overCap, overCap / more / (baseCrit + base) * 100))
end
if env.mode_effective and skillModList: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 skillModList:Flag(cfg, "NoCritMultiplier") then
output.CritMultiplier = 1
else
local extraDamage = skillModList:Sum("BASE", cfg, "CritMultiplier") / 100
local multiOverride = skillModList:Override(skillCfg, "CritMultiplier")
if multiOverride then
extraDamage = (multiOverride - 100) / 100
end
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)", skillModList: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 skillModList:Flag(cfg, "NoCritDegenMultiplier") then
output.CritDegenMultiplier = 1
else
output.CritDegenMultiplier = 1 + skillModList:Sum("BASE", cfg, "CritDegenMultiplier") / 100 + (skillModList:Sum("BASE", cfg, "CritMultiplier") - 50) * skillModList: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(skillModList:Sum("BASE", cfg, "DoubleDamageChance"), 100)
output.DoubleDamageEffect = 1 + output.DoubleDamageChance / 100
output.RuthlessBlowMaxCount = skillModList:Sum("BASE", cfg, "RuthlessBlowMaxCount")
if output.RuthlessBlowMaxCount > 0 then
output.RuthlessBlowChance = round(100 / output.RuthlessBlowMaxCount)
else
output.RuthlessBlowChance = 0
end
output.RuthlessBlowMultiplier = 1 + skillModList: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 = activeSkill.activeEffect.grantedEffectLevel.baseMultiplier or 1
local damageEffectiveness = activeSkill.activeEffect.grantedEffectLevel.damageEffectiveness or skillData.damageEffectiveness or 1
local addedMin = skillModList:Sum("BASE", cfg, damageTypeMin)
local addedMax = skillModList: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
local ghostReaver = skillModList:Flag(nil, "GhostReaver")
output.LifeLeech = 0
output.LifeLeechInstant = 0
output.EnergyShieldLeech = 0
output.EnergyShieldLeechInstant = 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 energyShieldLeechTotal = 0
local manaLeechTotal = 0
local noLifeLeech = skillModList:Flag(cfg, "CannotLeechLife") or enemyDB:Flag(nil, "CannotLeechLifeFromSelf")
local noEnergyShieldLeech = skillModList:Flag(cfg, "CannotLeechEnergyShield") or enemyDB:Flag(nil, "CannotLeechEnergyShieldFromSelf")
local noManaLeech = skillModList:Flag(cfg, "CannotLeechMana") or enemyDB:Flag(nil, "CannotLeechManaFromSelf")
for _, damageType in ipairs(dmgTypeList) do
local min, max
if skillFlags.hit and canDeal[damageType] then
min, max = calcDamage(activeSkill, output, cfg, pass == 2 and breakdown and breakdown[damageType], damageType, 0)
local convMult = activeSkill.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 = skillModList:Sum("BASE", cfg, damageType.."Penetration", "ElementalPenetration")
taken = taken + enemyDB:Sum("INC", nil, "ElementalDamageTaken")
elseif damageType == "Chaos" then
pen = skillModList:Sum("BASE", cfg, "ChaosPenetration")
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 skillModList:Flag(cfg, "Ignore"..damageType.."Resistance", isElemental[damageType] and "IgnoreElementalResistances" or nil) and not enemyDB: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 = skillModList: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 skillModList:Flag(nil, "LifeLeechBasedOnChaosDamage") then
if damageType == "Chaos" then
lifeLeech = skillModList:Sum("BASE", cfg, "DamageLeech", "DamageLifeLeech", "PhysicalDamageLifeLeech", "LightningDamageLifeLeech", "ColdDamageLifeLeech", "FireDamageLifeLeech", "ChaosDamageLifeLeech", "ElementalDamageLifeLeech") + enemyDB:Sum("BASE", cfg, "SelfDamageLifeLeech") / 100
else
lifeLeech = 0
end
else
lifeLeech = skillModList:Sum("BASE", cfg, "DamageLeech", "DamageLifeLeech", damageType.."DamageLifeLeech", isElemental[damageType] and "ElementalDamageLifeLeech" or nil) + enemyDB:Sum("BASE", cfg, "SelfDamageLifeLeech") / 100
end
if lifeLeech > 0 then
lifeLeechTotal = lifeLeechTotal + (min + max) / 2 * lifeLeech / 100
end
end
if not noEnergyShieldLeech then
local energyShieldLeech = skillModList:Sum("BASE", cfg, "DamageEnergyShieldLeech", damageType.."DamageEnergyShieldLeech", isElemental[damageType] and "ElementalDamageEnergyShieldLeech" or nil) + enemyDB:Sum("BASE", cfg, "SelfDamageEnergyShieldLeech") / 100
if energyShieldLeech > 0 then
energyShieldLeechTotal = energyShieldLeechTotal + (min + max) / 2 * energyShieldLeech / 100
end
end
if not noManaLeech then
local manaLeech = skillModList:Sum("BASE", cfg, "DamageLeech", "DamageManaLeech", damageType.."DamageManaLeech", isElemental[damageType] and "ElementalDamageManaLeech" or nil) + enemyDB:Sum("BASE", cfg, "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 skillModList:Flag(cfg, "InstantLifeLeech") and not ghostReaver then
output.LifeLeechInstant = output.LifeLeechInstant + lifeLeechTotal * portion
else
output.LifeLeech = output.LifeLeech + lifeLeechTotal * portion
end
if skillModList:Flag(cfg, "InstantEnergyShieldLeech") then
output.EnergyShieldLeechInstant = output.EnergyShieldLeechInstant + energyShieldLeechTotal * portion
else
output.EnergyShieldLeech = output.EnergyShieldLeech + energyShieldLeechTotal * portion
end
if skillModList: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 skillModList: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
if ghostReaver then
output.EnergyShieldLeech = output.EnergyShieldLeech + output.LifeLeech
output.EnergyShieldLeechInstant = output.EnergyShieldLeechInstant + output.LifeLeechInstant
output.LifeLeech = 0
output.LifeLeechInstant = 0
end
output.LifeLeech = m_min(output.LifeLeech, globalOutput.MaxLifeLeechInstance)
output.LifeLeechDuration, output.LifeLeechInstances = getLeechInstances(output.LifeLeech, globalOutput.Life)
output.LifeLeechInstantRate = output.LifeLeechInstant * hitRate
output.EnergyShieldLeech = m_min(output.EnergyShieldLeech, globalOutput.MaxEnergyShieldLeechInstance)
output.EnergyShieldLeechDuration, output.EnergyShieldLeechInstances = getLeechInstances(output.EnergyShieldLeech, globalOutput.EnergyShield)
output.EnergyShieldLeechInstantRate = output.EnergyShieldLeechInstant * hitRate
output.ManaLeech = m_min(output.ManaLeech, globalOutput.MaxManaLeechInstance)
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 = (skillModList:Sum("BASE", cfg, "LifeOnHit") + enemyDB:Sum("BASE", cfg, "SelfLifeOnHit")) * globalOutput.LifeRecoveryMod
output.EnergyShieldOnHit = (skillModList:Sum("BASE", cfg, "EnergyShieldOnHit") + enemyDB:Sum("BASE", cfg, "SelfEnergyShieldOnHit")) * globalOutput.EnergyShieldRecoveryMod
output.ManaOnHit = (skillModList:Sum("BASE", cfg, "ManaOnHit") + enemyDB:Sum("BASE", cfg, "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("EnergyShieldLeechDuration", "DPS")
combineStat("EnergyShieldLeechInstances", "DPS")
combineStat("EnergyShieldLeechInstant", "DPS")
combineStat("EnergyShieldLeechInstantRate", "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
output.LifeLeechInstanceRate = output.Life * 0.02 * calcLib.mod(skillModList, 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.EnergyShieldLeechInstanceRate = output.EnergyShield * 0.02 * calcLib.mod(skillModList, skillCfg, "EnergyShieldLeechRate")
output.EnergyShieldLeechRate = output.EnergyShieldLeechInstantRate * output.EnergyShieldRecoveryMod + m_min(output.EnergyShieldLeechInstances * output.EnergyShieldLeechInstanceRate, output.MaxEnergyShieldLeechRate) * output.EnergyShieldRecoveryRateMod
output.EnergyShieldLeechPerHit = output.EnergyShieldLeechInstant * output.EnergyShieldRecoveryMod + m_min(output.EnergyShieldLeechInstanceRate, output.MaxEnergyShieldLeechRate) * output.EnergyShieldLeechDuration * output.EnergyShieldRecoveryRateMod
output.ManaLeechInstanceRate = output.Mana * 0.02 * calcLib.mod(skillModList, 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
skillFlags.leechLife = output.LifeLeechRate > 0
skillFlags.leechES = output.EnergyShieldLeechRate > 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.EnergyShieldLeechInstant, output.EnergyShieldLeechInstantRate, output.EnergyShieldLeechInstances, output.EnergyShield, "EnergyShieldLeechRate", output.MaxEnergyShieldLeechRate, output.EnergyShieldLeechDuration)
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)),
}
activeSkill.dotCfg = dotCfg
output.TotalDot = 0
for _, damageType in ipairs(dmgTypeList) do
local dotTypeCfg = copyTable(dotCfg, true)
dotTypeCfg.keywordFlags = bor(dotTypeCfg.keywordFlags, KeywordFlag[damageType.."Dot"])
activeSkill["dot"..damageType.."Cfg"] = dotTypeCfg
local baseVal
if canDeal[damageType] then
baseVal = skillData[damageType.."Dot"] or 0
else
baseVal = 0
end
if baseVal > 0 then
skillFlags.dot = true
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 = skillModList:Sum("INC", dotTypeCfg, "Damage", damageType.."Damage", isElemental[damageType] and "ElementalDamage" or nil)
local more = round(skillModList:More(dotTypeCfg, "Damage", damageType.."Damage", isElemental[damageType] and "ElementalDamage" or nil), 2)
local mult = skillModList:Sum("BASE", dotTypeCfg, damageType.."DotMultiplier")
local total = baseVal * (1 + inc/100) * more * (1 + mult/100) * effMult
if skillFlags.aura then
total = total * calcLib.mod(skillModList, dotTypeCfg, "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, mult, nil, effMult, total)
end
end
end
skillFlags.bleed = false
skillFlags.poison = false
skillFlags.ignite = false
skillFlags.igniteCanStack = skillModList:Flag(skillCfg, "IgniteCanStack")
skillFlags.shock = false
skillFlags.freeze = false
skillFlags.impale = 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 skillModList:Flag(cfg, "CannotBleed") then
output.BleedChanceOnCrit = 0
else
output.BleedChanceOnCrit = m_min(100, skillModList:Sum("BASE", cfg, "BleedChance") + enemyDB:Sum("BASE", nil, "SelfBleedChance"))
end
if not skillFlags.hit or skillModList:Flag(cfg, "CannotPoison") then
output.PoisonChanceOnCrit = 0
else
output.PoisonChanceOnCrit = m_min(100, skillModList:Sum("BASE", cfg, "PoisonChance") + enemyDB:Sum("BASE", nil, "SelfPoisonChance"))
end
if not skillFlags.hit or skillModList:Flag(cfg, "CannotIgnite") then
output.IgniteChanceOnCrit = 0
else
output.IgniteChanceOnCrit = 100
end
if not skillFlags.hit or skillModList:Flag(cfg, "CannotShock") then
output.ShockChanceOnCrit = 0
else
output.ShockChanceOnCrit = 100
end
if not skillFlags.hit or skillModList:Flag(cfg, "CannotFreeze") then
output.FreezeChanceOnCrit = 0
else
output.FreezeChanceOnCrit = 100
end
if not skillFlags.hit or skillModList:Flag(cfg, "CannotKnockback") then
output.KnockbackChanceOnCrit = 0
else
output.KnockbackChanceOnCrit = skillModList:Sum("BASE", cfg, "EnemyKnockbackChance")
end
cfg.skillCond["CriticalStrike"] = false
if not skillFlags.attack or skillModList:Flag(cfg, "CannotBleed") then
output.BleedChanceOnHit = 0
else
output.BleedChanceOnHit = m_min(100, skillModList:Sum("BASE", cfg, "BleedChance") + enemyDB:Sum("BASE", nil, "SelfBleedChance"))
end
if not skillFlags.hit or skillModList:Flag(cfg, "CannotPoison") then
output.PoisonChanceOnHit = 0
output.ChaosPoisonChance = 0
else
output.PoisonChanceOnHit = m_min(100, skillModList:Sum("BASE", cfg, "PoisonChance") + enemyDB:Sum("BASE", nil, "SelfPoisonChance"))
output.ChaosPoisonChance = m_min(100, skillModList:Sum("BASE", cfg, "ChaosPoisonChance"))
end
if not skillFlags.hit or skillModList:Flag(cfg, "CannotIgnite") then
output.IgniteChanceOnHit = 0
else
output.IgniteChanceOnHit = m_min(100, skillModList:Sum("BASE", cfg, "EnemyIgniteChance") + enemyDB:Sum("BASE", nil, "SelfIgniteChance"))
end
if not skillFlags.hit or skillModList:Flag(cfg, "CannotShock") then
output.ShockChanceOnHit = 0
else
output.ShockChanceOnHit = m_min(100, skillModList:Sum("BASE", cfg, "EnemyShockChance") + enemyDB:Sum("BASE", nil, "SelfShockChance"))
end
if not skillFlags.hit or skillModList:Flag(cfg, "CannotFreeze") then
output.FreezeChanceOnHit = 0
else
output.FreezeChanceOnHit = m_min(100, skillModList:Sum("BASE", cfg, "EnemyFreezeChance") + enemyDB:Sum("BASE", nil, "SelfFreezeChance"))
if skillModList:Flag(cfg, "CritsDontAlwaysFreeze") then
output.FreezeChanceOnCrit = output.FreezeChanceOnHit
end
end
if not skillFlags.hit or skillModList:Flag(cfg, "CannotKnockback") then
output.KnockbackChanceOnHit = 0
else
output.KnockbackChanceOnHit = skillModList:Sum("BASE", cfg, "EnemyKnockbackChance")
end
if not skillFlags.attack then
output.ImpaleChance = 0
else
output.ImpaleChance = m_min(100, skillModList:Sum("BASE", cfg, "ImpaleChance"))
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 activeSkill.bleedCfg then
activeSkill.bleedCfg = {
skillName = skillCfg.skillName,
skillPart = skillCfg.skillPart,
skillTypes = skillCfg.skillTypes,
slotName = skillCfg.slotName,
flags = bor(ModFlag.Dot, ModFlag.Ailment, band(skillCfg.flags, ModFlag.Melee) ~= 0 and ModFlag.MeleeHit or 0),
keywordFlags = bor(band(skillCfg.keywordFlags, bnot(KeywordFlag.Hit)), KeywordFlag.Bleed, KeywordFlag.Ailment, KeywordFlag.PhysicalDot),
skillCond = { },
}
end
local dotCfg = activeSkill.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(activeSkill, 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(skillModList, dotCfg, "AilmentEffect")
local rateMod = calcLib.mod(skillModList, cfg, "BleedFaster")
output.BleedDPS = baseVal * effectMod * rateMod * effMult
local durationBase
if skillData.bleedDurationIsSkillDuration then
durationBase = skillData.duration
else
durationBase = 5
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
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 },
{ "%.2f ^8(damage rate modifier)", rateMod },
{ "%.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 rateMod ~= 1 then
t_insert(globalBreakdown.BleedDuration, s_format("/ %.2f ^8(damage rate modifier)", rateMod))
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 activeSkill.poisonCfg then
activeSkill.poisonCfg = {
skillName = skillCfg.skillName,
skillPart = skillCfg.skillPart,
skillTypes = skillCfg.skillTypes,
slotName = skillCfg.slotName,
flags = bor(ModFlag.Dot, ModFlag.Ailment, band(skillCfg.flags, ModFlag.Melee) ~= 0 and ModFlag.MeleeHit or 0),
keywordFlags = bor(band(skillCfg.keywordFlags, bnot(KeywordFlag.Hit)), KeywordFlag.Poison, KeywordFlag.Ailment, KeywordFlag.ChaosDot),
skillCond = { },
}
end
local dotCfg = activeSkill.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(activeSkill, 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 skillModList:Flag(cfg, "LightningCanPoison") then
local min, max = calcAilmentSourceDamage(activeSkill, 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 skillModList:Flag(cfg, "ColdCanPoison") then
local min, max = calcAilmentSourceDamage(activeSkill, 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 skillModList:Flag(cfg, "FireCanPoison") then
local min, max = calcAilmentSourceDamage(activeSkill, 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(activeSkill, 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(skillModList, dotCfg, "AilmentEffect")
local rateMod = calcLib.mod(skillModList, cfg, "PoisonFaster")
output.PoisonDPS = baseVal * effectMod * rateMod * effMult
local durationBase
if skillData.poisonDurationIsSkillDuration then
durationBase = skillData.duration
else
durationBase = 2
end
local durationMod = calcLib.mod(skillModList, dotCfg, "EnemyPoisonDuration", "SkillAndDamagingAilmentDuration", skillData.poisonIsSkillEffect and "Duration" or nil) * calcLib.mod(enemyDB, nil, "SelfPoisonDuration")
globalOutput.PoisonDuration = durationBase * durationMod / rateMod * 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 },
{ "%.2f ^8(damage rate modifier)", rateMod },
{ "%.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 rateMod ~= 1 then
t_insert(globalBreakdown.PoisonDuration, s_format("/ %.2f ^8(damage rate modifier)", rateMod))
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 activeSkill.igniteCfg then
activeSkill.igniteCfg = {
skillName = skillCfg.skillName,
skillPart = skillCfg.skillPart,
skillTypes = skillCfg.skillTypes,
slotName = skillCfg.slotName,
flags = bor(ModFlag.Dot, ModFlag.Ailment, band(skillCfg.flags, ModFlag.Melee) ~= 0 and ModFlag.MeleeHit or 0),
keywordFlags = bor(band(skillCfg.keywordFlags, bnot(KeywordFlag.Hit)), KeywordFlag.Ignite, KeywordFlag.Ailment, KeywordFlag.FireDot),
skillCond = { },
}
end
local dotCfg = activeSkill.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 skillModList:Flag(cfg, "PhysicalCanIgnite") then
local min, max = calcAilmentSourceDamage(activeSkill, 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 skillModList:Flag(cfg, "LightningCanIgnite") then
local min, max = calcAilmentSourceDamage(activeSkill, 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 skillModList:Flag(cfg, "ColdCanIgnite") then
local min, max = calcAilmentSourceDamage(activeSkill, 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 skillModList:Flag(cfg, "FireCannotIgnite") then
local min, max = calcAilmentSourceDamage(activeSkill, 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 skillModList:Flag(cfg, "ChaosCanIgnite") then
local min, max = calcAilmentSourceDamage(activeSkill, 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(skillModList, dotCfg, "AilmentEffect")
local rateMod = calcLib.mod(skillModList, cfg, "IgniteBurnFaster") / calcLib.mod(skillModList, cfg, "IgniteBurnSlower")
output.IgniteDPS = baseVal * effectMod * rateMod * effMult
local incDur = skillModList:Sum("INC", dotCfg, "EnemyIgniteDuration", "SkillAndDamagingAilmentDuration") + enemyDB:Sum("INC", nil, "SelfIgniteDuration")
local moreDur = enemyDB:More(nil, "SelfIgniteDuration")
globalOutput.IgniteDuration = 4 * (1 + incDur / 100) * moreDur / rateMod * 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)", rateMod },
{ "%.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 rateMod ~= 1 then
t_insert(globalBreakdown.IgniteDuration, s_format("/ %.2f ^8(burn rate modifier)", rateMod))
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 skillModList:Flag(cfg, "PhysicalCanShock") then
sourceHitDmg = sourceHitDmg + output.PhysicalHitAverage
sourceCritDmg = sourceCritDmg + output.PhysicalCritAverage
end
if canDeal.Lightning and not skillModList:Flag(cfg, "LightningCannotShock") then
sourceHitDmg = sourceHitDmg + output.LightningHitAverage
sourceCritDmg = sourceCritDmg + output.LightningCritAverage
end
if canDeal.Cold and skillModList:Flag(cfg, "ColdCanShock") then
sourceHitDmg = sourceHitDmg + output.ColdHitAverage
sourceCritDmg = sourceCritDmg + output.ColdCritAverage
end
if canDeal.Fire and skillModList:Flag(cfg, "FireCanShock") then
sourceHitDmg = sourceHitDmg + output.FireHitAverage
sourceCritDmg = sourceCritDmg + output.FireCritAverage
end
if canDeal.Chaos and skillModList: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 + skillModList: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 skillModList:Flag(cfg, "ColdCannotFreeze") then
sourceHitDmg = sourceHitDmg + output.ColdHitAverage
sourceCritDmg = sourceCritDmg + output.ColdCritAverage
end
if canDeal.Lightning and skillModList: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 + skillModList: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(skillModList, cfg, "EnemyKnockbackDistance"))
if breakdown then
breakdown.KnockbackDistance = {
radius = output.KnockbackDistance,
}
end
end
-- Calculate enemy stun modifiers
local enemyStunThresholdRed = -skillModList: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 = skillModList: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
-- Calculate impale chance and modifiers
if canDeal.Physical and output.ImpaleChance > 0 then
skillFlags.impale = true
local impaleChance = m_min(output.ImpaleChance/100, 1)
local maxStacks = 5 + skillModList:Sum("BASE", cfg, "ImpaleStacksMax") -- magic number: base stacks duration
local configStacks = enemyDB:Sum("BASE", nil, "Multiplier:ImpaleStack")
local impaleStacks = configStacks > 0 and m_min(configStacks, maxStacks) or maxStacks
local baseStoredDamage = 0.1 -- magic number: base impale stored damage
local storedDamageInc = skillModList:Sum("INC", cfg, "ImpaleEffect")/100
local storedDamageMore = round(skillModList:More(cfg, "ImpaleEffect"), 2)
local storedDamageModifier = (1 + storedDamageInc) * storedDamageMore
local impaleStoredDamage = baseStoredDamage * storedDamageModifier
local impaleDMGModifier = impaleStoredDamage * impaleStacks * impaleChance
globalOutput.ImpaleStacksMax = maxStacks
globalOutput.ImpaleStacks = impaleStacks
output.ImpaleStoredDamage = impaleStoredDamage * 100
output.ImpaleModifier = 1 + impaleDMGModifier
if breakdown then
breakdown.ImpaleStoredDamage = {}
t_insert(breakdown.ImpaleStoredDamage, "10% ^8(base value)")
t_insert(breakdown.ImpaleStoredDamage, s_format("x %.2f ^8(increased effectiveness)", storedDamageModifier))
t_insert(breakdown.ImpaleStoredDamage, s_format("= %.1f%%", output.ImpaleStoredDamage))
breakdown.ImpaleModifier = {}
t_insert(breakdown.ImpaleModifier, s_format("%d ^8(numer of stacks, can be overridden in the Configuration tab)", impaleStacks))
t_insert(breakdown.ImpaleModifier, s_format("x %.3f ^8(stored damage)", impaleStoredDamage))
t_insert(breakdown.ImpaleModifier, s_format("x %.2f ^8(impale chance)", impaleChance))
t_insert(breakdown.ImpaleModifier, s_format("= %.3f", impaleDMGModifier))
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")
combineStat("ImpaleChance", "AVERAGE")
combineStat("ImpaleStoredDamage", "AVERAGE")
combineStat("ImpaleModifier", "CHANCE", "ImpaleChance")
end
if skillFlags.hit and skillData.decay and canDeal.Chaos then
-- Calculate DPS for Essence of Delirium's Decay effect
skillFlags.decay = true
activeSkill.decayCfg = {
skillName = skillCfg.skillName,
skillPart = skillCfg.skillPart,
skillTypes = skillCfg.skillTypes,
slotName = skillCfg.slotName,
flags = ModFlag.Dot,
keywordFlags = bor(band(skillCfg.keywordFlags, bnot(KeywordFlag.Hit)), KeywordFlag.ChaosDot),
}
local dotCfg = activeSkill.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 = skillModList:Sum("INC", dotCfg, "Damage", "ChaosDamage")
local more = round(skillModList:More(dotCfg, "Damage", "ChaosDamage"), 2)
local mult = skillModList:Sum("BASE", dotTypeCfg, "ChaosDotMultiplier")
output.DecayDPS = skillData.decay * (1 + inc/100) * more * (1 + mult/100) * effMult
local durationMod = calcLib.mod(skillModList, 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, mult, 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