1782 lines
78 KiB
Lua
1782 lines
78 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_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(activeSkill, source, cfg, breakdown, damageType, ...)
|
|
local skillModList = activeSkill.skillModList
|
|
|
|
local damageTypeMin = damageType.."Min"
|
|
local damageTypeMax = damageType.."Max"
|
|
|
|
-- Calculate base values
|
|
local damageEffectiveness = activeSkill.activeEffect.grantedEffectLevel.damageEffectiveness or activeSkill.skillData.damageEffectiveness or 1
|
|
local addedMin = skillModList:Sum("BASE", cfg, damageTypeMin)
|
|
local addedMax = skillModList: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 = 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 = calcHitDamage(activeSkill, 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.damageTypes, {
|
|
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 + 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 = (...) 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, 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, "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 }, "PhysicalDamage")) do
|
|
local mod = value.mod
|
|
if band(mod.flags, ModFlag.Claw) ~= 0 then
|
|
skillModList:NewMod("PhysicalDamage", mod.type, mod.value, mod.source, bor(band(mod.flags, bnot(ModFlag.Claw)), ModFlag.Unarmed), mod.keywordFlags, unpack(mod))
|
|
end
|
|
end
|
|
end
|
|
if 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 }, "Speed")) 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
|
|
|
|
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
|
|
output.ChainMax = skillModList:Sum("BASE", skillCfg, "ChainCountMax")
|
|
output.Chain = m_min(output.ChainMax, skillModList:Sum("BASE", skillCfg, "ChainCount"))
|
|
output.ChainRemaining = m_max(0, output.ChainMax - output.Chain)
|
|
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
|
|
output.ProjectileCount = skillModList:Sum("BASE", skillCfg, "ProjectileCount")
|
|
output.PierceChance = m_min(100, skillModList:Sum("BASE", skillCfg, "PierceChance"))
|
|
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 (env.data.weaponTypeInfo["None"].range + 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 (env.data.weaponTypeInfo["None"].range + 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
|
|
baseRadius = skillData.radiusSecondary + (skillData.radiusExtra or 0)
|
|
output.AreaOfEffectRadiusSecondary = m_floor(baseRadius * m_sqrt(output.AreaOfEffectMod))
|
|
if breakdown then
|
|
breakdown.AreaOfEffectRadiusSecondary = breakdown.area(baseRadius, output.AreaOfEffectMod, 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")
|
|
output.TrapCooldown = (skillData.trapCooldown or skillData.cooldown or 4) / 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
|
|
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
|
|
output.Cooldown = 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
|
|
|
|
-- 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")
|
|
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 = activeSkill.activeEffect.grantedEffect.setupFunc
|
|
if setupFunc then
|
|
setupFunc(activeSkill, output)
|
|
end
|
|
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)
|
|
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)
|
|
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)
|
|
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 = { }
|
|
if skillFlags.weapon1Attack then
|
|
if breakdown then
|
|
breakdown.MainHand = LoadModule(calcs.breakdownModule, skillModList, output.MainHand)
|
|
end
|
|
activeSkill.weapon1Cfg.skillStats = output.MainHand
|
|
t_insert(passList, {
|
|
label = "Main Hand",
|
|
source = actor.weaponData1,
|
|
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
|
|
t_insert(passList, {
|
|
label = "Off Hand",
|
|
source = actor.weaponData2,
|
|
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 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
|
|
local inc = skillModList:Sum("INC", cfg, "Speed")
|
|
local more = skillModList:More(cfg, "Speed")
|
|
output.Speed = baseSpeed * round((1 + inc/100) * more, 2)
|
|
if skillData.attackRateCap then
|
|
output.Speed = m_min(output.Speed, skillData.attackRateCap)
|
|
end
|
|
if skillFlags.selfCast then
|
|
-- Self-cast skill; apply action speed
|
|
output.Speed = output.Speed * globalOutput.ActionSpeedMod
|
|
end
|
|
if breakdown then
|
|
breakdown.Speed = { }
|
|
breakdown.multiChain(breakdown.Speed, {
|
|
base = s_format("%.2f ^8(base)", baseSpeed),
|
|
{ "%.2f ^8(increased/reduced)", 1 + inc/100 },
|
|
{ "%.2f ^8(more/less)", more },
|
|
{ "%.2f ^8(action speed modifier)", skillFlags.selfCast and globalOutput.ActionSpeedMod or 1 },
|
|
total = s_format("= %.2f ^8per second", output.Speed)
|
|
})
|
|
end
|
|
if output.Speed == 0 then
|
|
output.Time = 0
|
|
else
|
|
output.Time = 1 / output.Speed
|
|
end
|
|
end
|
|
if skillData.hitTimeOverride then
|
|
output.HitTime = skillData.hitTimeOverride
|
|
output.HitSpeed = 1 / output.HitTime
|
|
end
|
|
end
|
|
|
|
if isAttack then
|
|
-- Combine hit chance and attack speed
|
|
combineStat("HitChance", "AVERAGE")
|
|
combineStat("Speed", "AVERAGE")
|
|
if output.Speed == 0 then
|
|
output.Time = 0
|
|
else
|
|
output.Time = 1 / output.Speed
|
|
end
|
|
if skillFlags.bothWeaponAttack then
|
|
if breakdown then
|
|
breakdown.Speed = {
|
|
"Both weapons:",
|
|
s_format("(%.2f + %.2f) / 2", output.MainHand.Speed, output.OffHand.Speed),
|
|
s_format("= %.2f", output.Speed),
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
for _, pass in ipairs(passList) do
|
|
local globalOutput, globalBreakdown = output, breakdown
|
|
local source, output, cfg, breakdown = pass.source, pass.output, pass.cfg, pass.breakdown
|
|
|
|
-- Calculate crit chance, crit multiplier, and their combined effect
|
|
if skillModList: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 = skillModList:Sum("BASE", cfg, "CritChance")
|
|
local inc = skillModList:Sum("INC", cfg, "CritChance")
|
|
local more = skillModList: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 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 > 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 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
|
|
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
|
|
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
|
|
local noLifeLeech = skillModList:Flag(cfg, "CannotLeechLife") or enemyDB:Flag(nil, "CannotLeechLifeFromSelf")
|
|
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
|
|
if breakdown then
|
|
breakdown[damageType] = { damageTypes = { } }
|
|
end
|
|
min, max = calcHitDamage(activeSkill, source, cfg, breakdown and breakdown[damageType], damageType)
|
|
local convMult = activeSkill.conversionTable[damageType].mult
|
|
local doubleChance = m_min(skillModList: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 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 = skillModList: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 skillModList: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 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", nil, "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", nil, "SelfDamageLifeLeech") / 100
|
|
end
|
|
if lifeLeech > 0 then
|
|
lifeLeechTotal = lifeLeechTotal + (min + max) / 2 * lifeLeech / 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", nil, "SelfDamageManaLeech") / 100
|
|
if manaLeech > 0 then
|
|
manaLeechTotal = manaLeechTotal + (min + max) / 2 * manaLeech / 100
|
|
end
|
|
end
|
|
end
|
|
else
|
|
min, max = 0, 0
|
|
if breakdown then
|
|
breakdown[damageType] = {
|
|
"You can't deal "..damageType.." damage"
|
|
}
|
|
end
|
|
end
|
|
if pass == 1 then
|
|
output[damageType.."CritAverage"] = (min + max) / 2
|
|
totalCritMin = totalCritMin + min
|
|
totalCritMax = totalCritMax + max
|
|
else
|
|
if env.mode == "CALCS" then
|
|
output[damageType.."Min"] = min
|
|
output[damageType.."Max"] = max
|
|
end
|
|
output[damageType.."HitAverage"] = (min + max) / 2
|
|
totalHitMin = totalHitMin + min
|
|
totalHitMax = totalHitMax + max
|
|
end
|
|
end
|
|
if skillData.lifeLeechPerUse then
|
|
lifeLeechTotal = lifeLeechTotal + skillData.lifeLeechPerUse
|
|
end
|
|
if skillData.manaLeechPerUse then
|
|
manaLeechTotal = manaLeechTotal + skillData.manaLeechPerUse
|
|
end
|
|
local portion = (pass == 1) and (output.CritChance / 100) or (1 - output.CritChance / 100)
|
|
if skillModList:Flag(cfg, "InstantLifeLeech") then
|
|
output.LifeLeechInstant = output.LifeLeechInstant + lifeLeechTotal * portion
|
|
else
|
|
output.LifeLeech = output.LifeLeech + lifeLeechTotal * 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
|
|
output.LifeLeechDuration, output.LifeLeechInstances = getLeechInstances(output.LifeLeech, skillModList: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 = (skillModList:Sum("BASE", skillCfg, "LifeOnHit") + enemyDB:Sum("BASE", skillCfg, "SelfLifeOnHit")) * globalOutput.LifeRecoveryMod
|
|
output.EnergyShieldOnHit = (skillModList:Sum("BASE", skillCfg, "EnergyShieldOnHit") + enemyDB:Sum("BASE", skillCfg, "SelfEnergyShieldOnHit")) * globalOutput.EnergyShieldRecoveryMod
|
|
output.ManaOnHit = (skillModList:Sum("BASE", skillCfg, "ManaOnHit") + enemyDB:Sum("BASE", skillCfg, "SelfManaOnHit")) * globalOutput.ManaRecoveryMod
|
|
end
|
|
output.LifeOnHitRate = output.LifeOnHit * hitRate
|
|
output.EnergyShieldOnHitRate = output.EnergyShieldOnHit * hitRate
|
|
output.ManaOnHitRate = output.ManaOnHit * hitRate
|
|
|
|
-- Calculate average damage and final DPS
|
|
output.AverageHit = (totalHitMin + totalHitMax) / 2 * (1 - output.CritChance / 100) + (totalCritMin + totalCritMax) / 2 * output.CritChance / 100
|
|
output.AverageDamage = output.AverageHit * output.HitChance / 100
|
|
output.TotalDPS = output.AverageDamage * (globalOutput.HitSpeed or globalOutput.Speed) * (skillData.dpsMultiplier or 1)
|
|
if breakdown then
|
|
if output.CritEffect ~= 1 then
|
|
breakdown.AverageHit = {
|
|
s_format("%.1f x (1 - %.4f) ^8(damage from non-crits)", (totalHitMin + totalHitMax) / 2, output.CritChance / 100),
|
|
s_format("+ %.1f x %.4f ^8(damage from crits)", (totalCritMin + totalCritMax) / 2, output.CritChance / 100),
|
|
s_format("= %.1f", output.AverageHit),
|
|
}
|
|
end
|
|
if isAttack then
|
|
breakdown.AverageDamage = { }
|
|
t_insert(breakdown.AverageDamage, s_format("%s:", pass.label))
|
|
t_insert(breakdown.AverageDamage, s_format("%.1f ^8(average hit)", output.AverageHit))
|
|
t_insert(breakdown.AverageDamage, s_format("x %.2f ^8(chance to hit)", output.HitChance / 100))
|
|
t_insert(breakdown.AverageDamage, s_format("= %.1f", output.AverageDamage))
|
|
end
|
|
end
|
|
end
|
|
|
|
if isAttack then
|
|
-- Combine crit stats, average damage and DPS
|
|
combineStat("PreEffectiveCritChance", "AVERAGE")
|
|
combineStat("CritChance", "AVERAGE")
|
|
combineStat("CritMultiplier", "AVERAGE")
|
|
combineStat("AverageDamage", "DPS")
|
|
combineStat("TotalDPS", "DPS")
|
|
combineStat("LifeLeechDuration", "DPS")
|
|
combineStat("LifeLeechInstances", "DPS")
|
|
combineStat("LifeLeechInstant", "DPS")
|
|
combineStat("LifeLeechInstantRate", "DPS")
|
|
combineStat("ManaLeechDuration", "DPS")
|
|
combineStat("ManaLeechInstances", "DPS")
|
|
combineStat("ManaLeechInstant", "DPS")
|
|
combineStat("ManaLeechInstantRate", "DPS")
|
|
combineStat("LifeOnHit", "DPS")
|
|
combineStat("LifeOnHitRate", "DPS")
|
|
combineStat("EnergyShieldOnHit", "DPS")
|
|
combineStat("EnergyShieldOnHitRate", "DPS")
|
|
combineStat("ManaOnHit", "DPS")
|
|
combineStat("ManaOnHitRate", "DPS")
|
|
if skillFlags.bothWeaponAttack then
|
|
if breakdown then
|
|
breakdown.AverageDamage = { }
|
|
t_insert(breakdown.AverageDamage, "Both weapons:")
|
|
if skillData.doubleHitsWhenDualWielding then
|
|
t_insert(breakdown.AverageDamage, s_format("%.1f + %.1f ^8(skill hits with both weapons at once)", output.MainHand.AverageDamage, output.OffHand.AverageDamage))
|
|
else
|
|
t_insert(breakdown.AverageDamage, s_format("(%.1f + %.1f) / 2 ^8(skill alternates weapons)", output.MainHand.AverageDamage, output.OffHand.AverageDamage))
|
|
end
|
|
t_insert(breakdown.AverageDamage, s_format("= %.1f", output.AverageDamage))
|
|
end
|
|
end
|
|
end
|
|
if env.mode == "CALCS" then
|
|
if skillData.showAverage then
|
|
output.DisplayDamage = s_format("%.1f average damage", output.AverageDamage)
|
|
else
|
|
output.DisplayDamage = s_format("%.1f DPS", output.TotalDPS)
|
|
end
|
|
end
|
|
if breakdown then
|
|
if isAttack then
|
|
breakdown.TotalDPS = {
|
|
s_format("%.1f ^8(average damage)", output.AverageDamage),
|
|
output.HitSpeed and s_format("x %.2f ^8(hit rate)", output.HitSpeed) or s_format("x %.2f ^8(attack rate)", output.Speed),
|
|
}
|
|
else
|
|
breakdown.TotalDPS = {
|
|
s_format("%.1f ^8(average hit)", output.AverageDamage),
|
|
output.HitSpeed and s_format("x %.2f ^8(hit rate)", output.HitSpeed) or s_format("x %.2f ^8(cast rate)", output.Speed),
|
|
}
|
|
end
|
|
if skillData.dpsMultiplier then
|
|
t_insert(breakdown.TotalDPS, s_format("x %g ^8(DPS multiplier for this skill)", skillData.dpsMultiplier))
|
|
end
|
|
t_insert(breakdown.TotalDPS, s_format("= %.1f", output.TotalDPS))
|
|
end
|
|
|
|
-- Calculate leech rates
|
|
if skillModList:Flag(nil, "GhostReaver") then
|
|
output.LifeLeechRate = 0
|
|
output.LifeLeechPerHit = 0
|
|
output.EnergyShieldLeechInstanceRate = output.EnergyShield * 0.02 * calcLib.mod(skillModList, skillCfg, "LifeLeechRate")
|
|
output.EnergyShieldLeechRate = output.LifeLeechInstantRate * output.EnergyShieldRecoveryMod + m_min(output.LifeLeechInstances * output.EnergyShieldLeechInstanceRate, output.MaxEnergyShieldLeechRate) * output.EnergyShieldRecoveryRateMod
|
|
output.EnergyShieldLeechPerHit = output.LifeLeechInstant * output.EnergyShieldRecoveryMod + m_min(output.EnergyShieldLeechInstanceRate, output.MaxEnergyShieldLeechRate) * output.LifeLeechDuration * output.EnergyShieldRecoveryRateMod
|
|
else
|
|
output.LifeLeechInstanceRate = output.Life * 0.02 * calcLib.mod(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.EnergyShieldLeechRate = 0
|
|
output.EnergyShieldLeechPerHit = 0
|
|
end
|
|
do
|
|
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
|
|
end
|
|
skillFlags.leechES = output.EnergyShieldLeechRate > 0
|
|
skillFlags.leechLife = output.LifeLeechRate > 0
|
|
skillFlags.leechMana = output.ManaLeechRate > 0
|
|
if skillData.showAverage then
|
|
output.LifeLeechGainPerHit = output.LifeLeechPerHit + output.LifeOnHit
|
|
output.EnergyShieldLeechGainPerHit = output.EnergyShieldLeechPerHit + output.EnergyShieldOnHit
|
|
output.ManaLeechGainPerHit = output.ManaLeechPerHit + output.ManaOnHit
|
|
else
|
|
output.LifeLeechGainRate = output.LifeLeechRate + output.LifeOnHitRate
|
|
output.EnergyShieldLeechGainRate = output.EnergyShieldLeechRate + output.EnergyShieldOnHitRate
|
|
output.ManaLeechGainRate = output.ManaLeechRate + output.ManaOnHitRate
|
|
end
|
|
if breakdown then
|
|
if skillFlags.leechLife then
|
|
breakdown.LifeLeech = breakdown.leech(output.LifeLeechInstant, output.LifeLeechInstantRate, output.LifeLeechInstances, output.Life, "LifeLeechRate", output.MaxLifeLeechRate, output.LifeLeechDuration)
|
|
end
|
|
if skillFlags.leechES then
|
|
breakdown.EnergyShieldLeech = breakdown.leech(output.LifeLeechInstant, output.LifeLeechInstantRate, output.LifeLeechInstances, output.EnergyShield, "LifeLeechRate", output.MaxEnergyShieldLeechRate, output.LifeLeechDuration)
|
|
end
|
|
if skillFlags.leechMana then
|
|
breakdown.ManaLeech = breakdown.leech(output.ManaLeechInstant, output.ManaLeechInstantRate, output.ManaLeechInstances, output.Mana, "ManaLeechRate", output.MaxManaLeechRate, output.ManaLeechDuration)
|
|
end
|
|
end
|
|
|
|
-- Calculate skill DOT components
|
|
local dotCfg = {
|
|
skillName = skillCfg.skillName,
|
|
skillPart = skillCfg.skillPart,
|
|
skillTypes = skillCfg.skillTypes,
|
|
slotName = skillCfg.slotName,
|
|
flags = bor(band(skillCfg.flags, ModFlag.SourceMask), ModFlag.Dot, skillData.dotIsSpell and ModFlag.Spell or 0, skillData.dotIsArea and ModFlag.Area or 0),
|
|
keywordFlags = skillCfg.keywordFlags
|
|
}
|
|
activeSkill.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 = skillModList:Sum("INC", dotCfg, "Damage", damageType.."Damage", isElemental[damageType] and "ElementalDamage" or nil)
|
|
local more = round(skillModList: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, 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
|
|
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 skillModList:Flag(cfg, "CannotBleed") then
|
|
output.BleedChanceOnCrit = 0
|
|
else
|
|
output.BleedChanceOnCrit = m_min(100, skillModList:Sum("BASE", cfg, "BleedChance"))
|
|
end
|
|
output.PoisonChanceOnCrit = m_min(100, skillModList:Sum("BASE", cfg, "PoisonChance"))
|
|
if skillModList:Flag(cfg, "CannotIgnite") then
|
|
output.IgniteChanceOnCrit = 0
|
|
else
|
|
output.IgniteChanceOnCrit = 100
|
|
end
|
|
if skillModList:Flag(cfg, "CannotShock") then
|
|
output.ShockChanceOnCrit = 0
|
|
else
|
|
output.ShockChanceOnCrit = 100
|
|
end
|
|
if skillModList:Flag(cfg, "CannotFreeze") then
|
|
output.FreezeChanceOnCrit = 0
|
|
else
|
|
output.FreezeChanceOnCrit = 100
|
|
end
|
|
if skillModList:Flag(cfg, "CannotKnockback") then
|
|
output.KnockbackChanceOnCrit = 0
|
|
else
|
|
output.KnockbackChanceOnCrit = skillModList:Sum("BASE", cfg, "EnemyKnockbackChance")
|
|
end
|
|
cfg.skillCond["CriticalStrike"] = false
|
|
if skillModList:Flag(cfg, "CannotBleed") then
|
|
output.BleedChanceOnHit = 0
|
|
else
|
|
output.BleedChanceOnHit = m_min(100, skillModList:Sum("BASE", cfg, "BleedChance"))
|
|
end
|
|
output.PoisonChanceOnHit = m_min(100, skillModList:Sum("BASE", cfg, "PoisonChance"))
|
|
output.ChaosPoisonChance = m_min(100, skillModList:Sum("BASE", cfg, "ChaosPoisonChance"))
|
|
if 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 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 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 skillModList:Flag(cfg, "CannotKnockback") then
|
|
output.KnockbackChanceOnHit = 0
|
|
else
|
|
output.KnockbackChanceOnHit = skillModList:Sum("BASE", cfg, "EnemyKnockbackChance")
|
|
end
|
|
if skillFlags.attack and skillFlags.projectile and skillModList: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
|
|
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 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 activeSkill.bleedCfg then
|
|
activeSkill.bleedCfg = {
|
|
skillName = skillCfg.skillName,
|
|
skillTypes = skillCfg.skillTypes,
|
|
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 = activeSkill.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 = skillModList:Sum("INC", dotCfg, "Damage", "PhysicalDamage")
|
|
local more = round(skillModList:More(dotCfg, "Damage", "PhysicalDamage"), 2)
|
|
output.BleedDPS = baseVal * (1 + inc/100) * more * effMult
|
|
local durationMod = calcLib.mod(skillModList, 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, 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 + output.ChaosPoisonChance) > 0 then
|
|
local sourceHitDmg = output.ChaosHitAverage
|
|
if output.ChaosPoisonChance > 0 and sourceHitDmg > 0 then
|
|
-- Additional chance for chaos; adjust Physical damage and inflict chance
|
|
local chaosChance = m_min(100, output.PoisonChanceOnHit + output.ChaosPoisonChance)
|
|
sourceHitDmg = sourceHitDmg + output.PhysicalHitAverage * output.PoisonChanceOnHit / chaosChance
|
|
output.PoisonChanceOnHit = chaosChance
|
|
else
|
|
sourceHitDmg = sourceHitDmg + output.PhysicalHitAverage
|
|
end
|
|
local sourceCritDmg = output.ChaosCritAverage
|
|
if output.ChaosPoisonChance > 0 and sourceCritDmg > 0 then
|
|
-- Additional chance for chaos; adjust Physical damage and inflict chance
|
|
local chaosChance = m_min(100, output.PoisonChanceOnCrit + output.ChaosPoisonChance)
|
|
sourceCritDmg = sourceCritDmg + output.PhysicalCritAverage * output.PoisonChanceOnCrit / chaosChance
|
|
output.PoisonChanceOnCrit = chaosChance
|
|
else
|
|
sourceCritDmg = sourceCritDmg + output.PhysicalCritAverage
|
|
end
|
|
local baseVal = calcSecondaryEffectBase("Poison", sourceHitDmg, sourceCritDmg * skillModList:More(cfg, "PoisonDamageOnCrit")) * 0.08
|
|
if baseVal > 0 then
|
|
skillFlags.poison = true
|
|
skillFlags.duration = true
|
|
if not activeSkill.poisonCfg then
|
|
activeSkill.poisonCfg = {
|
|
skillName = skillCfg.skillName,
|
|
skillTypes = skillCfg.skillTypes,
|
|
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 = activeSkill.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 = skillModList:Sum("INC", dotCfg, "Damage", "ChaosDamage")
|
|
local more = round(skillModList: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(skillModList, 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.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.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, 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))
|
|
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
|
|
local sourceHitDmg = 0
|
|
local sourceCritDmg = 0
|
|
if canDeal.Physical and skillModList:Flag(cfg, "PhysicalCanIgnite") then
|
|
sourceHitDmg = sourceHitDmg + output.PhysicalHitAverage
|
|
sourceCritDmg = sourceCritDmg + output.PhysicalCritAverage
|
|
end
|
|
if canDeal.Lightning and skillModList:Flag(cfg, "LightningCanIgnite") then
|
|
sourceHitDmg = sourceHitDmg + output.LightningHitAverage
|
|
sourceCritDmg = sourceCritDmg + output.LightningCritAverage
|
|
end
|
|
if canDeal.Cold and skillModList:Flag(cfg, "ColdCanIgnite") then
|
|
sourceHitDmg = sourceHitDmg + output.ColdHitAverage
|
|
sourceCritDmg = sourceCritDmg + output.ColdCritAverage
|
|
end
|
|
if canDeal.Fire and not skillModList:Flag(cfg, "FireCannotIgnite") then
|
|
sourceHitDmg = sourceHitDmg + output.FireHitAverage
|
|
sourceCritDmg = sourceCritDmg + output.FireCritAverage
|
|
end
|
|
if canDeal.Chaos and skillModList:Flag(cfg, "ChaosCanIgnite") then
|
|
sourceHitDmg = sourceHitDmg + output.ChaosHitAverage
|
|
sourceCritDmg = sourceCritDmg + output.ChaosCritAverage
|
|
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 activeSkill.igniteCfg then
|
|
activeSkill.igniteCfg = {
|
|
skillName = skillCfg.skillName,
|
|
skillTypes = skillCfg.skillTypes,
|
|
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 = activeSkill.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 = skillModList:Sum("INC", dotCfg, "Damage", "FireDamage", "ElementalDamage")
|
|
local more = round(skillModList:More(dotCfg, "Damage", "FireDamage", "ElementalDamage"), 2)
|
|
local burnRateMod = calcLib.mod(skillModList, cfg, "IgniteBurnFaster") / calcLib.mod(skillModList, cfg, "IgniteBurnSlower")
|
|
output.IgniteDPS = baseVal * (1 + inc/100) * more * burnRateMod * effMult
|
|
local incDur = skillModList:Sum("INC", dotCfg, "EnemyIgniteDuration") + enemyDB:Sum("INC", nil, "SelfIgniteDuration")
|
|
local moreDur = enemyDB:More(nil, "SelfIgniteDuration")
|
|
globalOutput.IgniteDuration = 4 * (1 + incDur / 100) * moreDur / burnRateMod * debuffDurationMult
|
|
if skillFlags.igniteCanStack then
|
|
output.IgniteDamage = output.IgniteDPS * globalOutput.IgniteDuration
|
|
if skillData.showAverage then
|
|
output.TotalIgniteAverageDamage = output.HitChance / 100 * output.IgniteChance / 100 * output.IgniteDamage
|
|
else
|
|
output.TotalIgniteStacks = output.HitChance / 100 * output.IgniteChance / 100 * globalOutput.IgniteDuration * (globalOutput.HitSpeed or globalOutput.Speed) * (skillData.dpsMultiplier or 1)
|
|
output.TotalIgniteDPS = output.IgniteDPS * output.TotalIgniteStacks
|
|
end
|
|
end
|
|
if breakdown then
|
|
t_insert(breakdown.IgniteDPS, "x 0.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, nil, 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))
|
|
if not skillData.showAverage then
|
|
breakdown.TotalIgniteStacks = { }
|
|
if isAttack then
|
|
t_insert(breakdown.TotalIgniteStacks, pass.label..":")
|
|
end
|
|
breakdown.multiChain(breakdown.TotalIgniteStacks, {
|
|
base = s_format("%.2fs ^8(ignite duration)", globalOutput.IgniteDuration),
|
|
{ "%.2f ^8(ignite chance)", output.IgniteChance / 100 },
|
|
{ "%.2f ^8(hit chance)", output.HitChance / 100 },
|
|
{ "%.2f ^8(hits per second)", globalOutput.HitSpeed or globalOutput.Speed },
|
|
{ "%g ^8(dps multiplier for this skill)", skillData.dpsMultiplier or 1 },
|
|
total = s_format("= %.1f", output.TotalIgniteStacks),
|
|
})
|
|
end
|
|
end
|
|
if globalOutput.IgniteDuration ~= 4 then
|
|
globalBreakdown.IgniteDuration = {
|
|
s_format("4.00s ^8(base duration)", durationBase)
|
|
}
|
|
if incDur ~= 0 then
|
|
t_insert(globalBreakdown.IgniteDuration, s_format("x %.2f ^8(increased/reduced duration)", 1 + incDur/100))
|
|
end
|
|
if moreDur ~= 1 then
|
|
t_insert(globalBreakdown.IgniteDuration, s_format("x %.2f ^8(more/less duration)", moreDur))
|
|
end
|
|
if burnRateMod ~= 1 then
|
|
t_insert(globalBreakdown.IgniteDuration, s_format("/ %.2f ^8(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.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 = calcSecondaryEffectBase("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 = calcSecondaryEffectBase("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 incDur = skillModList: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("TotalPoisonStacks", "DPS")
|
|
combineStat("TotalPoisonDPS", "DPS")
|
|
end
|
|
combineStat("IgniteChance", "AVERAGE")
|
|
combineStat("IgniteDPS", "CHANCE", "IgniteChance")
|
|
if skillFlags.igniteCanStack then
|
|
combineStat("IgniteDamage", "CHANCE", "IgniteChance")
|
|
if skillData.showAverage then
|
|
combineStat("TotalIgniteAverageDamage", "DPS")
|
|
else
|
|
combineStat("TotalIgniteStacks", "DPS")
|
|
combineStat("TotalIgniteDPS", "DPS")
|
|
end
|
|
end
|
|
combineStat("ShockChance", "AVERAGE")
|
|
combineStat("ShockDurationMod", "AVERAGE")
|
|
combineStat("FreezeChance", "AVERAGE")
|
|
combineStat("FreezeDurationMod", "AVERAGE")
|
|
end
|
|
|
|
if skillFlags.hit and skillData.decay then
|
|
-- Calculate DPS for Essence of Delirium's Decay effect
|
|
skillFlags.decay = true
|
|
activeSkill.decayCfg = {
|
|
slotName = skillCfg.slotName,
|
|
skillTypes = skillCfg.skillTypes,
|
|
flags = bor(band(skillCfg.flags, ModFlag.SourceMask), ModFlag.Dot, skillData.dotIsSpell and ModFlag.Spell or 0),
|
|
keywordFlags = skillCfg.keywordFlags,
|
|
}
|
|
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)
|
|
output.DecayDPS = skillData.decay * (1 + inc/100) * more * effMult
|
|
local durationMod = calcLib.mod(skillModList, 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, 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 |