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