- 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
1622 lines
69 KiB
Lua
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 |