Files
PathOfBuilding/Modules/CalcOffence.lua
Openarl 8c01c85f1f Release 1.4.12
- Added shared item list
- Added options screen
- Added toasts
- Program now always updates on first run, but continues if update check fails
- Updated libcurl to 7.54.0
2017-05-19 14:50:33 +10:00

1622 lines
69 KiB
Lua

-- Path of Building
--
-- Module: Calc Offence
-- Performs offence calculations.
--
local calcs = ...
local pairs = pairs
local ipairs = ipairs
local t_insert = table.insert
local m_floor = math.floor
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"}
-- Calculate min/max damage of a hit for the given damage type
local function calcHitDamage(actor, source, cfg, breakdown, damageType, ...)
local modDB = actor.modDB
local damageTypeMin = damageType.."Min"
local damageTypeMax = damageType.."Max"
-- Calculate base values
local damageEffectiveness = actor.mainSkill.skillData.damageEffectiveness or 1
local addedMin = modDB:Sum("BASE", cfg, damageTypeMin)
local addedMax = modDB:Sum("BASE", cfg, damageTypeMax)
local baseMin = (source[damageTypeMin] or 0) + addedMin * damageEffectiveness
local baseMax = (source[damageTypeMax] or 0) + addedMax * damageEffectiveness
if breakdown and not (...) and baseMin ~= 0 and baseMax ~= 0 then
t_insert(breakdown, "Base damage:")
local plus = ""
if (source[damageTypeMin] or 0) ~= 0 or (source[damageTypeMax] or 0) ~= 0 then
t_insert(breakdown, s_format("%d to %d ^8(base damage from %s)", source[damageTypeMin], source[damageTypeMax], source.type and "weapon" or "skill"))
plus = "+ "
end
if addedMin ~= 0 or addedMax ~= 0 then
if damageEffectiveness ~= 1 then
t_insert(breakdown, s_format("%s(%d to %d) x %.2f ^8(added damage multiplied by damage effectiveness)", plus, addedMin, addedMax, damageEffectiveness))
else
t_insert(breakdown, s_format("%s%d to %d ^8(added damage)", plus, addedMin, addedMax))
end
end
t_insert(breakdown, s_format("= %.1f to %.1f", baseMin, baseMax))
end
-- Calculate conversions
local addMin, addMax = 0, 0
local conversionTable = actor.conversionTable
for _, otherType in ipairs(dmgTypeList) do
if otherType == damageType then
-- Damage can only be converted from damage types that preceed this one in the conversion sequence, so stop here
break
end
local convMult = conversionTable[otherType][damageType]
if convMult > 0 then
-- Damage is being converted/gained from the other damage type
local min, max = calcHitDamage(actor, source, cfg, breakdown, otherType, 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
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.rowList, {
source = damageType,
convSrc = (addMin ~= 0 or addMax ~= 0) and (addMin .. " to " .. addMax),
total = addMin .. " to " .. addMax,
convDst = (...) and s_format("%d%% to %s", conversionTable[damageType][...] * 100, ...),
})
end
return addMin, addMax
end
-- Build lists of applicable modifier names
local addElemental = isElemental[damageType]
local modNames = { damageType.."Damage", "Damage" }
for i = 1, select('#', ...) do
local dstElem = select(i, ...)
-- Add modifiers for damage types to which this damage is being converted
addElemental = addElemental or isElemental[dstElem]
t_insert(modNames, dstElem.."Damage")
end
if addElemental then
-- Damage is elemental or is being converted to elemental damage, add global elemental modifiers
t_insert(modNames, "ElementalDamage")
end
-- Combine modifiers
local inc = 1 + modDB:Sum("INC", cfg, unpack(modNames)) / 100
local more = m_floor(modDB:Sum("MORE", cfg, unpack(modNames)) * 100 + 0.50000001) / 100
if breakdown then
t_insert(breakdown.rowList, {
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 = (...) and s_format("%d%% to %s", conversionTable[damageType][...] * 100, ...),
})
end
return (round(baseMin * inc * more) + addMin),
(round(baseMax * inc * more) + addMax)
end
-- Performs all offensive calculations
function calcs.offence(env, actor)
local modDB = actor.modDB
local enemyDB = actor.enemy.modDB
local output = actor.output
local breakdown = actor.breakdown
local mainSkill = actor.mainSkill
local skillData = mainSkill.skillData
local skillFlags = mainSkill.skillFlags
local skillCfg = mainSkill.skillCfg
if skillData.showAverage then
skillFlags.showAverage = true
else
skillFlags.notAverage = true
end
if skillFlags.disable then
-- Skill is disabled
output.CombinedDPS = 0
return
end
-- Merge main skill mods
modDB:AddList(mainSkill.skillModList)
-- Update skill data
for _, value in ipairs(modDB:Sum("LIST", skillCfg, "SkillData")) do
if value.merge == "MAX" then
skillData[value.key] = m_max(value.value, skillData[value.key] or 0)
else
skillData[value.key] = value.value
end
end
skillCfg.skillCond["SkillIsTriggered"] = skillData.triggered
-- Add addition stat bonuses
if modDB:Sum("FLAG", nil, "IronGrip") then
modDB:NewMod("PhysicalDamage", "INC", actor.strDmgBonus, "Strength", bor(ModFlag.Attack, ModFlag.Projectile))
end
if modDB:Sum("FLAG", nil, "IronWill") then
modDB:NewMod("Damage", "INC", actor.strDmgBonus, "Strength", ModFlag.Spell)
end
if modDB:Sum("FLAG", nil, "MinionDamageAppliesToPlayer") then
-- Minion Damage conversion from The Scourge
for _, value in ipairs(modDB:Sum("LIST", env.player.mainSkill.skillCfg, "MinionModifier")) do
if value.mod.name == "Damage" then
modDB:AddMod(value.mod)
end
end
end
if modDB:Sum("FLAG", nil, "SpellDamageAppliesToAttacks") then
-- Spell Damage conversion from Crown of Eyes
for i, mod in ipairs(modDB.mods.Damage or { }) do
if mod.type == "INC" and band(mod.flags, ModFlag.Spell) ~= 0 then
modDB:NewMod("Damage", "INC", mod.value, mod.source, bor(band(mod.flags, bnot(ModFlag.Spell)), ModFlag.Attack), mod.keywordFlags, unpack(mod.tagList))
end
end
end
if modDB:Sum("FLAG", nil, "ClawDamageAppliesToUnarmed") then
-- Claw Damage conversion from Rigwald's Curse
for i, mod in ipairs(modDB.mods.PhysicalDamage or { }) do
if band(mod.flags, ModFlag.Claw) ~= 0 then
modDB:NewMod("PhysicalDamage", mod.type, mod.value, mod.source, bor(band(mod.flags, bnot(ModFlag.Claw)), ModFlag.Unarmed), mod.keywordFlags, unpack(mod.tagList))
end
end
end
if modDB:Sum("FLAG", nil, "ClawAttackSpeedAppliesToUnarmed") then
-- Claw Attack Speed conversion from Rigwald's Curse
for i, mod in ipairs(modDB.mods.Speed or { }) do
if band(mod.flags, ModFlag.Claw) ~= 0 and band(mod.flags, ModFlag.Attack) ~= 0 then
modDB:NewMod("Speed", mod.type, mod.value, mod.source, bor(band(mod.flags, bnot(ModFlag.Claw)), ModFlag.Unarmed), mod.keywordFlags, unpack(mod.tagList))
end
end
end
if modDB:Sum("FLAG", nil, "ClawCritChanceAppliesToUnarmed") then
-- Claw Crit Chance conversion from Rigwald's Curse
for i, mod in ipairs(modDB.mods.CritChance or { }) do
if band(mod.flags, ModFlag.Claw) ~= 0 then
modDB:NewMod("CritChance", mod.type, mod.value, mod.source, bor(band(mod.flags, bnot(ModFlag.Claw)), ModFlag.Unarmed), mod.keywordFlags, unpack(mod.tagList))
end
end
end
local isAttack = skillFlags.attack
-- Calculate skill type stats
if skillFlags.minion then
if mainSkill.minion and mainSkill.minion.minionData.limit then
output.ActiveMinionLimit = m_floor(calcLib.val(modDB, mainSkill.minion.minionData.limit, skillCfg))
end
end
if skillFlags.projectile then
if modDB:Sum("FLAG", nil, "PointBlank") then
modDB:NewMod("Damage", "MORE", 50, "Point Blank", bor(ModFlag.Attack, ModFlag.Projectile), { type = "DistanceRamp", ramp = {{10,1},{35,0},{150,-1}} })
end
output.ProjectileCount = modDB:Sum("BASE", skillCfg, "ProjectileCount")
output.PierceChance = m_min(100, modDB:Sum("BASE", skillCfg, "PierceChance"))
output.ProjectileSpeedMod = calcLib.mod(modDB, skillCfg, "ProjectileSpeed")
if breakdown then
breakdown.ProjectileSpeedMod = breakdown.mod(skillCfg, "ProjectileSpeed")
end
end
if skillFlags.melee then
if skillFlags.weapon1Attack then
actor.weaponRange1 = (actor.weaponData1.range and actor.weaponData1.range + modDB:Sum("BASE", skillCfg, "MeleeWeaponRange")) or (data.weaponTypeInfo["None"].range + modDB:Sum("BASE", skillCfg, "UnarmedRange"))
end
if skillFlags.weapon2Attack then
actor.weaponRange2 = (actor.weaponData2.range and actor.weaponData2.range + modDB:Sum("BASE", skillCfg, "MeleeWeaponRange")) or (data.weaponTypeInfo["None"].range + modDB:Sum("BASE", skillCfg, "UnarmedRange"))
end
if mainSkill.skillTypes[SkillType.MeleeSingleTarget] then
local range = 100
if skillFlags.weapon1Attack then
range = m_min(range, actor.weaponRange1)
end
if skillFlags.weapon2Attack then
range = m_min(range, actor.weaponRange2)
end
output.WeaponRange = range + 2
if breakdown then
breakdown.WeaponRange = {
radius = output.WeaponRange
}
end
end
end
if skillFlags.area or skillData.radius then
output.AreaOfEffectMod = calcLib.mod(modDB, skillCfg, "AreaOfEffect")
if skillData.radiusIsWeaponRange then
local range = 0
if skillFlags.weapon1Attack then
range = m_max(range, actor.weaponRange1)
end
if skillFlags.weapon2Attack then
range = m_max(range, actor.weaponRange2)
end
skillData.radius = range + 2
end
if skillData.radius then
skillFlags.area = true
local baseRadius = skillData.radius + (skillData.radiusExtra or 0)
output.AreaOfEffectRadius = m_floor(baseRadius * m_sqrt(output.AreaOfEffectMod))
if breakdown then
if output.AreaOfEffectRadius ~= baseRadius then
breakdown.AreaOfEffectRadius = {
s_format("%d ^8(base radius)", baseRadius),
s_format("x %.2f ^8(square root of area of effect modifier)", m_sqrt(output.AreaOfEffectMod)),
s_format("= %d", output.AreaOfEffectRadius),
}
else
breakdown.AreaOfEffectRadius = { }
end
breakdown.AreaOfEffectRadius.radius = output.AreaOfEffectRadius
end
if skillData.radiusSecondary then
baseRadius = skillData.radiusSecondary + (skillData.radiusExtra or 0)
output.AreaOfEffectRadiusSecondary = m_floor(baseRadius * m_sqrt(output.AreaOfEffectMod))
if breakdown then
if output.AreaOfEffectRadiusSecondary ~= baseRadius then
breakdown.AreaOfEffectRadiusSecondary = {
s_format("%d ^8(base radius)", baseRadius),
s_format("x %.2f ^8(square root of area of effect modifier)", m_sqrt(output.AreaOfEffectMod)),
s_format("= %d", output.AreaOfEffectRadiusSecondary),
}
else
breakdown.AreaOfEffectRadiusSecondary = { }
end
breakdown.AreaOfEffectRadiusSecondary.radius = output.AreaOfEffectRadiusSecondary
end
end
end
if breakdown then
breakdown.AreaOfEffectMod = breakdown.mod(skillCfg, "AreaOfEffect")
end
end
if skillFlags.trap then
output.ActiveTrapLimit = modDB:Sum("BASE", skillCfg, "ActiveTrapLimit")
output.TrapCooldown = (skillData.trapCooldown or 4) / calcLib.mod(modDB, skillCfg, "CooldownRecovery")
if breakdown then
breakdown.TrapCooldown = {
s_format("%.2fs ^8(base)", skillData.trapCooldown or 4),
s_format("/ %.2f ^8(increased/reduced cooldown recovery)", 1 + modDB:Sum("INC", skillCfg, "CooldownRecovery") / 100),
s_format("= %.2fs", output.TrapCooldown)
}
end
elseif skillData.cooldown then
output.Cooldown = skillData.cooldown / calcLib.mod(modDB, skillCfg, "CooldownRecovery")
if breakdown then
breakdown.Cooldown = {
s_format("%.2fs ^8(base)", skillData.cooldown),
s_format("/ %.2f ^8(increased/reduced cooldown recovery)", 1 + modDB:Sum("INC", skillCfg, "CooldownRecovery") / 100),
s_format("= %.2fs", output.Cooldown)
}
end
end
if skillFlags.mine then
output.ActiveMineLimit = modDB:Sum("BASE", skillCfg, "ActiveMineLimit")
end
if skillFlags.totem then
output.ActiveTotemLimit = modDB:Sum("BASE", skillCfg, "ActiveTotemLimit")
output.TotemLifeMod = calcLib.mod(modDB, skillCfg, "TotemLife")
output.TotemLife = round(m_floor(data.monsterLifeTable[skillData.totemLevel] * data.totemLifeMult[mainSkill.skillTotemId]) * output.TotemLifeMod)
if breakdown then
breakdown.TotemLifeMod = breakdown.mod(skillCfg, "TotemLife")
breakdown.TotemLife = {
"Totem level: "..skillData.totemLevel,
data.monsterLifeTable[skillData.totemLevel].." ^8(base life for a level "..skillData.totemLevel.." monster)",
"x "..data.totemLifeMult[mainSkill.skillTotemId].." ^8(life multiplier for this totem type)",
"x "..output.TotemLifeMod.." ^8(totem life modifier)",
"= "..output.TotemLife,
}
end
end
-- Skill duration
local debuffDurationMult
if env.mode_effective then
debuffDurationMult = 1 / calcLib.mod(enemyDB, skillCfg, "BuffExpireFaster")
else
debuffDurationMult = 1
end
do
output.DurationMod = calcLib.mod(modDB, skillCfg, "Duration")
if breakdown then
breakdown.DurationMod = breakdown.mod(skillCfg, "Duration")
end
local durationBase = skillData.duration or 0
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
end
-- Run skill setup function
do
local setupFunc = mainSkill.activeGem.data.setupFunc
if setupFunc then
setupFunc(actor, output)
end
end
-- Cache global damage disabling flags
local canDeal = { }
for _, damageType in pairs(dmgTypeList) do
canDeal[damageType] = not modDB:Sum("FLAG", skillCfg, "DealNo"..damageType)
end
-- Calculate damage conversion percentages
actor.conversionTable = wipeTable(actor.conversionTable)
for damageTypeIndex = 1, 4 do
local damageType = dmgTypeList[damageTypeIndex]
local globalConv = wipeTable(tempTable1)
local skillConv = wipeTable(tempTable2)
local add = wipeTable(tempTable3)
local globalTotal, skillTotal = 0, 0
for otherTypeIndex = damageTypeIndex + 1, 5 do
-- For all possible destination types, check for global and skill conversions
otherType = dmgTypeList[otherTypeIndex]
globalConv[otherType] = modDB:Sum("BASE", skillCfg, damageType.."DamageConvertTo"..otherType, isElemental[damageType] and "ElementalDamageConvertTo"..otherType or nil)
globalTotal = globalTotal + globalConv[otherType]
skillConv[otherType] = modDB:Sum("BASE", skillCfg, "Skill"..damageType.."DamageConvertTo"..otherType)
skillTotal = skillTotal + skillConv[otherType]
add[otherType] = modDB:Sum("BASE", skillCfg, damageType.."DamageGainAs"..otherType, isElemental[damageType] and "ElementalDamageGainAs"..otherType or nil)
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
-- The game currently doesn't scale this down even though it is supposed to
--skillConv[type] = val * factor
end
for type, val in pairs(globalConv) do
globalConv[type] = 0
end
elseif globalTotal + skillTotal > 100 then
-- Conversion exceeds 100%, scale down non-skill conversions
local factor = (100 - skillTotal) / globalTotal
for type, val in pairs(globalConv) do
globalConv[type] = val * factor
end
globalTotal = globalTotal * factor
end
local dmgTable = { }
for type, val in pairs(globalConv) do
dmgTable[type] = (globalConv[type] + skillConv[type] + add[type]) / 100
end
dmgTable.mult = 1 - m_min((globalTotal + skillTotal) / 100, 1)
actor.conversionTable[damageType] = dmgTable
end
actor.conversionTable["Chaos"] = { mult = 1 }
-- Calculate mana cost (may be slightly off due to rounding differences)
do
local more = m_floor(modDB:Sum("MORE", skillCfg, "ManaCost") * 100 + 0.0001) / 100
local inc = modDB:Sum("INC", skillCfg, "ManaCost")
local base = modDB:Sum("BASE", skillCfg, "ManaCost")
output.ManaCost = m_floor(m_max(0, (skillData.manaCost or 0) * more * (1 + inc / 100) + base))
if mainSkill.skillTypes[SkillType.ManaCostPercent] and skillFlags.totem then
output.ManaCost = m_floor(output.Mana * output.ManaCost / 100)
end
if breakdown and output.ManaCost ~= (skillData.manaCost or 0) then
breakdown.ManaCost = {
s_format("%d ^8(base mana cost)", skillData.manaCost or 0)
}
if more ~= 1 then
t_insert(breakdown.ManaCost, s_format("x %.2f ^8(mana cost multiplier)", more))
end
if inc ~= 0 then
t_insert(breakdown.ManaCost, s_format("x %.2f ^8(increased/reduced mana cost)", 1 + inc/100))
end
if base ~= 0 then
t_insert(breakdown.ManaCost, s_format("- %d ^8(- mana cost)", -base))
end
t_insert(breakdown.ManaCost, s_format("= %d", output.ManaCost))
end
end
-- Configure damage passes
local passList = { }
if isAttack then
output.MainHand = { }
output.OffHand = { }
if skillFlags.weapon1Attack then
if breakdown then
breakdown.MainHand = LoadModule("Modules/CalcBreakdown", modDB, output.MainHand)
end
mainSkill.weapon1Cfg.skillStats = output.MainHand
t_insert(passList, {
label = "Main Hand",
source = actor.weaponData1,
cfg = mainSkill.weapon1Cfg,
output = output.MainHand,
breakdown = breakdown and breakdown.MainHand,
})
end
if skillFlags.weapon2Attack then
if breakdown then
breakdown.OffHand = LoadModule("Modules/CalcBreakdown", modDB, output.OffHand)
end
mainSkill.weapon2Cfg.skillStats = output.OffHand
t_insert(passList, {
label = "Off Hand",
source = actor.weaponData2,
cfg = mainSkill.weapon2Cfg,
output = output.OffHand,
breakdown = breakdown and breakdown.OffHand,
})
end
else
t_insert(passList, {
label = "Skill",
source = skillData,
cfg = skillCfg,
output = output,
breakdown = breakdown,
})
end
local function combineStat(stat, mode, ...)
-- Combine stats from Main Hand and Off Hand according to the mode
if mode == "OR" or not skillFlags.bothWeaponAttack then
output[stat] = output.MainHand[stat] or output.OffHand[stat]
elseif mode == "ADD" then
output[stat] = (output.MainHand[stat] or 0) + (output.OffHand[stat] or 0)
elseif mode == "AVERAGE" then
output[stat] = ((output.MainHand[stat] or 0) + (output.OffHand[stat] or 0)) / 2
elseif mode == "CHANCE" then
if output.MainHand[stat] and output.OffHand[stat] then
local mainChance = output.MainHand[...] * output.MainHand.HitChance
local offChance = output.OffHand[...] * output.OffHand.HitChance
local mainPortion = mainChance / (mainChance + offChance)
local offPortion = offChance / (mainChance + offChance)
output[stat] = output.MainHand[stat] * mainPortion + output.OffHand[stat] * offPortion
if breakdown then
if not breakdown[stat] then
breakdown[stat] = { }
end
t_insert(breakdown[stat], "Contribution from Main Hand:")
t_insert(breakdown[stat], s_format("%.1f", output.MainHand[stat]))
t_insert(breakdown[stat], s_format("x %.3f ^8(portion of instances created by main hand)", mainPortion))
t_insert(breakdown[stat], s_format("= %.1f", output.MainHand[stat] * mainPortion))
t_insert(breakdown[stat], "Contribution from Off Hand:")
t_insert(breakdown[stat], s_format("%.1f", output.OffHand[stat]))
t_insert(breakdown[stat], s_format("x %.3f ^8(portion of instances created by off hand)", offPortion))
t_insert(breakdown[stat], s_format("= %.1f", output.OffHand[stat] * offPortion))
t_insert(breakdown[stat], "Total:")
t_insert(breakdown[stat], s_format("%.1f + %.1f", output.MainHand[stat] * mainPortion, output.OffHand[stat] * offPortion))
t_insert(breakdown[stat], s_format("= %.1f", output[stat]))
end
else
output[stat] = output.MainHand[stat] or output.OffHand[stat]
end
elseif mode == "DPS" then
output[stat] = (output.MainHand[stat] or 0) + (output.OffHand[stat] or 0)
if not skillData.doubleHitsWhenDualWielding then
output[stat] = output[stat] / 2
end
end
end
for _, pass in ipairs(passList) do
local source, output, cfg, breakdown = pass.source, pass.output, pass.cfg, pass.breakdown
-- Calculate hit chance
output.Accuracy = calcLib.val(modDB, "Accuracy", cfg)
if breakdown then
breakdown.Accuracy = breakdown.simple(nil, cfg, output.Accuracy, "Accuracy")
end
if not isAttack or modDB:Sum("FLAG", cfg, "CannotBeEvaded") or skillData.cannotBeEvaded then
output.HitChance = 100
else
local enemyEvasion = round(calcLib.val(enemyDB, "Evasion"))
output.HitChance = calcLib.hitChance(enemyEvasion, output.Accuracy)
if breakdown then
breakdown.HitChance = {
"Enemy level: "..env.enemyLevel..(env.configInput.enemyLevel and " ^8(overridden from the Configuration tab" or " ^8(can be overridden in the Configuration tab)"),
"Average enemy evasion: "..enemyEvasion,
"Approximate hit chance: "..output.HitChance.."%",
}
end
end
-- Calculate attack/cast speed
if skillData.timeOverride then
output.Time = skillData.timeOverride
output.Speed = 1 / output.Time
else
local baseSpeed
if isAttack then
if skillData.castTimeOverridesAttackTime then
-- Skill is overriding weapon attack speed
baseSpeed = 1 / skillData.castTime * (1 + (source.AttackSpeedInc or 0) / 100)
else
baseSpeed = source.attackRate or 1
end
else
baseSpeed = 1 / (skillData.castTime or 1)
end
output.Speed = baseSpeed * round(calcLib.mod(modDB, cfg, "Speed"), 2)
if breakdown then
breakdown.Speed = breakdown.simple(baseSpeed, cfg, output.Speed, "Speed")
end
if skillData.attackRateCap then
output.Speed = m_min(output.Speed, skillData.attackRateCap)
end
output.Time = 1 / output.Speed
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")
output.Time = 1 / output.Speed
if skillFlags.bothWeaponAttack then
if breakdown then
breakdown.Speed = {
"Both weapons:",
s_format("(%.2f + %.2f) / 2", output.MainHand.Speed, output.OffHand.Speed),
s_format("= %.2f", output.Speed),
}
end
end
end
for _, pass in ipairs(passList) do
local globalOutput, globalBreakdown = output, breakdown
local source, output, cfg, breakdown = pass.source, pass.output, pass.cfg, pass.breakdown
-- Calculate crit chance, crit multiplier, and their combined effect
if modDB:Sum("FLAG", nil, "NeverCrit") then
output.PreEffectiveCritChance = 0
output.CritChance = 0
output.CritMultiplier = 0
output.CritEffect = 1
else
local baseCrit = source.critChance or 0
if baseCrit == 100 then
output.PreEffectiveCritChance = 100
output.CritChance = 100
else
local base = modDB:Sum("BASE", cfg, "CritChance")
local inc = modDB:Sum("INC", cfg, "CritChance")
local more = modDB:Sum("MORE", cfg, "CritChance")
local enemyExtra = env.mode_effective and enemyDB:Sum("BASE", nil, "SelfExtraCritChance") or 0
output.CritChance = (baseCrit + base) * (1 + inc / 100) * more
local preCapCritChance = output.CritChance
output.CritChance = m_min(output.CritChance, 95)
if (baseCrit + base) > 0 then
output.CritChance = m_max(output.CritChance, 5)
end
output.PreEffectiveCritChance = output.CritChance
if enemyExtra ~= 0 then
output.CritChance = m_min(output.CritChance + enemyExtra, 100)
end
local preLuckyCritChance = output.CritChance
if env.mode_effective and modDB:Sum("FLAG", cfg, "CritChanceLucky") then
output.CritChance = (1 - (1 - output.CritChance / 100) ^ 2) * 100
end
local preHitCheckCritChance = output.CritChance
if env.mode_effective then
output.CritChance = output.CritChance * output.HitChance / 100
end
if breakdown and output.CritChance ~= baseCrit then
breakdown.CritChance = { }
if base ~= 0 then
t_insert(breakdown.CritChance, s_format("(%g + %g) ^8(base)", baseCrit, base))
else
t_insert(breakdown.CritChance, s_format("%g ^8(base)", baseCrit + base))
end
if inc ~= 0 then
t_insert(breakdown.CritChance, s_format("x %.2f", 1 + inc/100).." ^8(increased/reduced)")
end
if more ~= 1 then
t_insert(breakdown.CritChance, s_format("x %.2f", more).." ^8(more/less)")
end
t_insert(breakdown.CritChance, s_format("= %.2f%% ^8(crit chance)", output.PreEffectiveCritChance))
if preCapCritChance > 95 then
local overCap = preCapCritChance - 95
t_insert(breakdown.CritChance, s_format("Crit is overcapped by %.2f%% (%d%% increased Critical Strike Chance)", overCap, overCap / more / (baseCrit + base) * 100))
end
if enemyExtra ~= 0 then
t_insert(breakdown.CritChance, s_format("+ %g ^8(extra chance for enemy to be crit)", enemyExtra))
t_insert(breakdown.CritChance, s_format("= %.2f%% ^8(chance to crit against enemy)", preLuckyCritChance))
end
if env.mode_effective and modDB:Sum("FLAG", cfg, "CritChanceLucky") then
t_insert(breakdown.CritChance, "Crit Chance is Lucky:")
t_insert(breakdown.CritChance, s_format("1 - (1 - %.4f) x (1 - %.4f)", preLuckyCritChance / 100, preLuckyCritChance / 100))
t_insert(breakdown.CritChance, s_format("= %.2f%%", preHitCheckCritChance))
end
if env.mode_effective and output.HitChance < 100 then
t_insert(breakdown.CritChance, "Crit confirmation roll:")
t_insert(breakdown.CritChance, s_format("%.2f%%", preHitCheckCritChance))
t_insert(breakdown.CritChance, s_format("x %.2f ^8(chance to hit)", output.HitChance / 100))
t_insert(breakdown.CritChance, s_format("= %.2f%%", output.CritChance))
end
end
end
if modDB:Sum("FLAG", cfg, "NoCritMultiplier") then
output.CritMultiplier = 1
else
local extraDamage = modDB:Sum("BASE", cfg, "CritMultiplier") / 100
if env.mode_effective then
local enemyInc = 1 + enemyDB:Sum("INC", nil, "SelfCritMultiplier") / 100
extraDamage = round(extraDamage * enemyInc, 2)
if breakdown and enemyInc ~= 1 then
breakdown.CritMultiplier = {
s_format("%d%% ^8(additional extra damage)", modDB:Sum("BASE", cfg, "CritMultiplier") / 100),
s_format("x %.2f ^8(increased/reduced extra crit damage taken by enemy)", enemyInc),
s_format("= %d%% ^8(extra crit damage)", extraDamage * 100),
}
end
end
output.CritMultiplier = 1 + m_max(0, extraDamage)
end
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 hit damage for each damage type
local totalHitMin, totalHitMax = 0, 0
local totalCritMin, totalCritMax = 0, 0
output.LifeLeech = 0
output.LifeLeechInstant = 0
output.ManaLeech = 0
output.ManaLeechInstant = 0
for pass = 1, 2 do
-- Pass 1 is critical strike damage, pass 2 is non-critical strike
cfg.skillCond["CriticalStrike"] = (pass == 1)
local lifeLeechTotal = 0
local manaLeechTotal = 0
for _, damageType in ipairs(dmgTypeList) do
local min, max
if skillFlags.hit and canDeal[damageType] then
if breakdown then
breakdown[damageType] = {
rowList = { },
colList = {
{ label = "From", key = "source", right = true },
{ label = "Base", key = "base" },
{ label = "Inc/red", key = "inc" },
{ label = "More/less", key = "more" },
{ label = "Converted Damage", key = "convSrc" },
{ label = "Total", key = "total" },
{ label = "Conversion", key = "convDst" },
}
}
end
min, max = calcHitDamage(actor, source, cfg, breakdown and breakdown[damageType], damageType)
local convMult = actor.conversionTable[damageType].mult
local doubleChance = m_min(modDB:Sum("BASE", cfg, "DoubleDamageChance"), 100)
if 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 doubleChance > 0 then
t_insert(breakdown[damageType], s_format("x %.2f ^8(chance to deal double damage)", 1 + doubleChance / 100))
end
end
min = min * convMult
max = max * convMult
if doubleChance > 0 then
min = min * (1 + doubleChance / 100)
max = max * (1 + doubleChance / 100)
end
if pass == 1 then
-- Apply crit multiplier
min = min * output.CritMultiplier
max = max * output.CritMultiplier
end
if (min ~= 0 or max ~= 0) and env.mode_effective then
-- Apply enemy resistances and damage taken modifiers
local preMult
local resist = 0
local pen = 0
local taken = enemyDB:Sum("INC", nil, "DamageTaken", damageType.."DamageTaken")
if damageType == "Physical" then
resist = enemyDB:Sum("BASE", nil, "PhysicalDamageReduction")
else
resist = enemyDB:Sum("BASE", nil, damageType.."Resist")
if isElemental[damageType] then
resist = resist + enemyDB:Sum("BASE", nil, "ElementalResist")
pen = modDB:Sum("BASE", cfg, damageType.."Penetration", "ElementalPenetration")
taken = taken + enemyDB:Sum("INC", nil, "ElementalDamageTaken")
end
resist = m_min(resist, 75)
end
if skillFlags.projectile then
taken = taken + enemyDB:Sum("INC", nil, "ProjectileDamageTaken")
end
local effMult = (1 + taken / 100)
if not isElemental[damageType] or not modDB:Sum("FLAG", cfg, "IgnoreElementalResistances") 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 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 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 modDB:Sum("FLAG", cfg, "CannotLeechLife") and not enemyDB:Sum("FLAG", nil, "CannotLeechLifeFromSelf") then
local lifeLeech = modDB:Sum("BASE", cfg, "DamageLifeLeechToPlayer")
if lifeLeech > 0 then
lifeLeechTotal = lifeLeechTotal + (min + max) / 2 * lifeLeech / 100
end
end
else
if not modDB:Sum("FLAG", cfg, "CannotLeechLife") and not enemyDB:Sum("FLAG", nil, "CannotLeechLifeFromSelf") then
local lifeLeech = modDB:Sum("BASE", cfg, "DamageLeech", "DamageLifeLeech", damageType.."DamageLifeLeech", isElemental[damageType] and "ElementalDamageLifeLeech" or nil) + enemyDB:Sum("BASE", nil, "SelfDamageLifeLeech") / 100
if lifeLeech > 0 then
lifeLeechTotal = lifeLeechTotal + (min + max) / 2 * lifeLeech / 100
end
end
if not modDB:Sum("FLAG", cfg, "CannotLeechMana") and not enemyDB:Sum("FLAG", nil, "CannotLeechManaFromSelf") then
local manaLeech = modDB:Sum("BASE", cfg, "DamageLeech", "DamageManaLeech", damageType.."DamageManaLeech", isElemental[damageType] and "ElementalDamageManaLeech" or nil) + enemyDB:Sum("BASE", nil, "SelfDamageManaLeech") / 100
if manaLeech > 0 then
manaLeechTotal = manaLeechTotal + (min + max) / 2 * manaLeech / 100
end
end
end
else
min, max = 0, 0
if breakdown then
breakdown[damageType] = {
"You can't deal "..damageType.." damage"
}
end
end
if pass == 1 then
output[damageType.."CritAverage"] = (min + max) / 2
totalCritMin = totalCritMin + min
totalCritMax = totalCritMax + max
else
if env.mode == "CALCS" then
output[damageType.."Min"] = min
output[damageType.."Max"] = max
end
output[damageType.."HitAverage"] = (min + max) / 2
totalHitMin = totalHitMin + min
totalHitMax = totalHitMax + max
end
end
local portion = (pass == 1) and (output.CritChance / 100) or (1 - output.CritChance / 100)
if modDB:Sum("FLAG", cfg, "InstantLifeLeech") then
output.LifeLeechInstant = output.LifeLeechInstant + lifeLeechTotal * portion
else
output.LifeLeech = output.LifeLeech + lifeLeechTotal * portion
end
if modDB:Sum("FLAG", cfg, "InstantManaLeech") then
output.ManaLeechInstant = output.ManaLeechInstant + manaLeechTotal * portion
else
output.ManaLeech = output.ManaLeech + manaLeechTotal * portion
end
end
output.TotalMin = totalHitMin
output.TotalMax = totalHitMax
if modDB:Sum("FLAG", skillCfg, "ElementalEquilibrium") and not env.configInput.EEIgnoreHitDamage and (output.FireHitAverage + output.ColdHitAverage + output.LightningHitAverage > 0) then
-- Update enemy hit-by-damage-type conditions
enemyDB.conditions.HitByFireDamage = output.FireHitAverage > 0
enemyDB.conditions.HitByColdDamage = output.ColdHitAverage > 0
enemyDB.conditions.HitByLightningDamage = output.LightningHitAverage > 0
end
if breakdown then
-- For each damage type, calculate percentage of total damage
for _, damageType in ipairs(dmgTypeList) do
if output[damageType.."HitAverage"] > 0 then
t_insert(breakdown[damageType], s_format("Portion of total damage: %d%%", output[damageType.."HitAverage"] / (totalHitMin + totalHitMax) * 200))
end
end
end
local hitRate = output.HitChance / 100 * (globalOutput.HitSpeed or globalOutput.Speed) * (skillData.dpsMultiplier or 1)
-- Calculate leech
local function getLeechInstances(amount, total)
if total == 0 then
return 0, 0
end
local duration = amount / total / 0.02
return duration, duration * hitRate
end
output.LifeLeechDuration, output.LifeLeechInstances = getLeechInstances(output.LifeLeech, modDB:Sum("FLAG", nil, "GhostReaver") and globalOutput.EnergyShield or globalOutput.Life)
output.LifeLeechInstantRate = output.LifeLeechInstant * hitRate
output.ManaLeechDuration, output.ManaLeechInstances = getLeechInstances(output.ManaLeech, globalOutput.Mana)
output.ManaLeechInstantRate = output.ManaLeechInstant * hitRate
-- Calculate gain on hit
if skillFlags.mine or skillFlags.trap or skillFlags.totem then
output.LifeOnHit = 0
output.EnergyShieldOnHit = 0
output.ManaOnHit = 0
else
output.LifeOnHit = (modDB:Sum("BASE", skillCfg, "LifeOnHit") + enemyDB:Sum("BASE", skillCfg, "SelfLifeOnHit")) * calcLib.mod(modDB, nil, "LifeRecovery")
output.EnergyShieldOnHit = (modDB:Sum("BASE", skillCfg, "EnergyShieldOnHit") + enemyDB:Sum("BASE", skillCfg, "SelfEnergyShieldOnHit")) * calcLib.mod(modDB, nil, "EnergyShieldRecovery")
output.ManaOnHit = (modDB:Sum("BASE", skillCfg, "ManaOnHit") + enemyDB:Sum("BASE", skillCfg, "SelfManaOnHit")) * calcLib.mod(modDB, nil, "ManaRecovery")
end
output.LifeOnHitRate = output.LifeOnHit * hitRate
output.EnergyShieldOnHitRate = output.EnergyShieldOnHit * hitRate
output.ManaOnHitRate = output.ManaOnHit * hitRate
-- Calculate average damage and final DPS
output.AverageHit = (totalHitMin + totalHitMax) / 2 * (1 - output.CritChance / 100) + (totalCritMin + totalCritMax) / 2 * output.CritChance / 100
output.AverageDamage = output.AverageHit * output.HitChance / 100
output.TotalDPS = output.AverageDamage * (globalOutput.HitSpeed or globalOutput.Speed) * (skillData.dpsMultiplier or 1)
if breakdown then
if output.CritEffect ~= 1 then
breakdown.AverageHit = {
s_format("%.1f x (1 - %.4f) ^8(damage from non-crits)", (totalHitMin + totalHitMax) / 2, output.CritChance / 100),
s_format("+ %.1f x %.4f ^8(damage from crits)", (totalCritMin + totalCritMax) / 2, output.CritChance / 100),
s_format("= %.1f", output.AverageHit),
}
end
if isAttack then
breakdown.AverageDamage = { }
t_insert(breakdown.AverageDamage, s_format("%s:", pass.label))
t_insert(breakdown.AverageDamage, s_format("%.1f ^8(average hit)", output.AverageHit))
t_insert(breakdown.AverageDamage, s_format("x %.2f ^8(chance to hit)", output.HitChance / 100))
t_insert(breakdown.AverageDamage, s_format("= %.1f", output.AverageDamage))
end
end
end
if isAttack then
-- Combine crit stats, average damage and DPS
combineStat("PreEffectiveCritChance", "AVERAGE")
combineStat("CritChance", "AVERAGE")
combineStat("CritMultiplier", "AVERAGE")
combineStat("AverageDamage", "DPS")
combineStat("TotalDPS", "DPS")
combineStat("LifeLeechDuration", "DPS")
combineStat("LifeLeechInstances", "DPS")
combineStat("LifeLeechInstant", "DPS")
combineStat("LifeLeechInstantRate", "DPS")
combineStat("ManaLeechDuration", "DPS")
combineStat("ManaLeechInstances", "DPS")
combineStat("ManaLeechInstant", "DPS")
combineStat("ManaLeechInstantRate", "DPS")
combineStat("LifeOnHit", "DPS")
combineStat("LifeOnHitRate", "DPS")
combineStat("EnergyShieldOnHit", "DPS")
combineStat("EnergyShieldOnHitRate", "DPS")
combineStat("ManaOnHit", "DPS")
combineStat("ManaOnHitRate", "DPS")
if skillFlags.bothWeaponAttack then
if breakdown then
breakdown.AverageDamage = { }
t_insert(breakdown.AverageDamage, "Both weapons:")
if skillData.doubleHitsWhenDualWielding then
t_insert(breakdown.AverageDamage, s_format("%.1f + %.1f ^8(skill hits with both weapons at once)", output.MainHand.AverageDamage, output.OffHand.AverageDamage))
else
t_insert(breakdown.AverageDamage, s_format("(%.1f + %.1f) / 2 ^8(skill alternates weapons)", output.MainHand.AverageDamage, output.OffHand.AverageDamage))
end
t_insert(breakdown.AverageDamage, s_format("= %.1f", output.AverageDamage))
end
end
end
if env.mode == "CALCS" then
if skillData.showAverage then
output.DisplayDamage = s_format("%.1f average damage", output.AverageDamage)
else
output.DisplayDamage = s_format("%.1f DPS", output.TotalDPS)
end
end
if breakdown then
if isAttack then
breakdown.TotalDPS = {
s_format("%.1f ^8(average damage)", output.AverageDamage),
output.HitSpeed and s_format("x %.2f ^8(hit rate)", output.HitSpeed) or s_format("x %.2f ^8(attack rate)", output.Speed),
}
else
breakdown.TotalDPS = {
s_format("%.1f ^8(average hit)", output.AverageDamage),
output.HitSpeed and s_format("x %.2f ^8(hit rate)", output.HitSpeed) or s_format("x %.2f ^8(cast rate)", output.Speed),
}
end
if skillData.dpsMultiplier then
t_insert(breakdown.TotalDPS, s_format("x %g ^8(DPS multiplier for this skill)", skillData.dpsMultiplier))
end
t_insert(breakdown.TotalDPS, s_format("= %.1f", output.TotalDPS))
end
-- Calculate leech rates
if modDB:Sum("FLAG", nil, "GhostReaver") then
output.LifeLeechRate = 0
output.LifeLeechPerHit = 0
output.EnergyShieldLeechInstanceRate = output.EnergyShield * 0.02 * calcLib.mod(modDB, skillCfg, "LifeLeechRate")
output.EnergyShieldLeechRate = (output.LifeLeechInstantRate + m_min(output.LifeLeechInstances * output.EnergyShieldLeechInstanceRate, output.MaxEnergyShieldLeechRate)) * calcLib.mod(modDB, nil, "EnergyShieldRecovery")
output.EnergyShieldLeechPerHit = (m_min(output.EnergyShieldLeechInstanceRate, output.MaxEnergyShieldLeechRate) * output.LifeLeechDuration + output.LifeLeechInstant) * calcLib.mod(modDB, nil, "EnergyShieldRecovery")
else
output.LifeLeechInstanceRate = output.Life * 0.02 * calcLib.mod(modDB, skillCfg, "LifeLeechRate")
output.LifeLeechRate = (output.LifeLeechInstantRate + m_min(output.LifeLeechInstances * output.LifeLeechInstanceRate, output.MaxLifeLeechRate)) * calcLib.mod(modDB, nil, "LifeRecovery")
output.LifeLeechPerHit = (m_min(output.LifeLeechInstanceRate, output.MaxLifeLeechRate) * output.LifeLeechDuration + output.LifeLeechInstant) * calcLib.mod(modDB, nil, "LifeRecovery")
output.EnergyShieldLeechRate = 0
output.EnergyShieldLeechPerHit = 0
end
output.ManaLeechInstanceRate = output.Mana * 0.02 * calcLib.mod(modDB, skillCfg, "ManaLeechRate")
output.ManaLeechRate = (output.ManaLeechInstantRate + m_min(output.ManaLeechInstances * output.ManaLeechInstanceRate, output.MaxManaLeechRate)) * calcLib.mod(modDB, nil, "ManaRecovery")
output.ManaLeechPerHit = (m_min(output.ManaLeechInstanceRate, output.MaxManaLeechRate) * output.ManaLeechDuration + output.ManaLeechInstant) * calcLib.mod(modDB, nil, "ManaRecovery")
skillFlags.leechES = output.EnergyShieldLeechRate > 0
skillFlags.leechLife = output.LifeLeechRate > 0
skillFlags.leechMana = output.ManaLeechRate > 0
if skillData.showAverage then
output.LifeLeechGainPerHit = output.LifeLeechPerHit + output.LifeOnHit
output.EnergyShieldLeechGainPerHit = output.EnergyShieldLeechPerHit + output.EnergyShieldOnHit
output.ManaLeechGainPerHit = output.ManaLeechPerHit + output.ManaOnHit
else
output.LifeLeechGainRate = output.LifeLeechRate + output.LifeOnHitRate
output.EnergyShieldLeechGainRate = output.EnergyShieldLeechRate + output.EnergyShieldOnHitRate
output.ManaLeechGainRate = output.ManaLeechRate + output.ManaOnHitRate
end
if breakdown then
if skillFlags.leechLife then
breakdown.LifeLeech = breakdown.leech(output.LifeLeechInstant, output.LifeLeechInstantRate, output.LifeLeechInstances, output.Life, "LifeLeechRate", output.MaxLifeLeechRate, output.LifeLeechDuration)
end
if skillFlags.leechES then
breakdown.EnergyShieldLeech = breakdown.leech(output.LifeLeechInstant, output.LifeLeechInstantRate, output.LifeLeechInstances, output.EnergyShield, "LifeLeechRate", output.MaxEnergyShieldLeechRate, output.LifeLeechDuration)
end
if skillFlags.leechMana then
breakdown.ManaLeech = breakdown.leech(output.ManaLeechInstant, output.ManaLeechInstantRate, output.ManaLeechInstances, output.Mana, "ManaLeechRate", output.MaxManaLeechRate, output.ManaLeechDuration)
end
end
-- Calculate skill DOT components
local dotCfg = {
skillName = skillCfg.skillName,
skillPart = skillCfg.skillPart,
slotName = skillCfg.slotName,
flags = bor(band(skillCfg.flags, ModFlag.SourceMask), ModFlag.Dot, skillData.dotIsSpell and ModFlag.Spell or 0, skillData.dotIsArea and ModFlag.Area or 0),
keywordFlags = skillCfg.keywordFlags
}
mainSkill.dotCfg = dotCfg
output.TotalDot = 0
for _, damageType in ipairs(dmgTypeList) do
local baseVal
if canDeal[damageType] then
baseVal = skillData[damageType.."Dot"] or 0
else
baseVal = 0
end
if baseVal > 0 then
skillFlags.dot = true
local effMult = 1
if env.mode_effective then
local resist = 0
local taken = enemyDB:Sum("INC", nil, "DamageTaken", "DamageTakenOverTime", damageType.."DamageTaken", damageType.."DamageTakenOverTime")
if damageType == "Physical" then
resist = enemyDB:Sum("BASE", nil, "PhysicalDamageReduction")
else
resist = enemyDB:Sum("BASE", nil, damageType.."Resist")
if isElemental[damageType] then
resist = resist + enemyDB:Sum("BASE", nil, "ElementalResist")
taken = taken + enemyDB:Sum("INC", nil, "ElementalDamageTaken")
end
resist = m_min(resist, 75)
end
effMult = (1 - resist / 100) * (1 + taken / 100)
output[damageType.."DotEffMult"] = effMult
if breakdown and effMult ~= 1 then
breakdown[damageType.."DotEffMult"] = breakdown.effMult(damageType, resist, 0, taken, effMult)
end
end
local inc = modDB:Sum("INC", dotCfg, "Damage", damageType.."Damage", isElemental[damageType] and "ElementalDamage" or nil)
local more = round(modDB:Sum("MORE", dotCfg, "Damage", damageType.."Damage", isElemental[damageType] and "ElementalDamage" or nil), 2)
local total = baseVal * (1 + inc/100) * more * effMult
output[damageType.."Dot"] = total
output.TotalDot = output.TotalDot + total
if breakdown then
breakdown[damageType.."Dot"] = { }
breakdown.dot(breakdown[damageType.."Dot"], baseVal, inc, more, nil, effMult, total)
end
end
end
skillFlags.bleed = false
skillFlags.poison = false
skillFlags.ignite = false
skillFlags.igniteCanStack = modDB:Sum("FLAG", skillCfg, "IgniteCanStack")
skillFlags.shock = false
skillFlags.freeze = false
for _, pass in ipairs(passList) do
local globalOutput, globalBreakdown = output, breakdown
local source, output, cfg, breakdown = pass.source, pass.output, pass.cfg, pass.breakdown
-- Calculate chance to inflict secondary dots/status effects
cfg.skillCond["CriticalStrike"] = true
if modDB:Sum("FLAG", cfg, "CannotBleed") then
output.BleedChanceOnCrit = 0
else
output.BleedChanceOnCrit = m_min(100, modDB:Sum("BASE", cfg, "BleedChance"))
end
output.PoisonChanceOnCrit = m_min(100, modDB:Sum("BASE", cfg, "PoisonChance"))
if modDB:Sum("FLAG", cfg, "CannotIgnite") then
output.IgniteChanceOnCrit = 0
else
output.IgniteChanceOnCrit = 100
end
if modDB:Sum("FLAG", cfg, "CannotShock") then
output.ShockChanceOnCrit = 0
else
output.ShockChanceOnCrit = 100
end
if modDB:Sum("FLAG", cfg, "CannotFreeze") then
output.FreezeChanceOnCrit = 0
else
output.FreezeChanceOnCrit = 100
end
cfg.skillCond["CriticalStrike"] = false
if modDB:Sum("FLAG", cfg, "CannotBleed") then
output.BleedChanceOnHit = 0
else
output.BleedChanceOnHit = m_min(100, modDB:Sum("BASE", cfg, "BleedChance"))
end
output.PoisonChanceOnHit = m_min(100, modDB:Sum("BASE", cfg, "PoisonChance"))
if modDB:Sum("FLAG", cfg, "CannotIgnite") then
output.IgniteChanceOnHit = 0
else
output.IgniteChanceOnHit = m_min(100, modDB:Sum("BASE", cfg, "EnemyIgniteChance") + enemyDB:Sum("BASE", nil, "SelfIgniteChance"))
end
if modDB:Sum("FLAG", cfg, "CannotShock") then
output.ShockChanceOnHit = 0
else
output.ShockChanceOnHit = m_min(100, modDB:Sum("BASE", cfg, "EnemyShockChance") + enemyDB:Sum("BASE", nil, "SelfShockChance"))
end
if modDB:Sum("FLAG", cfg, "CannotFreeze") then
output.FreezeChanceOnHit = 0
else
output.FreezeChanceOnHit = m_min(100, modDB:Sum("BASE", cfg, "EnemyFreezeChance") + enemyDB:Sum("BASE", nil, "SelfFreezeChance"))
if modDB:Sum("FLAG", cfg, "CritsDontAlwaysFreeze") then
output.FreezeChanceOnCrit = output.FreezeChanceOnHit
end
end
if skillFlags.attack and skillFlags.projectile and modDB:Sum("FLAG", cfg, "ArrowsThatPierceCauseBleeding") then
output.BleedChanceOnHit = 100 - (1 - output.BleedChanceOnHit / 100) * (1 - globalOutput.PierceChance / 100) * 100
output.BleedChanceOnCrit = 100 - (1 - output.BleedChanceOnCrit / 100) * (1 - globalOutput.PierceChance / 100) * 100
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
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 calcSecondaryEffectBase(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, "Base damage:")
t_insert(breakdownDPS, s_format("%.1f ^8(source damage)",sourceHitDmg))
else
if baseFromHit > 0 then
t_insert(breakdownDPS, "Base 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, "Base 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 base 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
local sourceHitDmg = output.PhysicalHitAverage
local sourceCritDmg = output.PhysicalCritAverage
local basePercent = skillData.bleedBasePercent or 10
local baseVal = calcSecondaryEffectBase("Bleed", sourceHitDmg, sourceCritDmg) * basePercent / 100
if baseVal > 0 then
skillFlags.bleed = true
skillFlags.duration = true
if not mainSkill.bleedCfg then
mainSkill.bleedCfg = {
skillName = skillCfg.skillName,
slotName = skillCfg.slotName,
flags = bor(band(skillCfg.flags, ModFlag.SourceMask), ModFlag.Dot, skillData.dotIsSpell and ModFlag.Spell or 0),
keywordFlags = bor(skillCfg.keywordFlags, KeywordFlag.Bleed)
}
end
local dotCfg = mainSkill.bleedCfg
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 inc = modDB:Sum("INC", dotCfg, "Damage", "PhysicalDamage")
local more = round(modDB:Sum("MORE", dotCfg, "Damage", "PhysicalDamage"), 2)
output.BleedDPS = baseVal * (1 + inc/100) * more * effMult
local durationMod = calcLib.mod(modDB, dotCfg, "Duration") * calcLib.mod(enemyDB, nil, "SelfBleedDuration")
globalOutput.BleedDuration = 5 * durationMod * debuffDurationMult
if breakdown then
t_insert(breakdown.BleedDPS, s_format("x %.2f ^8(bleed deals %d%% per second)", basePercent/100, basePercent))
t_insert(breakdown.BleedDPS, s_format("= %.1f", baseVal))
t_insert(breakdown.BleedDPS, "Bleed DPS:")
breakdown.dot(breakdown.BleedDPS, baseVal, inc, more, nil, effMult, output.BleedDPS)
if globalOutput.BleedDuration ~= 5 then
globalBreakdown.BleedDuration = {
"5.00s ^8(base duration)"
}
if durationMod ~= 1 then
t_insert(globalBreakdown.BleedDuration, s_format("x %.2f ^8(duration modifier)", durationMod))
end
if debuffDurationMult ~= 1 then
t_insert(globalBreakdown.BleedDuration, s_format("/ %.2f ^8(debuff expires slower/faster)", 1 / debuffDurationMult))
end
t_insert(globalBreakdown.BleedDuration, s_format("= %.2fs", globalOutput.BleedDuration))
end
end
end
end
-- Calculate poison chance and damage
if canDeal.Chaos and (output.PoisonChanceOnHit + output.PoisonChanceOnCrit) > 0 then
local sourceHitDmg = output.PhysicalHitAverage + output.ChaosHitAverage
local sourceCritDmg = output.PhysicalCritAverage + output.ChaosCritAverage
local baseVal = calcSecondaryEffectBase("Poison", sourceHitDmg, sourceCritDmg * modDB:Sum("MORE", cfg, "PoisonDamageOnCrit")) * 0.08
if baseVal > 0 then
skillFlags.poison = true
skillFlags.duration = true
if not mainSkill.poisonCfg then
mainSkill.poisonCfg = {
skillName = skillCfg.skillName,
slotName = skillCfg.slotName,
flags = bor(band(skillCfg.flags, ModFlag.SourceMask), ModFlag.Dot, skillData.dotIsSpell and ModFlag.Spell or 0),
keywordFlags = bor(skillCfg.keywordFlags, KeywordFlag.Poison)
}
end
local dotCfg = mainSkill.poisonCfg
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 inc = modDB:Sum("INC", dotCfg, "Damage", "ChaosDamage")
local more = round(modDB:Sum("MORE", dotCfg, "Damage", "ChaosDamage"), 2)
output.PoisonDPS = baseVal * (1 + inc/100) * more * effMult
local durationBase
if skillData.poisonDurationIsSkillDuration then
durationBase = skillData.duration
else
durationBase = 2
end
local durationMod = calcLib.mod(modDB, dotCfg, "Duration") * calcLib.mod(enemyDB, nil, "SelfPoisonDuration")
globalOutput.PoisonDuration = durationBase * durationMod * debuffDurationMult
output.PoisonDamage = output.PoisonDPS * globalOutput.PoisonDuration
if skillData.showAverage then
output.TotalPoisonAverageDamage = output.HitChance / 100 * output.PoisonChance / 100 * output.PoisonDamage
else
output.TotalPoisonDPS = output.HitChance / 100 * output.PoisonChance / 100 * output.PoisonDamage * (globalOutput.HitSpeed or globalOutput.Speed) * (skillData.dpsMultiplier or 1)
end
if breakdown then
t_insert(breakdown.PoisonDPS, "x 0.08 ^8(poison deals 8% per second)")
t_insert(breakdown.PoisonDPS, s_format("= %.1f", baseVal, 1))
t_insert(breakdown.PoisonDPS, "Poison DPS:")
breakdown.dot(breakdown.PoisonDPS, baseVal, inc, more, nil, effMult, output.PoisonDPS)
if globalOutput.PoisonDuration ~= 2 then
globalBreakdown.PoisonDuration = {
s_format("%.2fs ^8(base duration)", durationBase)
}
if durationMod ~= 1 then
t_insert(globalBreakdown.PoisonDuration, s_format("x %.2f ^8(duration modifier)", durationMod))
end
if debuffDurationMult ~= 1 then
t_insert(globalBreakdown.PoisonDuration, s_format("/ %.2f ^8(debuff expires slower/faster)", 1 / debuffDurationMult))
end
t_insert(globalBreakdown.PoisonDuration, s_format("= %.2fs", globalOutput.PoisonDuration))
end
breakdown.PoisonDamage = { }
if isAttack then
t_insert(breakdown.PoisonDamage, pass.label..":")
end
t_insert(breakdown.PoisonDamage, s_format("%.1f ^8(damage per second)", output.PoisonDPS))
t_insert(breakdown.PoisonDamage, s_format("x %.2fs ^8(poison duration)", globalOutput.PoisonDuration))
t_insert(breakdown.PoisonDamage, s_format("= %.1f ^8damage per poison stack", output.PoisonDamage))
end
end
end
-- Calculate ignite chance and damage
if canDeal.Fire and (output.IgniteChanceOnHit + output.IgniteChanceOnCrit) > 0 then
local sourceHitDmg = 0
local sourceCritDmg = 0
if canDeal.Fire and not modDB:Sum("FLAG", cfg, "FireCannotIgnite") then
sourceHitDmg = sourceHitDmg + output.FireHitAverage
sourceCritDmg = sourceCritDmg + output.FireCritAverage
end
if canDeal.Cold and modDB:Sum("FLAG", cfg, "ColdCanIgnite") then
sourceHitDmg = sourceHitDmg + output.ColdHitAverage
sourceCritDmg = sourceCritDmg + output.ColdCritAverage
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 = calcSecondaryEffectBase("Ignite", sourceHitDmg, sourceCritDmg) * 0.2
if baseVal > 0 then
skillFlags.ignite = true
if not mainSkill.igniteCfg then
mainSkill.igniteCfg = {
skillName = skillCfg.skillName,
slotName = skillCfg.slotName,
flags = bor(band(skillCfg.flags, ModFlag.SourceMask), ModFlag.Dot, skillData.dotIsSpell and ModFlag.Spell or 0),
keywordFlags = skillCfg.keywordFlags,
}
end
local dotCfg = mainSkill.igniteCfg
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 inc = modDB:Sum("INC", dotCfg, "Damage", "FireDamage", "ElementalDamage")
local more = round(modDB:Sum("MORE", dotCfg, "Damage", "FireDamage", "ElementalDamage"), 2)
local burnRateMod = calcLib.mod(modDB, cfg, "IgniteBurnRate")
output.IgniteDPS = baseVal * (1 + inc/100) * more * burnRateMod * effMult
local incDur = modDB:Sum("INC", dotCfg, "EnemyIgniteDuration") + enemyDB:Sum("INC", nil, "SelfIgniteDuration")
local moreDur = enemyDB:Sum("MORE", nil, "SelfIgniteDuration")
globalOutput.IgniteDuration = 4 * (1 + incDur / 100) * moreDur / burnRateMod * debuffDurationMult
if skillFlags.igniteCanStack then
output.IgniteDamage = output.IgniteDPS * globalOutput.IgniteDuration
if skillData.showAverage then
output.TotalIgniteAverageDamage = output.HitChance / 100 * output.IgniteChance / 100 * output.IgniteDamage
else
output.TotalIgniteDPS = output.HitChance / 100 * output.IgniteChance / 100 * output.IgniteDamage * (globalOutput.HitSpeed or globalOutput.Speed) * (skillData.dpsMultiplier or 1)
end
end
if breakdown then
t_insert(breakdown.IgniteDPS, "x 0.2 ^8(ignite deals 20% per second)")
t_insert(breakdown.IgniteDPS, s_format("= %.1f", baseVal, 1))
t_insert(breakdown.IgniteDPS, "Ignite DPS:")
breakdown.dot(breakdown.IgniteDPS, baseVal, inc, more, burnRateMod, effMult, 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))
end
if globalOutput.IgniteDuration ~= 4 then
globalBreakdown.IgniteDuration = {
s_format("4.00s ^8(base duration)", durationBase)
}
if incDur ~= 0 then
t_insert(globalBreakdown.IgniteDuration, s_format("x %.2f ^8(increased/reduced duration)", 1 + incDur/100))
end
if moreDur ~= 1 then
t_insert(globalBreakdown.IgniteDuration, s_format("x %.2f ^8(more/less duration)", moreDur))
end
if burnRateMod ~= 1 then
t_insert(globalBreakdown.IgniteDuration, s_format("/ %.2f ^8(rate modifier)", burnRateMod))
end
if debuffDurationMult ~= 1 then
t_insert(globalBreakdown.IgniteDuration, s_format("/ %.2f ^8(debuff expires slower/faster)", 1 / debuffDurationMult))
end
t_insert(globalBreakdown.IgniteDuration, s_format("= %.2fs", globalOutput.IgniteDuration))
end
end
end
end
-- Calculate shock and freeze chance + duration modifier
if (output.ShockChanceOnHit + output.ShockChanceOnCrit) > 0 then
local sourceHitDmg = 0
local sourceCritDmg = 0
if canDeal.Lightning and not modDB:Sum("FLAG", cfg, "LightningCannotShock") then
sourceHitDmg = sourceHitDmg + output.LightningHitAverage
sourceCritDmg = sourceCritDmg + output.LightningCritAverage
end
if canDeal.Physical and modDB:Sum("FLAG", cfg, "PhysicalCanShock") then
sourceHitDmg = sourceHitDmg + output.PhysicalHitAverage
sourceCritDmg = sourceCritDmg + output.PhysicalCritAverage
end
if canDeal.Fire and modDB:Sum("FLAG", cfg, "FireCanShock") then
sourceHitDmg = sourceHitDmg + output.FireHitAverage
sourceCritDmg = sourceCritDmg + output.FireCritAverage
end
if canDeal.Chaos and modDB:Sum("FLAG", cfg, "ChaosCanShock") then
sourceHitDmg = sourceHitDmg + output.ChaosHitAverage
sourceCritDmg = sourceCritDmg + output.ChaosCritAverage
end
local baseVal = calcSecondaryEffectBase("Shock", sourceHitDmg, sourceCritDmg)
if baseVal > 0 then
skillFlags.shock = true
output.ShockDurationMod = 1 + modDB:Sum("INC", cfg, "EnemyShockDuration") / 100 + enemyDB:Sum("INC", nil, "SelfShockDuration") / 100
if breakdown then
t_insert(breakdown.ShockDPS, s_format("For shock to apply, target must have no more than %d life.", baseVal * 20 * output.ShockDurationMod))
end
end
end
if (output.FreezeChanceOnHit + output.FreezeChanceOnCrit) > 0 then
local sourceHitDmg = 0
local sourceCritDmg = 0
if canDeal.Cold and not modDB:Sum("FLAG", cfg, "ColdCannotFreeze") then
sourceHitDmg = sourceHitDmg + output.ColdHitAverage
sourceCritDmg = sourceCritDmg + output.ColdCritAverage
end
if canDeal.Lightning and modDB:Sum("FLAG", cfg, "LightningCanFreeze") then
sourceHitDmg = sourceHitDmg + output.LightningHitAverage
sourceCritDmg = sourceCritDmg + output.LightningCritAverage
end
local baseVal = calcSecondaryEffectBase("Freeze", sourceHitDmg, sourceCritDmg)
if baseVal > 0 then
skillFlags.freeze = true
output.FreezeDurationMod = 1 + modDB:Sum("INC", cfg, "EnemyFreezeDuration") / 100 + enemyDB:Sum("INC", nil, "SelfFreezeDuration") / 100
if breakdown then
t_insert(breakdown.FreezeDPS, s_format("For freeze to apply, target must have no more than %d life.", baseVal * 20 * output.FreezeDurationMod))
end
end
end
-- Calculate enemy stun modifiers
local enemyStunThresholdRed = -modDB:Sum("INC", cfg, "EnemyStunThreshold")
if enemyStunThresholdRed > 75 then
output.EnemyStunThresholdMod = 1 - (75 + (enemyStunThresholdRed - 75) * 25 / (enemyStunThresholdRed - 50)) / 100
else
output.EnemyStunThresholdMod = 1 - enemyStunThresholdRed / 100
end
local incDur = modDB:Sum("INC", cfg, "EnemyStunDuration")
local incRecov = enemyDB:Sum("INC", nil, "StunRecovery")
output.EnemyStunDuration = 0.35 * (1 + incDur / 100) / (1 + incRecov / 100)
if breakdown then
if output.EnemyStunDuration ~= 0.35 then
breakdown.EnemyStunDuration = {
"0.35s ^8(base duration)"
}
if incDur ~= 0 then
t_insert(breakdown.EnemyStunDuration, s_format("x %.2f ^8(increased/reduced stun duration)", 1 + incDur/100))
end
if incRecov ~= 0 then
t_insert(breakdown.EnemyStunDuration, s_format("/ %.2f ^8(increased/reduced enemy stun recovery)", 1 + incRecov/100))
end
t_insert(breakdown.EnemyStunDuration, s_format("= %.2fs", output.EnemyStunDuration))
end
end
end
-- Combine secondary effect stats
if isAttack then
combineStat("BleedChance", "AVERAGE")
combineStat("BleedDPS", "CHANCE", "BleedChance")
combineStat("PoisonChance", "AVERAGE")
combineStat("PoisonDPS", "CHANCE", "PoisonChance")
combineStat("PoisonDamage", "CHANCE", "PoisonChance")
if skillData.showAverage then
combineStat("TotalPoisonAverageDamage", "DPS")
else
combineStat("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("TotalIgniteDPS", "DPS")
end
end
combineStat("ShockChance", "AVERAGE")
combineStat("ShockDurationMod", "AVERAGE")
combineStat("FreezeChance", "AVERAGE")
combineStat("FreezeDurationMod", "AVERAGE")
end
if skillFlags.hit and skillData.decay then
-- Calculate DPS for Essence of Delirium's Decay effect
skillFlags.decay = true
mainSkill.decayCfg = {
slotName = skillCfg.slotName,
flags = bor(band(skillCfg.flags, ModFlag.SourceMask), ModFlag.Dot, skillData.dotIsSpell and ModFlag.Spell or 0),
keywordFlags = skillCfg.keywordFlags,
}
local dotCfg = mainSkill.decayCfg
local effMult = 1
if env.mode_effective then
local resist = m_min(enemyDB:Sum("BASE", nil, "ChaosResist"), 75)
local taken = enemyDB:Sum("INC", nil, "DamageTaken", "DamageTakenOverTime", "ChaosDamageTaken", "ChaosDamageTakenOverTime")
effMult = (1 - resist / 100) * (1 + taken / 100)
output["DecayEffMult"] = effMult
if breakdown and effMult ~= 1 then
breakdown.DecayEffMult = breakdown.effMult("Chaos", resist, 0, taken, effMult)
end
end
local inc = modDB:Sum("INC", dotCfg, "Damage", "ChaosDamage")
local more = round(modDB:Sum("MORE", dotCfg, "Damage", "ChaosDamage"), 2)
output.DecayDPS = skillData.decay * (1 + inc/100) * more * effMult
local durationMod = calcLib.mod(modDB, dotCfg, "Duration")
output.DecayDuration = 10 * durationMod * debuffDurationMult
if breakdown then
breakdown.DecayDPS = { }
t_insert(breakdown.DecayDPS, "Decay DPS:")
breakdown.dot(breakdown.DecayDPS, skillData.decay, inc, more, nil, effMult, output.DecayDPS)
if output.DecayDuration ~= 2 then
breakdown.DecayDuration = {
s_format("%.2fs ^8(base duration)", 10)
}
if durationMod ~= 1 then
t_insert(breakdown.DecayDuration, s_format("x %.2f ^8(duration modifier)", durationMod))
end
if debuffDurationMult ~= 1 then
t_insert(breakdown.DecayDuration, s_format("/ %.2f ^8(debuff expires slower/faster)", 1 / debuffDurationMult))
end
t_insert(breakdown.DecayDuration, s_format("= %.2fs", output.DecayDuration))
end
end
end
-- Calculate combined DPS estimate, including DoTs
local baseDPS = output[(skillData.showAverage and "AverageDamage") or "TotalDPS"] + output.TotalDot
output.CombinedDPS = baseDPS
if skillFlags.poison then
if skillData.showAverage then
output.CombinedDPS = output.CombinedDPS + output.TotalPoisonAverageDamage
output.WithPoisonAverageDamage = baseDPS + output.TotalPoisonAverageDamage
else
output.CombinedDPS = output.CombinedDPS + output.TotalPoisonDPS
output.WithPoisonDPS = baseDPS + output.TotalPoisonDPS
end
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