3574 lines
182 KiB
Lua
3574 lines
182 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_abs = math.abs
|
|
local m_floor = math.floor
|
|
local m_ceil = math.ceil
|
|
local m_min = math.min
|
|
local m_max = math.max
|
|
local m_sqrt = math.sqrt
|
|
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 precede 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
|
|
local moreMinDamage = skillModList:More(cfg, "Min"..damageType.."Damage")
|
|
local moreMaxDamage = skillModList:More(cfg, "Max"..damageType.."Damage")
|
|
|
|
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) * moreMinDamage),
|
|
round(((baseMax * inc * more) + addMax) * moreMaxDamage)
|
|
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
|
|
|
|
---Calculates skill radius
|
|
---@param baseRadius number
|
|
---@param areaMod number
|
|
---@return number
|
|
local function calcRadius(baseRadius, areaMod)
|
|
return m_floor(baseRadius * m_floor(100 * m_sqrt(areaMod)) / 100)
|
|
end
|
|
|
|
---Calculates modifiers needed to reach the next and previous radius breakpoints
|
|
---@param baseRadius number
|
|
---@param incArea number @Additive modifier
|
|
---@param moreArea number @Multiplicative modifier
|
|
---@return number, number, number, number @Next breakpoint: increased, more; Previous breakpoint: reduced, less
|
|
local function calcRadiusBreakpoints(baseRadius, incArea, moreArea)
|
|
local radius = calcRadius(baseRadius, round(round(incArea * moreArea, 10), 2))
|
|
local incAreaBreakpoint, redAreaBreakpoint, moreAreaBreakpoint, lessAreaBreakpoint
|
|
if radius > 0 then
|
|
incAreaBreakpoint = 0
|
|
repeat
|
|
incAreaBreakpoint = incAreaBreakpoint + 1
|
|
local newRadius = calcRadius(baseRadius, round(round((incArea + incAreaBreakpoint / 100) * moreArea, 10), 2))
|
|
until (newRadius > radius)
|
|
redAreaBreakpoint = 0
|
|
repeat
|
|
redAreaBreakpoint = redAreaBreakpoint + 1
|
|
local newRadius = calcRadius(baseRadius, round(round((incArea - redAreaBreakpoint / 100) * moreArea, 10), 2))
|
|
until (newRadius < radius)
|
|
moreAreaBreakpoint = 0
|
|
repeat
|
|
moreAreaBreakpoint = moreAreaBreakpoint + 1
|
|
local newRadius = calcRadius(baseRadius, round(round(incArea * moreArea * (1 + moreAreaBreakpoint / 100), 10), 2))
|
|
until (newRadius > radius)
|
|
lessAreaBreakpoint = 0
|
|
repeat
|
|
lessAreaBreakpoint = lessAreaBreakpoint + 1
|
|
local newRadius = calcRadius(baseRadius, round(round(incArea * moreArea * (1 - lessAreaBreakpoint / 100), 10), 2))
|
|
until (newRadius < radius)
|
|
end
|
|
return incAreaBreakpoint, moreAreaBreakpoint, redAreaBreakpoint, lessAreaBreakpoint
|
|
end
|
|
|
|
function calcSkillCooldown(skillModList, skillCfg, skillData)
|
|
local cooldownOverride = skillModList:Override(skillCfg, "CooldownRecovery")
|
|
local cooldown = cooldownOverride or (skillData.cooldown + skillModList:Sum("BASE", skillCfg, "CooldownRecovery")) / calcLib.mod(skillModList, skillCfg, "CooldownRecovery")
|
|
cooldown = m_ceil(cooldown * data.misc.ServerTickRate) / data.misc.ServerTickRate
|
|
return cooldown
|
|
end
|
|
|
|
local function calcWarcryCastTime(skillModList, skillCfg, actor)
|
|
local baseSpeed = 1 / skillModList:Sum("BASE", skillCfg, "WarcryCastTime")
|
|
local warcryCastTime = baseSpeed * calcLib.mod(skillModList, skillCfg, "WarcrySpeed") * calcs.actionSpeedMod(actor)
|
|
warcryCastTime = m_min(warcryCastTime, data.misc.ServerTickRate)
|
|
warcryCastTime = 1 / warcryCastTime
|
|
if skillModList:Flag(skillCfg, "InstantWarcry") then
|
|
warcryCastTime = 0
|
|
end
|
|
return warcryCastTime
|
|
end
|
|
|
|
local function calcSkillDuration(skillModList, skillCfg, skillData, env, enemyDB)
|
|
local durationMod = calcLib.mod(skillModList, skillCfg, "Duration", "PrimaryDuration", "SkillAndDamagingAilmentDuration", skillData.mineDurationAppliesToSkill and "MineDuration" or nil)
|
|
local durationBase = (skillData.duration or 0) + skillModList:Sum("BASE", skillCfg, "Duration", "PrimaryDuration")
|
|
local duration = durationBase * durationMod
|
|
local debuffDurationMult = 1
|
|
if env.mode_effective then
|
|
debuffDurationMult = 1 / m_max(data.misc.BuffExpirationSlowCap, calcLib.mod(enemyDB, skillCfg, "BuffExpireFaster"))
|
|
end
|
|
if skillData.debuff then
|
|
duration = duration * debuffDurationMult
|
|
end
|
|
return duration
|
|
end
|
|
|
|
-- Performs all offensive calculations
|
|
function calcs.offence(env, actor, activeSkill)
|
|
local modDB = actor.modDB
|
|
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
|
|
|
|
local function calcAreaOfEffect(skillModList, skillCfg, skillData, skillFlags, output, breakdown)
|
|
local incArea, moreArea = calcLib.mods(skillModList, skillCfg, "AreaOfEffect")
|
|
output.AreaOfEffectMod = round(round(incArea * moreArea, 10), 2)
|
|
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 = calcRadius(baseRadius, output.AreaOfEffectMod)
|
|
if breakdown then
|
|
local incAreaBreakpoint, moreAreaBreakpoint, redAreaBreakpoint, lessAreaBreakpoint = calcRadiusBreakpoints(baseRadius, incArea, moreArea)
|
|
breakdown.AreaOfEffectRadius = breakdown.area(baseRadius, output.AreaOfEffectMod, output.AreaOfEffectRadius, incAreaBreakpoint, moreAreaBreakpoint, redAreaBreakpoint, lessAreaBreakpoint, skillData.radiusLabel)
|
|
end
|
|
if skillData.radiusSecondary then
|
|
local incAreaSecondary, moreAreaSecondary = calcLib.mods(skillModList, skillCfg, "AreaOfEffect", "AreaOfEffectSecondary")
|
|
output.AreaOfEffectModSecondary = round(round(incAreaSecondary * moreAreaSecondary, 10), 2)
|
|
baseRadius = skillData.radiusSecondary + (skillData.radiusExtra or 0)
|
|
output.AreaOfEffectRadiusSecondary = calcRadius(baseRadius, output.AreaOfEffectModSecondary)
|
|
if breakdown then
|
|
local incAreaBreakpointSecondary, moreAreaBreakpointSecondary, redAreaBreakpointSecondary, lessAreaBreakpointSecondary = calcRadiusBreakpoints(baseRadius, incAreaSecondary, moreAreaSecondary)
|
|
breakdown.AreaOfEffectRadiusSecondary = breakdown.area(baseRadius, output.AreaOfEffectModSecondary, output.AreaOfEffectRadiusSecondary, incAreaBreakpointSecondary, moreAreaBreakpointSecondary, redAreaBreakpointSecondary, lessAreaBreakpointSecondary, skillData.radiusSecondaryLabel)
|
|
end
|
|
end
|
|
if skillData.radiusTertiary then
|
|
local incAreaTertiary, moreAreaTertiary = calcLib.mods(skillModList, skillCfg, "AreaOfEffect", "AreaOfEffectTertiary")
|
|
output.AreaOfEffectModTertiary = round(round(incAreaTertiary * moreAreaTertiary, 10), 2)
|
|
baseRadius = skillData.radiusTertiary + (skillData.radiusExtra or 0)
|
|
output.AreaOfEffectRadiusTertiary = calcRadius(baseRadius, output.AreaOfEffectModTertiary)
|
|
if breakdown then
|
|
local incAreaBreakpointTertiary, moreAreaBreakpointTertiary, redAreaBreakpointTertiary, lessAreaBreakpointTertiary = calcRadiusBreakpoints(baseRadius, incAreaTertiary, moreAreaTertiary)
|
|
breakdown.AreaOfEffectRadiusTertiary = breakdown.area(baseRadius, output.AreaOfEffectModTertiary, output.AreaOfEffectRadiusTertiary, incAreaBreakpointTertiary, moreAreaBreakpointTertiary, redAreaBreakpointTertiary, lessAreaBreakpointTertiary, skillData.radiusTertiaryLabel)
|
|
end
|
|
end
|
|
end
|
|
if breakdown then
|
|
breakdown.AreaOfEffectMod = { }
|
|
breakdown.multiChain(breakdown.AreaOfEffectMod, {
|
|
{ "%.2f ^8(increased/reduced)", 1 + skillModList:Sum("INC", skillCfg, "AreaOfEffect") / 100 },
|
|
{ "%.2f ^8(more/less)", skillModList:More(skillCfg, "AreaOfEffect") },
|
|
total = s_format("= %.2f", output.AreaOfEffectMod),
|
|
})
|
|
end
|
|
end
|
|
|
|
local function runSkillFunc(name)
|
|
local func = activeSkill.activeEffect.grantedEffect[name]
|
|
if func then
|
|
func(activeSkill, output)
|
|
end
|
|
end
|
|
|
|
runSkillFunc("initialFunc")
|
|
|
|
-- 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 or 0, "Strength", bor(ModFlag.Attack, ModFlag.Projectile))
|
|
end
|
|
if skillModList:Flag(nil, "IronWill") then
|
|
skillModList:NewMod("Damage", "INC", actor.strDmgBonus or 0, "Strength", ModFlag.Spell)
|
|
end
|
|
|
|
if skillModList:Flag(nil, "TransfigurationOfBody") then
|
|
skillModList:NewMod("Damage", "INC", m_floor(skillModList:Sum("INC", nil, "Life") * data.misc.Transfiguration), "Transfiguration of Body", ModFlag.Attack)
|
|
end
|
|
if skillModList:Flag(nil, "TransfigurationOfMind") then
|
|
skillModList:NewMod("Damage", "INC", m_floor(skillModList:Sum("INC", nil, "Mana") * data.misc.Transfiguration), "Transfiguration of Mind")
|
|
end
|
|
if skillModList:Flag(nil, "TransfigurationOfSoul") then
|
|
skillModList:NewMod("Damage", "INC", m_floor(skillModList:Sum("INC", nil, "EnergyShield") * data.misc.Transfiguration), "Transfiguration of Soul", ModFlag.Spell)
|
|
end
|
|
|
|
-- modType: To look for "INC" or "BASE" for getting the percent conversion
|
|
-- modName: Mod name to look for getting the percent conversion
|
|
local getConversionMultiplier = function(modType, modName)
|
|
-- Default to 100% conversion
|
|
local multiplier = 1
|
|
if modType and modName then
|
|
local maxIncrease = 0
|
|
for i, value in ipairs(skillModList:Tabulate(modType, skillCfg, modName)) do
|
|
maxIncrease = m_max(maxIncrease, value.mod.value)
|
|
end
|
|
-- Convert from percent to fraction
|
|
multiplier = maxIncrease / 100.
|
|
end
|
|
return multiplier
|
|
end
|
|
|
|
-- Correct the tags on conversion with multipliers so they carry over correctly
|
|
local getConvertedModTags = function(mod, multiplier, minionMods)
|
|
local modifiers = modLib.extractModTags(mod)
|
|
for k, value in ipairs(modifiers) do
|
|
if minionMods and value.type == "ActorCondition" and value.actor == "parent" then
|
|
modifiers[k] = { type = "Condition", var = value.var }
|
|
elseif value.limitTotal then
|
|
-- LimitTotal can apply to 'per stat' or 'multiplier', so just copy the whole and update the limit
|
|
local copy = copyTable(value)
|
|
copy.limit = copy.limit * multiplier
|
|
modifiers[k] = copy
|
|
end
|
|
end
|
|
return modifiers
|
|
end
|
|
|
|
if skillModList:Flag(nil, "MinionDamageAppliesToPlayer") then
|
|
-- Minion Damage conversion from Spiritual Aid and The Scourge
|
|
local multiplier = getConversionMultiplier("INC", "ImprovedMinionDamageAppliesToPlayer")
|
|
for _, value in ipairs(skillModList:List(skillCfg, "MinionModifier")) do
|
|
if value.mod.name == "Damage" and value.mod.type == "INC" then
|
|
local mod = value.mod
|
|
local modifiers = getConvertedModTags(mod, multiplier, true)
|
|
skillModList:NewMod("Damage", "INC", mod.value * multiplier, mod.source, mod.flags, mod.keywordFlags, unpack(modifiers))
|
|
end
|
|
end
|
|
end
|
|
if skillModList:Flag(nil, "MinionAttackSpeedAppliesToPlayer") then
|
|
-- Minion Damage conversion from Spiritual Command
|
|
local multiplier = getConversionMultiplier("INC", "ImprovedMinionAttackSpeedAppliesToPlayer")
|
|
-- 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
|
|
local modifiers = getConvertedModTags(value.mod, multiplier, true)
|
|
skillModList:NewMod("Speed", "INC", value.mod.value * multiplier, value.mod.source, ModFlag.Attack, value.mod.keywordFlags, unpack(modifiers))
|
|
end
|
|
end
|
|
end
|
|
if skillModList:Flag(nil, "SpellDamageAppliesToAttacks") then
|
|
-- Spell Damage conversion from Crown of Eyes, Kinetic Bolt, and the Wandslinger notable
|
|
local multiplier = getConversionMultiplier("INC", "ImprovedSpellDamageAppliesToAttacks")
|
|
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
|
|
local modifiers = getConvertedModTags(mod, multiplier)
|
|
skillModList:NewMod("Damage", "INC", mod.value * multiplier, mod.source, bor(band(mod.flags, bnot(ModFlag.Spell)), ModFlag.Attack), mod.keywordFlags, unpack(modifiers))
|
|
end
|
|
end
|
|
end
|
|
|
|
if skillModList:Flag(nil, "CastSpeedAppliesToAttacks") then
|
|
-- Get all increases for this; assumption is that multiple sources would not stack, so find the max
|
|
local multiplier = getConversionMultiplier("INC", "ImprovedCastSpeedAppliesToAttacks")
|
|
for i, value in ipairs(skillModList:Tabulate("INC", { flags = ModFlag.Cast }, "Speed")) do
|
|
local mod = value.mod
|
|
-- Add a new mod for all mods that are cast only
|
|
-- Replace this with a single mod for the sum?
|
|
if band(mod.flags, ModFlag.Cast) ~= 0 then
|
|
local modifiers = getConvertedModTags(mod, multiplier)
|
|
skillModList:NewMod("Speed", "INC", mod.value * multiplier, mod.source, bor(band(mod.flags, bnot(ModFlag.Cast)), ModFlag.Attack), mod.keywordFlags, unpack(modifiers))
|
|
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, keywordFlags = KeywordFlag.Hit }, "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, ModFlag.Hit) }, "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 = bor(ModFlag.Claw, ModFlag.Hit) }, "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, "ClawCritChanceAppliesToMinions") then
|
|
-- Claw Crit Chance conversion from Law of the Wilds
|
|
for i, value in ipairs(skillModList:Tabulate("INC", { flags = bor(ModFlag.Claw, ModFlag.Hit) }, "CritChance")) do
|
|
local mod = value.mod
|
|
if band(mod.flags, ModFlag.Claw) ~= 0 then
|
|
env.minion.modDB:NewMod("CritChance", mod.type, mod.value, mod.source)
|
|
end
|
|
end
|
|
end
|
|
if skillModList:Flag(nil, "ClawCritMultiplierAppliesToMinions") then
|
|
-- Claw Crit Multi conversion from Law of the Wilds
|
|
for i, value in ipairs(skillModList:Tabulate("BASE", { flags = bor(ModFlag.Claw, ModFlag.Hit) }, "CritMultiplier")) do
|
|
local mod = value.mod
|
|
if band(mod.flags, ModFlag.Claw) ~= 0 then
|
|
env.minion.modDB:NewMod("CritMultiplier", mod.type, mod.value, mod.source)
|
|
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
|
|
if skillData.arrowSpeedAppliesToAreaOfEffect then
|
|
-- Arrow Speed conversion for Galvanic Arrow
|
|
for i, value in ipairs(skillModList:Tabulate("INC", { flags = ModFlag.Bow }, "ProjectileSpeed")) do
|
|
local mod = value.mod
|
|
skillModList:NewMod("AreaOfEffect", "INC", mod.value, mod.source, mod.flags, mod.keywordFlags, unpack(mod))
|
|
end
|
|
end
|
|
if skillData.projectileSpeedAppliesToMSAreaOfEffect then
|
|
-- Projectile Speed conversion for Molten Stikes Projectile Range
|
|
for i, value in ipairs(skillModList:Tabulate("INC", { }, "ProjectileSpeed")) do
|
|
local mod = value.mod
|
|
skillModList:NewMod("AreaOfEffectSecondary", "INC", mod.value, mod.source, mod.flags, mod.keywordFlags, unpack(mod))
|
|
skillModList:NewMod("AreaOfEffectTertiary", "INC", mod.value, mod.source, mod.flags, mod.keywordFlags, unpack(mod))
|
|
end
|
|
for i, value in ipairs(skillModList:Tabulate("MORE", { }, "ProjectileSpeed")) do
|
|
local mod = value.mod
|
|
skillModList:NewMod("AreaOfEffectSecondary", "MORE", mod.value, mod.source, mod.flags, mod.keywordFlags, unpack(mod))
|
|
skillModList:NewMod("AreaOfEffectTertiary", "MORE", mod.value, mod.source, mod.flags, mod.keywordFlags, unpack(mod))
|
|
end
|
|
end
|
|
if skillModList:Flag(nil, "SequentialProjectiles") and not skillModList:Flag(nil, "OneShotProj") and not skillModList:Flag(nil,"NoAdditionalProjectiles") and not skillModList:Flag(nil, "TriggeredBySnipe") then
|
|
-- Applies DPS multiplier based on projectile count
|
|
skillData.dpsMultiplier = skillModList:Sum("BASE", skillCfg, "ProjectileCount")
|
|
end
|
|
if skillData.gainPercentBaseWandDamage then
|
|
local mult = skillData.gainPercentBaseWandDamage / 100
|
|
if actor.weaponData1.type == "Wand" and actor.weaponData2.type == "Wand" then
|
|
for _, damageType in ipairs(dmgTypeList) do
|
|
skillModList:NewMod(damageType.."Min", "BASE", ((actor.weaponData1[damageType.."Min"] or 0) + (actor.weaponData2[damageType.."Min"] or 0)) / 2 * mult, "Spellslinger")
|
|
skillModList:NewMod(damageType.."Max", "BASE", ((actor.weaponData1[damageType.."Max"] or 0) + (actor.weaponData2[damageType.."Max"] or 0)) / 2 * mult, "Spellslinger")
|
|
end
|
|
elseif actor.weaponData1.type == "Wand" then
|
|
for _, damageType in ipairs(dmgTypeList) do
|
|
skillModList:NewMod(damageType.."Min", "BASE", (actor.weaponData1[damageType.."Min"] or 0) * mult, "Spellslinger")
|
|
skillModList:NewMod(damageType.."Max", "BASE", (actor.weaponData1[damageType.."Max"] or 0) * mult, "Spellslinger")
|
|
end
|
|
elseif actor.weaponData2.type == "Wand" then
|
|
for _, damageType in ipairs(dmgTypeList) do
|
|
skillModList:NewMod(damageType.."Min", "BASE", (actor.weaponData2[damageType.."Min"] or 0) * mult, "Spellslinger")
|
|
skillModList:NewMod(damageType.."Max", "BASE", (actor.weaponData2[damageType.."Max"] or 0) * mult, "Spellslinger")
|
|
end
|
|
end
|
|
end
|
|
if skillModList:Flag(nil, "TriggeredBySnipe") and activeSkill.skillTypes[SkillType.Triggerable] then
|
|
skillModList:NewMod("Damage", "MORE", 165, "Config", ModFlag.Hit, { type = "Multiplier", var = "SnipeStage" } )
|
|
skillModList:NewMod("Damage", "MORE", 120, "Config", ModFlag.Ailment, { type = "Multiplier", var = "SnipeStage" } )
|
|
end
|
|
if skillModList:Sum("BASE", nil, "CritMultiplierAppliesToDegen") > 0 then
|
|
for i, value in ipairs(skillModList:Tabulate("BASE", skillCfg, "CritMultiplier")) do
|
|
local mod = value.mod
|
|
if mod.source ~= "Base" then -- The global base Crit Multi doesn't apply to ailments with Perfect Agony
|
|
skillModList:NewMod("DotMultiplier", "BASE", m_floor(mod.value / 2), mod.source, ModFlag.Ailment, { type = "Condition", var = "CriticalStrike" }, unpack(mod))
|
|
end
|
|
end
|
|
end
|
|
if skillModList:Sum("BASE", skillCfg, "PhysicalDamageGainAsRandom", "PhysicalDamageConvertToRandom", "PhysicalDamageGainAsColdOrLightning") > 0 then
|
|
skillFlags.randomPhys = true
|
|
local physMode = env.configInput.physMode or "AVERAGE"
|
|
for i, value in ipairs(skillModList:Tabulate("BASE", skillCfg, "PhysicalDamageGainAsRandom")) do
|
|
local mod = value.mod
|
|
local effVal = mod.value / 3
|
|
if physMode == "AVERAGE" then
|
|
skillModList:NewMod("PhysicalDamageGainAsFire", "BASE", effVal, mod.source, mod.flags, mod.keywordFlags, unpack(mod))
|
|
skillModList:NewMod("PhysicalDamageGainAsCold", "BASE", effVal, mod.source, mod.flags, mod.keywordFlags, unpack(mod))
|
|
skillModList:NewMod("PhysicalDamageGainAsLightning", "BASE", effVal, mod.source, mod.flags, mod.keywordFlags, unpack(mod))
|
|
elseif physMode == "FIRE" then
|
|
skillModList:NewMod("PhysicalDamageGainAsFire", "BASE", mod.value, mod.source, mod.flags, mod.keywordFlags, unpack(mod))
|
|
elseif physMode == "COLD" then
|
|
skillModList:NewMod("PhysicalDamageGainAsCold", "BASE", mod.value, mod.source, mod.flags, mod.keywordFlags, unpack(mod))
|
|
elseif physMode == "LIGHTNING" then
|
|
skillModList:NewMod("PhysicalDamageGainAsLightning", "BASE", mod.value, mod.source, mod.flags, mod.keywordFlags, unpack(mod))
|
|
end
|
|
end
|
|
for i, value in ipairs(skillModList:Tabulate("BASE", skillCfg, "PhysicalDamageConvertToRandom")) do
|
|
local mod = value.mod
|
|
local effVal = mod.value / 3
|
|
if physMode == "AVERAGE" then
|
|
skillModList:NewMod("PhysicalDamageConvertToFire", "BASE", effVal, mod.source, mod.flags, mod.keywordFlags, unpack(mod))
|
|
skillModList:NewMod("PhysicalDamageConvertToCold", "BASE", effVal, mod.source, mod.flags, mod.keywordFlags, unpack(mod))
|
|
skillModList:NewMod("PhysicalDamageConvertToLightning", "BASE", effVal, mod.source, mod.flags, mod.keywordFlags, unpack(mod))
|
|
elseif physMode == "FIRE" then
|
|
skillModList:NewMod("PhysicalDamageConvertToFire", "BASE", mod.value, mod.source, mod.flags, mod.keywordFlags, unpack(mod))
|
|
elseif physMode == "COLD" then
|
|
skillModList:NewMod("PhysicalDamageConvertToCold", "BASE", mod.value, mod.source, mod.flags, mod.keywordFlags, unpack(mod))
|
|
elseif physMode == "LIGHTNING" then
|
|
skillModList:NewMod("PhysicalDamageConvertToLightning", "BASE", mod.value, mod.source, mod.flags, mod.keywordFlags, unpack(mod))
|
|
end
|
|
end
|
|
for i, value in ipairs(skillModList:Tabulate("BASE", skillCfg, "PhysicalDamageGainAsColdOrLightning")) do
|
|
local mod = value.mod
|
|
local effVal = mod.value / 2
|
|
if physMode == "AVERAGE" or physMode == "FIRE" then
|
|
skillModList:NewMod("PhysicalDamageGainAsCold", "BASE", effVal, mod.source, mod.flags, mod.keywordFlags, unpack(mod))
|
|
skillModList:NewMod("PhysicalDamageGainAsLightning", "BASE", effVal, mod.source, mod.flags, mod.keywordFlags, unpack(mod))
|
|
elseif physMode == "COLD" then
|
|
skillModList:NewMod("PhysicalDamageGainAsCold", "BASE", mod.value, mod.source, mod.flags, mod.keywordFlags, unpack(mod))
|
|
elseif physMode == "LIGHTNING" then
|
|
skillModList:NewMod("PhysicalDamageGainAsLightning", "BASE", mod.value, mod.source, mod.flags, mod.keywordFlags, unpack(mod))
|
|
end
|
|
end
|
|
end
|
|
|
|
local isAttack = skillFlags.attack
|
|
|
|
runSkillFunc("preSkillTypeFunc")
|
|
|
|
-- 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", not skillFlags.projectile and "BeamChainCountMax" or nil)
|
|
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", 30, "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
|
|
if skillModList:Flag(skillCfg, "NoAdditionalProjectiles") then
|
|
output.ProjectileCount = 1
|
|
else
|
|
local projBase = skillModList:Sum("BASE", skillCfg, "ProjectileCount")
|
|
local projMore = skillModList:More(skillCfg, "ProjectileCount")
|
|
output.ProjectileCount = m_floor(projBase * projMore)
|
|
end
|
|
if skillModList:Flag(skillCfg, "CannotFork") then
|
|
output.ForkCountString = "Cannot fork"
|
|
elseif skillModList:Flag(skillCfg, "ForkOnce") then
|
|
skillFlags.forking = true
|
|
if skillModList:Flag(skillCfg, "ForkTwice") then
|
|
output.ForkCountMax = m_min(skillModList:Sum("BASE", skillCfg, "ForkCountMax"), 2)
|
|
else
|
|
output.ForkCountMax = m_min(skillModList:Sum("BASE", skillCfg, "ForkCountMax"), 1)
|
|
end
|
|
output.ForkedCount = m_min(output.ForkCountMax, skillModList:Sum("BASE", skillCfg, "ForkedCount"))
|
|
output.ForkCountString = output.ForkCountMax
|
|
output.ForkRemaining = m_max(0, output.ForkCountMax - output.ForkedCount)
|
|
else
|
|
output.ForkCountString = "0"
|
|
end
|
|
if skillModList:Flag(skillCfg, "CannotPierce") then
|
|
output.PierceCount = 0
|
|
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(skillModList, skillCfg, "ProjectileSpeed")
|
|
end
|
|
end
|
|
if skillFlags.melee then
|
|
if skillFlags.weapon1Attack then
|
|
actor.weaponRange1 = (actor.weaponData1.range and actor.weaponData1.range + skillModList:Sum("BASE", activeSkill.weapon1Cfg, "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", activeSkill.weapon2Cfg, "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 or (skillFlags.mine and activeSkill.skillTypes[SkillType.Aura]) then
|
|
calcAreaOfEffect(skillModList, skillCfg, skillData, skillFlags, output, breakdown)
|
|
end
|
|
if activeSkill.skillTypes[SkillType.Aura] then
|
|
output.AuraEffectMod = calcLib.mod(skillModList, skillCfg, "AuraEffect")
|
|
if breakdown then
|
|
breakdown.AuraEffectMod = breakdown.mod(skillModList, skillCfg, "AuraEffect")
|
|
end
|
|
end
|
|
if activeSkill.skillTypes[SkillType.Hex] or activeSkill.skillTypes[SkillType.Mark]then
|
|
output.CurseEffectMod = calcLib.mod(skillModList, skillCfg, "CurseEffect")
|
|
if breakdown then
|
|
breakdown.CurseEffectMod = breakdown.mod(skillModList, skillCfg, "CurseEffect")
|
|
end
|
|
end
|
|
if skillFlags.trap then
|
|
local baseSpeed = 1 / skillModList:Sum("BASE", skillCfg, "TrapThrowingTime")
|
|
local timeMod = calcLib.mod(skillModList, skillCfg, "SkillTrapThrowingTime")
|
|
if timeMod > 0 then
|
|
baseSpeed = baseSpeed * (1 / timeMod)
|
|
end
|
|
output.TrapThrowingSpeed = baseSpeed * calcLib.mod(skillModList, skillCfg, "TrapThrowingSpeed") * output.ActionSpeedMod
|
|
output.TrapThrowingSpeed = m_min(output.TrapThrowingSpeed, data.misc.ServerTickRate)
|
|
output.TrapThrowingTime = 1 / output.TrapThrowingSpeed
|
|
if breakdown then
|
|
breakdown.TrapThrowingSpeed = { }
|
|
breakdown.multiChain(breakdown.TrapThrowingSpeed, {
|
|
label = "Throwing rate:",
|
|
base = s_format("%.2f ^8(base throwing rate)", 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
|
|
if breakdown and timeMod > 0 then
|
|
breakdown.TrapThrowingTime = { }
|
|
breakdown.multiChain(breakdown.TrapThrowingTime, {
|
|
label = "Throwing time:",
|
|
base = s_format("%.2f ^8(base throwing time)", 1 / (output.TrapThrowingSpeed * timeMod)),
|
|
{ "%.2f ^8(total modifier)", timeMod },
|
|
total = s_format("= %.2f ^8seconds per throw", output.TrapThrowingTime),
|
|
})
|
|
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 incArea, moreArea = calcLib.mods(skillModList, skillCfg, "TrapTriggerAreaOfEffect")
|
|
local areaMod = round(round(incArea * moreArea, 10), 2)
|
|
output.TrapTriggerRadius = calcRadius(data.misc.TrapTriggerRadiusBase, areaMod)
|
|
if breakdown then
|
|
local incAreaBreakpoint, moreAreaBreakpoint, redAreaBreakpoint, lessAreaBreakpoint = calcRadiusBreakpoints(data.misc.TrapTriggerRadiusBase, incArea, moreArea)
|
|
breakdown.TrapTriggerRadius = breakdown.area(data.misc.TrapTriggerRadiusBase, areaMod, output.TrapTriggerRadius, incAreaBreakpoint, moreAreaBreakpoint, redAreaBreakpoint, lessAreaBreakpoint)
|
|
end
|
|
elseif skillData.cooldown then
|
|
output.Cooldown = calcSkillCooldown(skillModList, skillCfg, skillData)
|
|
if breakdown then
|
|
breakdown.Cooldown = {
|
|
s_format("%.2fs ^8(base)", skillData.cooldown + skillModList:Sum("BASE", skillCfg, "CooldownRecovery")),
|
|
s_format("/ %.2f ^8(increased/reduced cooldown recovery)", 1 + skillModList:Sum("INC", skillCfg, "CooldownRecovery") / 100),
|
|
s_format("rounded up to nearest server tick"),
|
|
s_format("= %.2fs", output.Cooldown)
|
|
}
|
|
end
|
|
end
|
|
if skillFlags.mine then
|
|
local baseSpeed = 1 / skillModList:Sum("BASE", skillCfg, "MineLayingTime")
|
|
local timeMod = calcLib.mod(skillModList, skillCfg, "SkillMineThrowingTime")
|
|
if timeMod > 0 then
|
|
baseSpeed = baseSpeed * (1 / timeMod)
|
|
end
|
|
output.MineLayingSpeed = baseSpeed * calcLib.mod(skillModList, skillCfg, "MineLayingSpeed") * output.ActionSpeedMod
|
|
output.MineLayingSpeed = m_min(output.MineLayingSpeed, data.misc.ServerTickRate)
|
|
output.MineLayingTime = 1 / output.MineLayingSpeed
|
|
if breakdown then
|
|
breakdown.MineLayingTime = { }
|
|
breakdown.multiChain(breakdown.MineLayingTime, {
|
|
label = "Throwing rate:",
|
|
base = s_format("%.2f ^8(base throwing rate)", baseSpeed),
|
|
{ "%.2f ^8(increased/reduced throwing speed)", 1 + skillModList:Sum("INC", skillCfg, "MineLayingSpeed") / 100 },
|
|
{ "%.2f ^8(more/less throwing speed)", skillModList:More(skillCfg, "MineLayingSpeed") },
|
|
{ "%.2f ^8(action speed modifier)", output.ActionSpeedMod },
|
|
total = s_format("= %.2f ^8per second", output.MineLayingSpeed),
|
|
})
|
|
end
|
|
if breakdown and timeMod > 0 then
|
|
breakdown.MineThrowingTime = { }
|
|
breakdown.multiChain(breakdown.MineThrowingTime, {
|
|
label = "Throwing time:",
|
|
base = s_format("%.2f ^8(base throwing time)", 1 / (output.MineLayingSpeed * timeMod)),
|
|
{ "%.2f ^8(total modifier)", timeMod },
|
|
total = s_format("= %.2f ^8seconds per throw", output.MineLayingTime),
|
|
})
|
|
end
|
|
output.ActiveMineLimit = skillModList:Sum("BASE", skillCfg, "ActiveMineLimit")
|
|
local incArea, moreArea = calcLib.mods(skillModList, skillCfg, "MineDetonationAreaOfEffect")
|
|
local areaMod = round(round(incArea * moreArea, 10), 2)
|
|
output.MineDetonationRadius = calcRadius(data.misc.MineDetonationRadiusBase, areaMod)
|
|
if breakdown then
|
|
local incAreaBreakpoint, moreAreaBreakpoint, redAreaBreakpoint, lessAreaBreakpoint = calcRadiusBreakpoints(data.misc.MineDetonationRadiusBase, incArea, moreArea)
|
|
breakdown.MineDetonationRadius = breakdown.area(data.misc.MineDetonationRadiusBase, areaMod, output.MineDetonationRadius, incAreaBreakpoint, moreAreaBreakpoint, redAreaBreakpoint, lessAreaBreakpoint)
|
|
end
|
|
if activeSkill.skillTypes[SkillType.Aura] then
|
|
output.MineAuraRadius = calcRadius(data.misc.MineAuraRadiusBase, output.AreaOfEffectMod)
|
|
if breakdown then
|
|
local incArea, moreArea = calcLib.mods(skillModList, skillCfg, "AreaOfEffect")
|
|
local incAreaBreakpoint, moreAreaBreakpoint, redAreaBreakpoint, lessAreaBreakpoint = calcRadiusBreakpoints(data.misc.MineAuraRadiusBase, incArea, moreArea)
|
|
breakdown.MineAuraRadius = breakdown.area(data.misc.MineAuraRadiusBase, output.AreaOfEffectMod, output.MineAuraRadius, incAreaBreakpoint, moreAreaBreakpoint, redAreaBreakpoint, lessAreaBreakpoint)
|
|
end
|
|
end
|
|
end
|
|
if skillFlags.totem then
|
|
if skillFlags.ballista then
|
|
baseSpeed = 1 / skillModList:Sum("BASE", skillCfg, "BallistaPlacementTime")
|
|
else
|
|
baseSpeed = 1 / skillModList:Sum("BASE", skillCfg, "TotemPlacementTime")
|
|
end
|
|
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", "ActiveBallistaLimit")
|
|
output.TotemsSummoned = env.modDB:Override(nil, "TotemsSummoned") or output.ActiveTotemLimit
|
|
if breakdown then
|
|
breakdown.ActiveTotemLimit = {
|
|
"Totems Summoned: "..output.TotemsSummoned..(env.configInput.TotemsSummoned and " ^8(overridden from the Configuration tab)" or " ^8(can be overridden in the Configuration tab)"),
|
|
}
|
|
end
|
|
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(skillModList, 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
|
|
if skillFlags.brand then
|
|
output.BrandAttachmentRange = calcLib.mod(skillModList, skillCfg, "BrandAttachmentRange")
|
|
output.ActiveBrandLimit = skillModList:Sum("BASE", skillCfg, "ActiveBrandLimit")
|
|
end
|
|
|
|
if skillFlags.warcry then
|
|
output.WarcryCastTime = calcWarcryCastTime(skillModList, skillCfg, actor)
|
|
end
|
|
|
|
-- Skill duration
|
|
local debuffDurationMult = 1
|
|
if env.mode_effective then
|
|
debuffDurationMult = 1 / m_max(data.misc.BuffExpirationSlowCap, calcLib.mod(enemyDB, skillCfg, "BuffExpireFaster"))
|
|
end
|
|
do
|
|
output.DurationMod = calcLib.mod(skillModList, skillCfg, "Duration", "PrimaryDuration", "SkillAndDamagingAilmentDuration", skillData.mineDurationAppliesToSkill and "MineDuration" or nil)
|
|
if breakdown then
|
|
breakdown.DurationMod = breakdown.mod(skillModList, skillCfg, "Duration", "PrimaryDuration", "SkillAndDamagingAilmentDuration", skillData.mineDurationAppliesToSkill and "MineDuration" or nil)
|
|
if breakdown.DurationMod and skillData.durationSecondary then
|
|
t_insert(breakdown.DurationMod, 1, "Primary duration:")
|
|
end
|
|
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", skillData.mineDurationAppliesToSkill and "MineDuration" or nil)
|
|
output.DurationSecondary = durationBase * durationMod
|
|
if skillData.debuffSecondary then
|
|
output.DurationSecondary = output.DurationSecondary * debuffDurationMult
|
|
end
|
|
if breakdown and output.DurationSecondary ~= durationBase then
|
|
breakdown.SecondaryDurationMod = breakdown.mod(skillModList, skillCfg, "Duration", "SecondaryDuration", "SkillAndDamagingAilmentDuration", skillData.mineDurationAppliesToSkill and "MineDuration" or nil)
|
|
if breakdown.SecondaryDurationMod then
|
|
t_insert(breakdown.SecondaryDurationMod, 1, "Secondary duration:")
|
|
end
|
|
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
|
|
|
|
-- Calculate mana cost (may be slightly off due to rounding differences)
|
|
do
|
|
local mult = m_floor(skillModList:More(skillCfg, "SupportManaMultiplier") * 100 + 0.0001) / 100
|
|
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
|
|
if skillData.baseManaCostIsAtLeastPercentUnreservedMana then
|
|
manaCost = m_max(manaCost, m_floor((output.ManaUnreserved or 0) * skillData.baseManaCostIsAtLeastPercentUnreservedMana / 100))
|
|
end
|
|
output.ManaCost = m_floor(manaCost * mult)
|
|
output.ManaCost = m_floor(m_abs(inc / 100) * output.ManaCost) * (inc >= 0 and 1 or -1) + output.ManaCost
|
|
output.ManaCost = m_floor(m_abs(more - 1) * output.ManaCost) * (more >= 1 and 1 or -1) + output.ManaCost
|
|
output.ManaCost = m_max(0, m_floor(output.ManaCost + 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 mult ~= 1 then
|
|
t_insert(breakdown.ManaCost, s_format("x %.2f ^8(mana cost multiplier)", mult))
|
|
end
|
|
if inc ~= 0 then
|
|
t_insert(breakdown.ManaCost, s_format("x %.2f ^8(increased/reduced mana cost)", 1 + inc/100))
|
|
end
|
|
if more ~= 1 then
|
|
t_insert(breakdown.ManaCost, s_format("x %.2f ^8(more/less mana cost)", more))
|
|
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
|
|
|
|
runSkillFunc("preDamageFunc")
|
|
|
|
-- Handle corpse explosions
|
|
if skillData.explodeCorpse and skillData.corpseLife then
|
|
local damageType = skillData.corpseExplosionDamageType or "Fire"
|
|
skillData[damageType.."BonusMin"] = skillData.corpseLife * ( skillData.corpseExplosionLifeMultiplier or skillData.selfFireExplosionLifeMultiplier )
|
|
skillData[damageType.."BonusMax"] = skillData.corpseLife * ( skillData.corpseExplosionLifeMultiplier or skillData.selfFireExplosionLifeMultiplier )
|
|
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 }
|
|
|
|
-- 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 = m_max(0, 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) * calcLib.mod(skillModList, cfg, "HitChance")
|
|
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
|
|
elseif skillData.triggerTime and skillData.triggered then
|
|
output.Time = skillData.triggerTime / (1 + skillModList:Sum("INC", cfg, "CooldownRecovery") / 100) * (skillModList:Sum("BASE", cfg, "CastWhileChannellingSpellsLinked") or 1)
|
|
output.TriggerTime = output.Time
|
|
output.Speed = 1 / output.Time
|
|
elseif skillData.triggeredByBrand and skillData.triggered then
|
|
output.Time = 1 / (1 + skillModList:Sum("INC", cfg, "Speed", "BrandActivationFrequency") / 100) / skillModList:More(cfg, "BrandActivationFrequency") * (skillModList:Sum("BASE", cfg, "ArcanistSpellsLinked") or 1)
|
|
output.TriggerTime = output.Time
|
|
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)
|
|
elseif calcLib.mod(skillModList, skillCfg, "SkillAttackTime") > 0 then
|
|
baseTime = (1 / ( source.AttackRate or 1 ) + skillModList:Sum("BASE", cfg, "Speed")) * calcLib.mod(skillModList, skillCfg, "SkillAttackTime")
|
|
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
|
|
output.Speed = m_min(output.Speed, data.misc.ServerTickRate)
|
|
if output.Speed == 0 then
|
|
output.Time = 0
|
|
else
|
|
output.Time = 1 / output.Speed
|
|
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 breakdown and calcLib.mod(skillModList, skillCfg, "SkillAttackTime") > 0 then
|
|
breakdown.Time = { }
|
|
breakdown.multiChain(breakdown.Time, {
|
|
base = s_format("%.2f ^8(base)", 1 / (output.Speed * calcLib.mod(skillModList, skillCfg, "SkillAttackTime") )),
|
|
{ "%.2f ^8(total modifier)", calcLib.mod(skillModList, skillCfg, "SkillAttackTime") },
|
|
total = s_format("= %.2f ^8seconds per attack", output.Time)
|
|
})
|
|
end
|
|
end
|
|
if skillData.hitTimeOverride then
|
|
output.HitTime = skillData.hitTimeOverride
|
|
output.HitSpeed = 1 / output.HitTime
|
|
--Brands always have hitTimeOverride
|
|
if skillFlags.brand then
|
|
output.BrandTicks = m_floor(output.Duration * output.HitSpeed)
|
|
end
|
|
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
|
|
|
|
-- Exerted Attack members
|
|
local exertedDoubleDamage = env.modDB:Sum("BASE", cfg, "ExertDoubleDamageChance")
|
|
globalOutput.OffensiveWarcryEffect = 1
|
|
globalOutput.MaxOffensiveWarcryEffect = 1
|
|
globalOutput.TheoreticalOffensiveWarcryEffect = 1
|
|
globalOutput.TheoreticalMaxOffensiveWarcryEffect = 1
|
|
globalOutput.SeismicHitEffect = 1
|
|
globalOutput.RallyingHitEffect = 1
|
|
globalOutput.AilmentWarcryEffect = 1
|
|
|
|
if env.mode_buffs then
|
|
-- Iterative over all the active skills to account for exerted attacks provided by warcries
|
|
if not activeSkill.skillTypes[SkillType.Vaal] and not activeSkill.skillTypes[SkillType.Channelled] and not activeSkill.skillModList:Flag(cfg, "SupportedByMultistrike") then
|
|
for index, value in ipairs(actor.activeSkillList) do
|
|
if value.activeEffect.grantedEffect.name == "Ancestral Cry" and activeSkill.skillTypes[SkillType.MeleeSingleTarget] and not globalOutput.AncestralCryCalculated then
|
|
globalOutput.AncestralCryDuration = calcSkillDuration(value.skillModList, value.skillCfg, value.skillData, env, enemyDB)
|
|
globalOutput.AncestralCryCooldown = calcSkillCooldown(value.skillModList, value.skillCfg, value.skillData)
|
|
output.GlobalWarcryCooldown = env.modDB:Sum("BASE", nil, "GlobalWarcryCooldown")
|
|
output.GlobalWarcryCount = env.modDB:Sum("BASE", nil, "GlobalWarcryCount")
|
|
if modDB:Flag(nil, "WarcryShareCooldown") then
|
|
globalOutput.AncestralCryCooldown = globalOutput.AncestralCryCooldown + (output.GlobalWarcryCooldown - globalOutput.AncestralCryCooldown) / output.GlobalWarcryCount
|
|
end
|
|
globalOutput.AncestralCryCastTime = calcWarcryCastTime(value.skillModList, value.skillCfg, actor)
|
|
globalOutput.AncestralExertsCount = env.modDB:Sum("BASE", nil, "NumAncestralExerts") or 0
|
|
globalOutput.AncestralUpTimeRatio = m_min((globalOutput.AncestralExertsCount / output.Speed) / (globalOutput.AncestralCryCooldown + globalOutput.AncestralCryCastTime), 1) * 100
|
|
if globalBreakdown then
|
|
globalBreakdown.AncestralUpTimeRatio = { }
|
|
t_insert(globalBreakdown.AncestralUpTimeRatio, s_format("(%d ^8(number of exerts)", globalOutput.AncestralExertsCount))
|
|
t_insert(globalBreakdown.AncestralUpTimeRatio, s_format("/ %.2f) ^8(attacks per second)", output.Speed))
|
|
if globalOutput.AncestralCryCastTime > 0 then
|
|
t_insert(globalBreakdown.AncestralUpTimeRatio, s_format("/ (%.2f ^8(warcry cooldown)", globalOutput.AncestralCryCooldown))
|
|
t_insert(globalBreakdown.AncestralUpTimeRatio, s_format("+ %.2f) ^8(warcry casttime)", globalOutput.AncestralCryCastTime))
|
|
else
|
|
t_insert(globalBreakdown.AncestralUpTimeRatio, s_format("/ %.2f ^8(average ooldown)", globalOutput.AncestralCryCooldown))
|
|
end
|
|
t_insert(globalBreakdown.AncestralUpTimeRatio, s_format("= %d%%", globalOutput.AncestralUpTimeRatio))
|
|
end
|
|
globalOutput.AncestralCryCalculated = true
|
|
elseif value.activeEffect.grantedEffect.name == "Infernal Cry" and not globalOutput.InfernalCryCalculated then
|
|
globalOutput.InfernalCryDuration = calcSkillDuration(value.skillModList, value.skillCfg, value.skillData, env, enemyDB)
|
|
globalOutput.InfernalCryCooldown = calcSkillCooldown(value.skillModList, value.skillCfg, value.skillData)
|
|
output.GlobalWarcryCooldown = env.modDB:Sum("BASE", nil, "GlobalWarcryCooldown")
|
|
output.GlobalWarcryCount = env.modDB:Sum("BASE", nil, "GlobalWarcryCount")
|
|
if modDB:Flag(nil, "WarcryShareCooldown") then
|
|
globalOutput.InfernalCryCooldown = globalOutput.InfernalCryCooldown + (output.GlobalWarcryCooldown - globalOutput.InfernalCryCooldown) / output.GlobalWarcryCount
|
|
end
|
|
globalOutput.InfernalCryCastTime = calcWarcryCastTime(value.skillModList, value.skillCfg, actor)
|
|
if activeSkill.skillTypes[SkillType.Melee] then
|
|
globalOutput.InfernalExertsCount = env.modDB:Sum("BASE", nil, "NumInfernalExerts") or 0
|
|
globalOutput.InfernalUpTimeRatio = m_min((globalOutput.InfernalExertsCount / output.Speed) / (globalOutput.InfernalCryCooldown + globalOutput.InfernalCryCastTime), 1) * 100
|
|
if globalBreakdown then
|
|
globalBreakdown.InfernalUpTimeRatio = { }
|
|
t_insert(globalBreakdown.InfernalUpTimeRatio, s_format("(%d ^8(number of exerts)", globalOutput.InfernalExertsCount))
|
|
t_insert(globalBreakdown.InfernalUpTimeRatio, s_format("/ %.2f) ^8(attacks per second)", output.Speed))
|
|
if globalOutput.InfernalCryCastTime > 0 then
|
|
t_insert(globalBreakdown.InfernalUpTimeRatio, s_format("/ (%.2f ^8(warcry cooldown)", globalOutput.InfernalCryCooldown))
|
|
t_insert(globalBreakdown.InfernalUpTimeRatio, s_format("+ %.2f) ^8(warcry casttime)", globalOutput.InfernalCryCastTime))
|
|
else
|
|
t_insert(globalBreakdown.InfernalUpTimeRatio, s_format("/ %.2f ^8(average warcry cooldown)", globalOutput.InfernalCryCooldown))
|
|
end
|
|
t_insert(globalBreakdown.InfernalUpTimeRatio, s_format("= %d%%", globalOutput.InfernalUpTimeRatio))
|
|
end
|
|
end
|
|
globalOutput.InfernalCryCalculated = true
|
|
elseif value.activeEffect.grantedEffect.name == "Intimidating Cry" and activeSkill.skillTypes[SkillType.Melee] and not globalOutput.IntimidatingCryCalculated then
|
|
globalOutput.CreateWarcryOffensiveCalcSection = true
|
|
globalOutput.IntimidatingCryDuration = calcSkillDuration(value.skillModList, value.skillCfg, value.skillData, env, enemyDB)
|
|
globalOutput.IntimidatingCryCooldown = calcSkillCooldown(value.skillModList, value.skillCfg, value.skillData)
|
|
output.GlobalWarcryCooldown = env.modDB:Sum("BASE", nil, "GlobalWarcryCooldown")
|
|
output.GlobalWarcryCount = env.modDB:Sum("BASE", nil, "GlobalWarcryCount")
|
|
if modDB:Flag(nil, "WarcryShareCooldown") then
|
|
globalOutput.IntimidatingCryCooldown = globalOutput.IntimidatingCryCooldown + (output.GlobalWarcryCooldown - globalOutput.IntimidatingCryCooldown) / output.GlobalWarcryCount
|
|
end
|
|
globalOutput.IntimidatingCryCastTime = calcWarcryCastTime(value.skillModList, value.skillCfg, actor)
|
|
globalOutput.IntimidatingExertsCount = env.modDB:Sum("BASE", nil, "NumIntimidatingExerts") or 0
|
|
globalOutput.IntimidatingUpTimeRatio = m_min((globalOutput.IntimidatingExertsCount / output.Speed) / (globalOutput.IntimidatingCryCooldown + globalOutput.IntimidatingCryCastTime), 1) * 100
|
|
if globalBreakdown then
|
|
globalBreakdown.IntimidatingUpTimeRatio = { }
|
|
t_insert(globalBreakdown.IntimidatingUpTimeRatio, s_format("(%d ^8(number of exerts)", globalOutput.IntimidatingExertsCount))
|
|
t_insert(globalBreakdown.IntimidatingUpTimeRatio, s_format("/ %.2f) ^8(attacks per second)", output.Speed))
|
|
if globalOutput.IntimidatingCryCastTime > 0 then
|
|
t_insert(globalBreakdown.IntimidatingUpTimeRatio, s_format("/ (%.2f ^8(warcry cooldown)", globalOutput.IntimidatingCryCooldown))
|
|
t_insert(globalBreakdown.IntimidatingUpTimeRatio, s_format("+ %.2f) ^8(warcry casttime)", globalOutput.IntimidatingCryCastTime))
|
|
else
|
|
t_insert(globalBreakdown.IntimidatingUpTimeRatio, s_format("/ %.2f ^8(average warcry cooldown)", globalOutput.IntimidatingCryCooldown))
|
|
end
|
|
t_insert(globalBreakdown.IntimidatingUpTimeRatio, s_format("= %d%%", globalOutput.IntimidatingUpTimeRatio))
|
|
end
|
|
local ddChance = m_min(skillModList:Sum("BASE", cfg, "DoubleDamageChance") + (env.mode_effective and enemyDB:Sum("BASE", cfg, "SelfDoubleDamageChance") or 0) + exertedDoubleDamage, 100)
|
|
globalOutput.IntimidatingAvgDmg = 2 * (1 - ddChance / 100) -- 1
|
|
if globalBreakdown then
|
|
globalBreakdown.IntimidatingAvgDmg = {
|
|
s_format("Average Intimidating Cry Damage:"),
|
|
s_format("%.2f%% ^8(base double damage increase to hit 100%%)", (1 - ddChance / 100) * 100 ),
|
|
s_format("x %d ^8(double damage multiplier)", 2),
|
|
s_format("= %.2f", globalOutput.IntimidatingAvgDmg),
|
|
}
|
|
end
|
|
globalOutput.IntimidatingHitEffect = 1 + globalOutput.IntimidatingAvgDmg * globalOutput.IntimidatingUpTimeRatio / 100
|
|
globalOutput.IntimidatingMaxHitEffect = 1 + globalOutput.IntimidatingAvgDmg
|
|
if globalBreakdown then
|
|
globalBreakdown.IntimidatingHitEffect = {
|
|
s_format("1 + (%.2f ^8(average exerted damage)", globalOutput.IntimidatingAvgDmg),
|
|
s_format("x %.2f) ^8(uptime %%)", globalOutput.IntimidatingUpTimeRatio / 100),
|
|
s_format("= %.2f", globalOutput.IntimidatingHitEffect),
|
|
}
|
|
end
|
|
|
|
globalOutput.TheoreticalOffensiveWarcryEffect = globalOutput.TheoreticalOffensiveWarcryEffect * globalOutput.IntimidatingHitEffect
|
|
globalOutput.TheoreticalMaxOffensiveWarcryEffect = globalOutput.TheoreticalMaxOffensiveWarcryEffect * globalOutput.IntimidatingMaxHitEffect
|
|
globalOutput.IntimidatingCryCalculated = true
|
|
elseif value.activeEffect.grantedEffect.name == "Rallying Cry" and activeSkill.skillTypes[SkillType.Melee] and not globalOutput.RallyingCryCalculated then
|
|
globalOutput.CreateWarcryOffensiveCalcSection = true
|
|
globalOutput.RallyingCryDuration = calcSkillDuration(value.skillModList, value.skillCfg, value.skillData, env, enemyDB)
|
|
globalOutput.RallyingCryCooldown = calcSkillCooldown(value.skillModList, value.skillCfg, value.skillData)
|
|
output.GlobalWarcryCooldown = env.modDB:Sum("BASE", nil, "GlobalWarcryCooldown")
|
|
output.GlobalWarcryCount = env.modDB:Sum("BASE", nil, "GlobalWarcryCount")
|
|
if modDB:Flag(nil, "WarcryShareCooldown") then
|
|
globalOutput.RallyingCryCooldown = globalOutput.RallyingCryCooldown + (output.GlobalWarcryCooldown - globalOutput.RallyingCryCooldown) / output.GlobalWarcryCount
|
|
end
|
|
globalOutput.RallyingCryCastTime = calcWarcryCastTime(value.skillModList, value.skillCfg, actor)
|
|
globalOutput.RallyingExertsCount = env.modDB:Sum("BASE", nil, "NumRallyingExerts") or 0
|
|
globalOutput.RallyingUpTimeRatio = m_min((globalOutput.RallyingExertsCount / output.Speed) / (globalOutput.RallyingCryCooldown + globalOutput.RallyingCryCastTime), 1) * 100
|
|
if globalBreakdown then
|
|
globalBreakdown.RallyingUpTimeRatio = { }
|
|
t_insert(globalBreakdown.RallyingUpTimeRatio, s_format("(%d ^8(number of exerts)", globalOutput.RallyingExertsCount))
|
|
t_insert(globalBreakdown.RallyingUpTimeRatio, s_format("/ %.2f) ^8(attacks per second)", output.Speed))
|
|
if globalOutput.RallyingCryCastTime > 0 then
|
|
t_insert(globalBreakdown.RallyingUpTimeRatio, s_format("/ (%.2f ^8(warcry cooldown)", globalOutput.RallyingCryCooldown))
|
|
t_insert(globalBreakdown.RallyingUpTimeRatio, s_format("+ %.2f) ^8(warcry casttime)", globalOutput.RallyingCryCastTime))
|
|
else
|
|
t_insert(globalBreakdown.RallyingUpTimeRatio, s_format("/ %.2f ^8(average warcry cooldown)", globalOutput.RallyingCryCooldown))
|
|
end
|
|
t_insert(globalBreakdown.RallyingUpTimeRatio, s_format("= %d%%", globalOutput.RallyingUpTimeRatio))
|
|
end
|
|
globalOutput.RallyingAvgDmg = m_min(env.modDB:Sum("BASE", cfg, "Multiplier:NearbyAlly"), 5) * (env.modDB:Sum("BASE", nil, "RallyingExertMoreDamagePerAlly") / 100)
|
|
if globalBreakdown then
|
|
globalBreakdown.RallyingAvgDmg = {
|
|
s_format("Average Rallying Cry Damage:"),
|
|
s_format("%.2f ^8(average damage multiplier per ally)", env.modDB:Sum("BASE", nil, "RallyingExertMoreDamagePerAlly") / 100),
|
|
s_format("x %d ^8(number of nearby allies (max=5))", m_min(env.modDB:Sum("BASE", cfg, "Multiplier:NearbyAlly"), 5)),
|
|
s_format("= %.2f", globalOutput.RallyingAvgDmg),
|
|
}
|
|
end
|
|
globalOutput.RallyingHitEffect = 1 + globalOutput.RallyingAvgDmg * globalOutput.RallyingUpTimeRatio / 100
|
|
globalOutput.RallyingMaxHitEffect = 1 + globalOutput.RallyingAvgDmg
|
|
if globalBreakdown then
|
|
globalBreakdown.RallyingHitEffect = {
|
|
s_format("1 + (%.2f ^8(average exerted damage)", globalOutput.RallyingAvgDmg),
|
|
s_format("x %.2f) ^8(uptime %%)", globalOutput.RallyingUpTimeRatio / 100),
|
|
s_format("= %.2f", globalOutput.RallyingHitEffect),
|
|
}
|
|
end
|
|
globalOutput.OffensiveWarcryEffect = globalOutput.OffensiveWarcryEffect * globalOutput.RallyingHitEffect
|
|
globalOutput.MaxOffensiveWarcryEffect = globalOutput.MaxOffensiveWarcryEffect * globalOutput.RallyingMaxHitEffect
|
|
globalOutput.TheoreticalOffensiveWarcryEffect = globalOutput.TheoreticalOffensiveWarcryEffect * globalOutput.RallyingHitEffect
|
|
globalOutput.TheoreticalMaxOffensiveWarcryEffect = globalOutput.TheoreticalMaxOffensiveWarcryEffect * globalOutput.RallyingMaxHitEffect
|
|
globalOutput.RallyingCryCalculated = true
|
|
|
|
elseif value.activeEffect.grantedEffect.name == "Seismic Cry" and activeSkill.skillTypes[SkillType.SlamSkill] and not globalOutput.SeismicCryCalculated then
|
|
globalOutput.CreateWarcryOffensiveCalcSection = true
|
|
globalOutput.SeismicCryDuration = calcSkillDuration(value.skillModList, value.skillCfg, value.skillData, env, enemyDB)
|
|
globalOutput.SeismicCryCooldown = calcSkillCooldown(value.skillModList, value.skillCfg, value.skillData)
|
|
output.GlobalWarcryCooldown = env.modDB:Sum("BASE", nil, "GlobalWarcryCooldown")
|
|
output.GlobalWarcryCount = env.modDB:Sum("BASE", nil, "GlobalWarcryCount")
|
|
if modDB:Flag(nil, "WarcryShareCooldown") then
|
|
globalOutput.SeismicCryCooldown = globalOutput.SeismicCryCooldown + (output.GlobalWarcryCooldown - globalOutput.SeismicCryCooldown) / output.GlobalWarcryCount
|
|
end
|
|
globalOutput.SeismicCryCastTime = calcWarcryCastTime(value.skillModList, value.skillCfg, actor)
|
|
globalOutput.SeismicExertsCount = env.modDB:Sum("BASE", nil, "NumSeismicExerts") or 0
|
|
globalOutput.SeismicUpTimeRatio = m_min((globalOutput.SeismicExertsCount / output.Speed) / (globalOutput.SeismicCryCooldown + globalOutput.SeismicCryCastTime), 1) * 100
|
|
if globalBreakdown then
|
|
globalBreakdown.SeismicUpTimeRatio = { }
|
|
t_insert(globalBreakdown.SeismicUpTimeRatio, s_format("(%d ^8(number of exerts)", globalOutput.SeismicExertsCount))
|
|
t_insert(globalBreakdown.SeismicUpTimeRatio, s_format("/ %.2f) ^8(attacks per second)", output.Speed))
|
|
if globalOutput.SeismicCryCastTime > 0 then
|
|
t_insert(globalBreakdown.SeismicUpTimeRatio, s_format("/ (%.2f ^8(warcry cooldown)", globalOutput.SeismicCryCooldown))
|
|
t_insert(globalBreakdown.SeismicUpTimeRatio, s_format("+ %.2f) ^8(warcry casttime)", globalOutput.SeismicCryCastTime))
|
|
else
|
|
t_insert(globalBreakdown.SeismicUpTimeRatio, s_format("/ %.2f ^8(average warcry cooldown)", globalOutput.SeismicCryCooldown))
|
|
end
|
|
t_insert(globalBreakdown.SeismicUpTimeRatio, s_format("= %d%%", globalOutput.SeismicUpTimeRatio))
|
|
end
|
|
-- calculate the stacking MORE dmg modifier of Seismic slams
|
|
local SeismicMoreDmgAndAoEPerExert = env.modDB:Sum("BASE", cfg, "SeismicMoreDmgPerExert") / 100
|
|
local TotalSeismicDmgImpact = 0
|
|
local ThisSeismicDmgImpact = 0
|
|
local LastSeismicImpact = 0
|
|
local AoEImpact = 0
|
|
local MaxSingleHitDmgImpact = 0
|
|
local MaxSingleAoEImpact = 0
|
|
for i = 1, globalOutput.SeismicExertsCount do
|
|
ThisSeismicDmgImpact = SeismicMoreDmgAndAoEPerExert + (1 + SeismicMoreDmgAndAoEPerExert) * LastSeismicImpact
|
|
MaxSingleHitDmgImpact = m_max(MaxSingleHitDmgImpact, ThisSeismicDmgImpact)
|
|
LastSeismicImpact = LastSeismicImpact + SeismicMoreDmgAndAoEPerExert
|
|
TotalSeismicDmgImpact = TotalSeismicDmgImpact + ThisSeismicDmgImpact
|
|
AoEImpact = AoEImpact + (i * SeismicMoreDmgAndAoEPerExert)
|
|
MaxSingleAoEImpact = MaxSingleAoEImpact + SeismicMoreDmgAndAoEPerExert
|
|
end
|
|
globalOutput.SeismicAvgDmg = (TotalSeismicDmgImpact / globalOutput.SeismicExertsCount)
|
|
local AvgAoEImpact = AoEImpact / globalOutput.SeismicExertsCount
|
|
if globalBreakdown then
|
|
globalBreakdown.SeismicAvgDmg = {
|
|
s_format("%.2f ^8(total seismic damage multiplier across all exerts)", TotalSeismicDmgImpact),
|
|
s_format("Average Seismic Damage:"),
|
|
s_format("(%.2f ^8(average damage multiplier per exert)", TotalSeismicDmgImpact / globalOutput.SeismicExertsCount),
|
|
s_format("= %.2f", globalOutput.SeismicAvgDmg),
|
|
}
|
|
end
|
|
globalOutput.SeismicHitEffect = 1 + globalOutput.SeismicAvgDmg * globalOutput.SeismicUpTimeRatio / 100
|
|
globalOutput.SeismicMaxHitEffect = 1 + MaxSingleHitDmgImpact
|
|
if globalBreakdown then
|
|
globalBreakdown.SeismicHitEffect = {
|
|
s_format("1 + (%.2f ^8(average exerted damage)", globalOutput.SeismicAvgDmg),
|
|
s_format("x %.2f) ^8(uptime %%)", globalOutput.SeismicUpTimeRatio / 100),
|
|
s_format("= %.2f", globalOutput.SeismicHitEffect),
|
|
}
|
|
end
|
|
globalOutput.OffensiveWarcryEffect = globalOutput.OffensiveWarcryEffect * globalOutput.SeismicHitEffect
|
|
globalOutput.MaxOffensiveWarcryEffect = globalOutput.MaxOffensiveWarcryEffect * globalOutput.SeismicMaxHitEffect
|
|
globalOutput.TheoreticalOffensiveWarcryEffect = globalOutput.TheoreticalOffensiveWarcryEffect * globalOutput.SeismicHitEffect
|
|
globalOutput.TheoreticalMaxOffensiveWarcryEffect = globalOutput.TheoreticalMaxOffensiveWarcryEffect * globalOutput.SeismicMaxHitEffect
|
|
|
|
-- account for AoE increase
|
|
if activeSkill.skillModList:Flag(nil, "Condition:WarcryMaxHit") then
|
|
skillModList:NewMod("AreaOfEffect", "INC", MaxSingleAoEImpact * 100, "Max Seismic Exert AoE")
|
|
else
|
|
skillModList:NewMod("AreaOfEffect", "INC", m_floor(AvgAoEImpact * globalOutput.SeismicUpTimeRatio), "Avg Seismic Exert AoE")
|
|
end
|
|
calcAreaOfEffect(skillModList, skillCfg, skillData, skillFlags, globalOutput, globalBreakdown)
|
|
globalOutput.SeismicCryCalculated = true
|
|
end
|
|
end
|
|
|
|
if activeSkill.skillModList:Flag(nil, "Condition:WarcryMaxHit") then
|
|
globalOutput.AilmentWarcryEffect = globalOutput.MaxOffensiveWarcryEffect
|
|
skillData.showAverage = true
|
|
skillFlags.showAverage = true
|
|
skillFlags.notAverage = false
|
|
else
|
|
globalOutput.AilmentWarcryEffect = globalOutput.OffensiveWarcryEffect
|
|
end
|
|
|
|
-- Calculate Exerted Attack Uptime
|
|
-- There are various strategies a player could use to maximize either warcry effect stacking or staggering
|
|
-- 1) they don't pay attention and therefore we calculated exerted attack uptime as just the maximum uptime of any enabled warcries that exert attacks
|
|
globalOutput.ExertedAttackUptimeRatio = m_max(m_max(m_max(globalOutput.AncestralUpTimeRatio or 0, globalOutput.InfernalUpTimeRatio or 0), m_max(globalOutput.IntimidatingUpTimeRatio or 0, globalOutput.RallyingUpTimeRatio or 0)), globalOutput.SeismicUpTimeRatio or 0)
|
|
if globalBreakdown then
|
|
globalBreakdown.ExertedAttackUptimeRatio = { }
|
|
t_insert(globalBreakdown.ExertedAttackUptimeRatio, s_format("Maximum of:"))
|
|
if globalOutput.AncestralUpTimeRatio then
|
|
t_insert(globalBreakdown.ExertedAttackUptimeRatio, s_format("%d%% ^8(Ancestral Cry Uptime)", globalOutput.AncestralUpTimeRatio or 0))
|
|
end
|
|
if globalOutput.InfernalUpTimeRatio then
|
|
t_insert(globalBreakdown.ExertedAttackUptimeRatio, s_format("%d%% ^8(Infernal Cry Uptime)", globalOutput.InfernalUpTimeRatio or 0))
|
|
end
|
|
if globalOutput.IntimidatingUpTimeRatio then
|
|
t_insert(globalBreakdown.ExertedAttackUptimeRatio, s_format("%d%% ^8(Intimidating Cry Uptime)", globalOutput.IntimidatingUpTimeRatio or 0))
|
|
end
|
|
if globalOutput.RallyingUpTimeRatio then
|
|
t_insert(globalBreakdown.ExertedAttackUptimeRatio, s_format("%d%% ^8(Rallying Cry Uptime)", globalOutput.RallyingUpTimeRatio or 0))
|
|
end
|
|
if globalOutput.SeismicUpTimeRatio then
|
|
t_insert(globalBreakdown.ExertedAttackUptimeRatio, s_format("%d%% ^8(Seismic Cry Uptime)", globalOutput.SeismicUpTimeRatio or 0))
|
|
end
|
|
t_insert(globalBreakdown.ExertedAttackUptimeRatio, s_format("= %d%%", globalOutput.ExertedAttackUptimeRatio))
|
|
end
|
|
if globalOutput.ExertedAttackUptimeRatio > 0 then
|
|
local incExertedAttacks = skillModList:Sum("INC", cfg, "ExertIncrease")
|
|
local moreExertedAttacks = skillModList:Sum("MORE", cfg, "ExertIncrease")
|
|
if activeSkill.skillModList:Flag(nil, "Condition:WarcryMaxHit") then
|
|
skillModList:NewMod("Damage", "INC", incExertedAttacks, "Exerted Attacks")
|
|
skillModList:NewMod("Damage", "MORE", moreExertedAttacks, "Exerted Attacks")
|
|
else
|
|
skillModList:NewMod("Damage", "INC", incExertedAttacks * globalOutput.ExertedAttackUptimeRatio / 100, "Uptime Scaled Exerted Attacks")
|
|
skillModList:NewMod("Damage", "MORE", moreExertedAttacks * globalOutput.ExertedAttackUptimeRatio / 100, "Uptime Scaled Exerted Attacks")
|
|
end
|
|
globalOutput.ExertedAttackAvgDmg = calcLib.mod(skillModList, skillCfg, "ExertIncrease")
|
|
globalOutput.ExertedAttackHitEffect = globalOutput.ExertedAttackAvgDmg * globalOutput.ExertedAttackUptimeRatio / 100
|
|
globalOutput.ExertedAttackMaxHitEffect = globalOutput.ExertedAttackAvgDmg
|
|
if globalBreakdown then
|
|
globalBreakdown.ExertedAttackHitEffect = {
|
|
s_format("(%.2f ^8(average exerted damage)", globalOutput.ExertedAttackAvgDmg),
|
|
s_format("x %.2f) ^8(uptime %%)", globalOutput.ExertedAttackUptimeRatio / 100),
|
|
s_format("= %.2f", globalOutput.ExertedAttackHitEffect),
|
|
}
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
output.RuthlessBlowEffect = 1
|
|
output.FistOfWarHitEffect = 1
|
|
output.FistOfWarAilmentEffect = 1
|
|
if env.mode_combat then
|
|
-- Calculate Ruthless Blow chance/multipliers + Fist of War multipliers
|
|
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
|
|
|
|
globalOutput.FistOfWarCooldown = skillModList:Sum("BASE", cfg, "FistOfWarCooldown") or 0
|
|
-- If Fist of War & Active Skill is a Slam Skill & NOT a Vaal Skill
|
|
if globalOutput.FistOfWarCooldown ~= 0 and activeSkill.skillTypes[SkillType.SlamSkill] and not activeSkill.skillTypes[SkillType.Vaal] then
|
|
globalOutput.FistOfWarHitMultiplier = skillModList:Sum("BASE", cfg, "FistOfWarHitMultiplier") / 100
|
|
globalOutput.FistOfWarAilmentMultiplier = skillModList:Sum("BASE", cfg, "FistOfWarAilmentMultiplier") / 100
|
|
globalOutput.FistOfWarUptimeRatio = m_min( (1 / output.Speed) / globalOutput.FistOfWarCooldown, 1) * 100
|
|
if globalBreakdown then
|
|
globalBreakdown.FistOfWarUptimeRatio = {
|
|
s_format("min( (1 / %.2f) ^8(second per attack)", output.Speed),
|
|
s_format("/ %.2f, 1) ^8(fist of war cooldown)", globalOutput.FistOfWarCooldown),
|
|
s_format("= %d%%", globalOutput.FistOfWarUptimeRatio),
|
|
}
|
|
end
|
|
globalOutput.AvgFistOfWarHit = globalOutput.FistOfWarHitMultiplier
|
|
globalOutput.AvgFistOfWarHitEffect = 1 + globalOutput.FistOfWarHitMultiplier * (globalOutput.FistOfWarUptimeRatio / 100)
|
|
if globalBreakdown then
|
|
globalBreakdown.AvgFistOfWarHitEffect = {
|
|
s_format("1 + (%.2f ^8(fist of war hit multiplier)", globalOutput.FistOfWarHitMultiplier),
|
|
s_format("x %.2f) ^8(fist of war uptime ratio)", globalOutput.FistOfWarUptimeRatio / 100),
|
|
s_format("= %.2f", globalOutput.AvgFistOfWarHitEffect),
|
|
}
|
|
end
|
|
globalOutput.AvgFistOfWarAilmentEffect = 1 + globalOutput.FistOfWarAilmentMultiplier * (globalOutput.FistOfWarUptimeRatio / 100)
|
|
globalOutput.MaxFistOfWarHitEffect = 1 + globalOutput.FistOfWarHitMultiplier
|
|
globalOutput.MaxFistOfWarAilmentEffect = 1 + globalOutput.FistOfWarAilmentMultiplier
|
|
if activeSkill.skillModList:Flag(nil, "Condition:WarcryMaxHit") then
|
|
output.FistOfWarHitEffect = globalOutput.MaxFistOfWarHitEffect
|
|
output.FistOfWarAilmentEffect = globalOutput.MaxFistOfWarAilmentEffect
|
|
else
|
|
output.FistOfWarHitEffect = globalOutput.AvgFistOfWarHitEffect
|
|
output.FistOfWarAilmentEffect = globalOutput.AvgFistOfWarAilmentEffect
|
|
end
|
|
globalOutput.TheoreticalOffensiveWarcryEffect = globalOutput.TheoreticalOffensiveWarcryEffect * globalOutput.AvgFistOfWarHitEffect
|
|
globalOutput.TheoreticalMaxOffensiveWarcryEffect = globalOutput.TheoreticalMaxOffensiveWarcryEffect * globalOutput.MaxFistOfWarHitEffect
|
|
else
|
|
output.FistOfWarHitEffect = 1
|
|
output.FistOfWarAilmentEffect = 1
|
|
end
|
|
end
|
|
|
|
-- 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.BonusCritDotMultiplier = 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") + (env.mode_effective and enemyDB:Sum("BASE", nil, "SelfCritChance") or 0)
|
|
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 = extraDamage + enemyDB:Sum("BASE", nil, "SelfCritMultiplier") / 100
|
|
extraDamage = round(extraDamage * enemyInc, 2)
|
|
if breakdown and enemyInc ~= 1 then
|
|
breakdown.CritMultiplier = {
|
|
s_format("%d%% ^8(additional extra damage)", (enemyDB:Sum("BASE", nil, "SelfCritMultiplier") + 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
|
|
local critChancePercentage = output.CritChance / 100
|
|
output.CritEffect = 1 - critChancePercentage + critChancePercentage * output.CritMultiplier
|
|
output.BonusCritDotMultiplier = (skillModList:Sum("BASE", cfg, "CritMultiplier") - 50) * skillModList:Sum("BASE", cfg, "CritMultiplierAppliesToDegen") / 10000
|
|
if breakdown and output.CritEffect ~= 1 then
|
|
breakdown.CritEffect = {
|
|
s_format("(1 - %.4f) ^8(portion of damage from non-crits)", critChancePercentage),
|
|
s_format("+ [ (%.4f x %g) ^8(portion of damage from crits)", critChancePercentage, output.CritMultiplier),
|
|
s_format("= %.3f", output.CritEffect),
|
|
}
|
|
end
|
|
end
|
|
|
|
output.ScaledDamageEffect = 1
|
|
|
|
-- Calculate chance and multiplier for dealing triple damage on Normal and Crit
|
|
output.TripleDamageChanceOnCrit = m_min(skillModList:Sum("BASE", cfg, "TripleDamageChanceOnCrit"), 100)
|
|
output.TripleDamageChance = m_min(skillModList:Sum("BASE", cfg, "TripleDamageChance") or 0 + (env.mode_effective and enemyDB:Sum("BASE", cfg, "SelfTripleDamageChance") or 0) + (output.TripleDamageChanceOnCrit * output.CritChance / 100), 100)
|
|
output.TripleDamageEffect = 1 + (2 * output.TripleDamageChance / 100)
|
|
output.ScaledDamageEffect = output.ScaledDamageEffect * output.TripleDamageEffect
|
|
|
|
-- Calculate chance and multiplier for dealing double damage on Normal and Crit
|
|
output.DoubleDamageChanceOnCrit = m_min(skillModList:Sum("BASE", cfg, "DoubleDamageChanceOnCrit"), 100)
|
|
output.DoubleDamageChance = m_min(skillModList:Sum("BASE", cfg, "DoubleDamageChance") + (env.mode_effective and enemyDB:Sum("BASE", cfg, "SelfDoubleDamageChance") or 0) + (output.DoubleDamageChanceOnCrit * output.CritChance / 100), 100)
|
|
if globalOutput.IntimidatingUpTimeRatio and activeSkill.skillModList:Flag(nil, "Condition:WarcryMaxHit") then
|
|
output.DoubleDamageChance = 100
|
|
elseif globalOutput.IntimidatingUpTimeRatio then
|
|
output.DoubleDamageChance = m_min(output.DoubleDamageChance + globalOutput.IntimidatingUpTimeRatio, 100)
|
|
end
|
|
-- Triple Damage overrides Double Damage. If you have both, it's the same as just having Triple
|
|
-- We need to subtract the probability of both happening in favor of Triple Damage
|
|
if output.TripleDamageChance > 0 then
|
|
output.DoubleDamageChance = m_max(output.DoubleDamageChance - output.TripleDamageChance * output.DoubleDamageChance / 100, 0)
|
|
end
|
|
output.DoubleDamageEffect = 1 + output.DoubleDamageChance / 100
|
|
output.ScaledDamageEffect = output.ScaledDamageEffect * output.DoubleDamageEffect
|
|
|
|
-- 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) + enemyDB:Sum("BASE", cfg, "Self"..damageTypeMin)
|
|
local addedMax = skillModList:Sum("BASE", cfg, damageTypeMax) + enemyDB:Sum("BASE", cfg, "Self"..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, totalHitAvg = 0, 0, 0
|
|
local totalCritMin, totalCritMax, totalCritAvg = 0, 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
|
|
output.impaleStoredHitAvg = 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 damageTypeHitMin, damageTypeHitMax, damageTypeHitAvg = 0, 0, 0
|
|
if skillFlags.hit and canDeal[damageType] then
|
|
damageTypeHitMin, damageTypeHitMax = 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)", damageTypeHitMin, damageTypeHitMax))
|
|
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.TripleDamageEffect ~= 1 then
|
|
t_insert(breakdown[damageType], s_format("x %.2f ^8(multiplier from %.2f%% chance to deal triple damage)", output.TripleDamageEffect, output.TripleDamageChance))
|
|
end
|
|
if output.DoubleDamageEffect ~= 1 then
|
|
t_insert(breakdown[damageType], s_format("x %.2f ^8(multiplier from %.2f%% chance to deal double damage)", output.DoubleDamageEffect, output.DoubleDamageChance))
|
|
end
|
|
if output.RuthlessBlowEffect ~= 1 then
|
|
t_insert(breakdown[damageType], s_format("x %.2f ^8(ruthless blow effect modifier)", output.RuthlessBlowEffect))
|
|
end
|
|
if output.FistOfWarHitEffect ~= 1 then
|
|
t_insert(breakdown[damageType], s_format("x %.2f ^8(fist of war effect modifier)", output.FistOfWarHitEffect))
|
|
end
|
|
if globalOutput.OffensiveWarcryEffect ~= 1 and not activeSkill.skillModList:Flag(nil, "Condition:WarcryMaxHit") then
|
|
t_insert(breakdown[damageType], s_format("x %.2f ^8(aggregated warcry exerted effect modifier)", globalOutput.OffensiveWarcryEffect))
|
|
end
|
|
if globalOutput.MaxOffensiveWarcryEffect ~= 1 and activeSkill.skillModList:Flag(nil, "Condition:WarcryMaxHit") then
|
|
t_insert(breakdown[damageType], s_format("x %.2f ^8(aggregated max warcry exerted effect modifier)", globalOutput.MaxOffensiveWarcryEffect))
|
|
end
|
|
end
|
|
if activeSkill.skillModList:Flag(nil, "Condition:WarcryMaxHit") then
|
|
output.allMult = convMult * output.ScaledDamageEffect * output.RuthlessBlowEffect * output.FistOfWarHitEffect * globalOutput.MaxOffensiveWarcryEffect
|
|
else
|
|
output.allMult = convMult * output.ScaledDamageEffect * output.RuthlessBlowEffect * output.FistOfWarHitEffect * globalOutput.OffensiveWarcryEffect
|
|
end
|
|
local allMult = output.allMult
|
|
if pass == 1 then
|
|
-- Apply crit multiplier
|
|
allMult = allMult * output.CritMultiplier
|
|
end
|
|
damageTypeHitMin = damageTypeHitMin * allMult
|
|
damageTypeHitMax = damageTypeHitMax * allMult
|
|
if skillModList:Flag(skillCfg, "LuckyHits")
|
|
or (pass == 2 and damageType == "Lightning" and skillModList:Flag(skillCfg, "LightningNoCritLucky"))
|
|
or (pass == 1 and skillModList:Flag(skillCfg, "CritLucky"))
|
|
or ((damageType == "Lightning" or damageType == "Cold" or damageType == "Fire") and skillModList:Flag(skillCfg, "ElementalLuckHits")) then
|
|
damageTypeHitAvg = (damageTypeHitMin / 3 + 2 * damageTypeHitMax / 3)
|
|
else
|
|
damageTypeHitAvg = (damageTypeHitMin / 2 + damageTypeHitMax / 2)
|
|
end
|
|
if (damageTypeHitMin ~= 0 or damageTypeHitMax ~= 0) and env.mode_effective then
|
|
-- Apply enemy resistances and damage taken modifiers
|
|
local resist = 0
|
|
local pen = 0
|
|
local sourceRes = 0
|
|
local takenInc = enemyDB:Sum("INC", cfg, "DamageTaken", damageType.."DamageTaken")
|
|
local takenMore = enemyDB:More(cfg, "DamageTaken", damageType.."DamageTaken")
|
|
-- Check if player is supposed to ignore a damage type, or if it's ignored on enemy side
|
|
local useThisResist = function(damageType)
|
|
return not skillModList:Flag(cfg, "Ignore"..damageType.."Resistance", isElemental[damageType] and "IgnoreElementalResistances" or nil) and not enemyDB:Flag(nil, "SelfIgnore"..damageType.."Resistance")
|
|
end
|
|
if damageType == "Physical" then
|
|
if isAttack then
|
|
-- store pre-armour physical damage from attacks for impale calculations
|
|
if pass == 1 then
|
|
output.impaleStoredHitAvg = output.impaleStoredHitAvg + damageTypeHitAvg * (output.CritChance / 100)
|
|
else
|
|
output.impaleStoredHitAvg = output.impaleStoredHitAvg + damageTypeHitAvg * (1 - output.CritChance / 100)
|
|
end
|
|
end
|
|
local enemyArmour = calcLib.val(enemyDB, "Armour")
|
|
local armourReduction = calcs.armourReductionF(enemyArmour, damageTypeHitAvg)
|
|
resist = m_max(0, enemyDB:Sum("BASE", nil, "PhysicalDamageReduction") + skillModList:Sum("BASE", cfg, "EnemyPhysicalDamageReduction") + armourReduction)
|
|
else
|
|
resist = enemyDB:Sum("BASE", nil, damageType.."Resist")
|
|
if isElemental[damageType] then
|
|
local base = resist + enemyDB:Sum("BASE", nil, "ElementalResist")
|
|
resist = base * calcLib.mod(enemyDB, nil, damageType.."Resist")
|
|
pen = skillModList:Sum("BASE", cfg, damageType.."Penetration", "ElementalPenetration")
|
|
takenInc = takenInc + enemyDB:Sum("INC", cfg, "ElementalDamageTaken")
|
|
elseif damageType == "Chaos" then
|
|
pen = skillModList:Sum("BASE", cfg, "ChaosPenetration")
|
|
if skillModList:Flag(cfg, "ChaosDamageUsesLowestResistance") then
|
|
-- Default to using Chaos
|
|
local elementUsed = "Chaos"
|
|
-- Find the lowest resist of all the elements and use that if it's lower than chaos
|
|
for _, damageTypeForChaos in ipairs(dmgTypeList) do
|
|
if isElemental[damageTypeForChaos] and useThisResist(damageTypeForChaos) then
|
|
local elementalResistForChaos = enemyDB:Sum("BASE", nil, damageTypeForChaos.."Resist")
|
|
local base = elementalResistForChaos + enemyDB:Sum("BASE", dotTypeCfg, "ElementalResist")
|
|
local currentElementResist = base * calcLib.mod(enemyDB, nil, damageTypeForChaos.."Resist")
|
|
-- If it's explicitly lower, then use the resist and update which element we're using to account for penetration
|
|
if resist > currentElementResist then
|
|
resist = currentElementResist
|
|
elementUsed = damageTypeForChaos
|
|
end
|
|
end
|
|
end
|
|
-- Update the penetration based on the element used
|
|
pen = skillModList:Sum("BASE", cfg, elementUsed.."Penetration", "ElementalPenetration")
|
|
sourceRes = elementUsed
|
|
end
|
|
end
|
|
resist = m_min(resist, data.misc.EnemyMaxResist)
|
|
end
|
|
if skillFlags.projectile then
|
|
takenInc = takenInc + enemyDB:Sum("INC", nil, "ProjectileDamageTaken")
|
|
end
|
|
if skillFlags.projectile and skillFlags.attack then
|
|
takenInc = takenInc + enemyDB:Sum("INC", nil, "ProjectileAttackDamageTaken")
|
|
end
|
|
if skillFlags.trap or skillFlags.mine then
|
|
takenInc = takenInc + enemyDB:Sum("INC", nil, "TrapMineDamageTaken")
|
|
end
|
|
local effMult = (1 + takenInc / 100) * takenMore
|
|
if skillModList:Flag(cfg, isElemental[damageType] and "CannotElePenIgnore" or nil) then
|
|
effMult = effMult * (1 - resist / 100)
|
|
elseif useThisResist(damageType) then
|
|
effMult = effMult * (1 - (resist - pen) / 100)
|
|
end
|
|
damageTypeHitMin = damageTypeHitMin * effMult
|
|
damageTypeHitMax = damageTypeHitMax * effMult
|
|
damageTypeHitAvg = damageTypeHitAvg * effMult
|
|
if env.mode == "CALCS" then
|
|
output[damageType.."EffMult"] = effMult
|
|
end
|
|
if pass == 2 and breakdown and effMult ~= 1 and skillModList:Flag(cfg, isElemental[damageType] and "CannotElePenIgnore" or nil) then
|
|
t_insert(breakdown[damageType], s_format("x %.3f ^8(effective DPS modifier)", effMult))
|
|
breakdown[damageType.."EffMult"] = breakdown.effMult(damageType, resist, 0, takenInc, effMult, takenMore, sourceRes)
|
|
elseif 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, takenInc, effMult, takenMore, sourceRes)
|
|
end
|
|
end
|
|
if pass == 2 and breakdown then
|
|
t_insert(breakdown[damageType], s_format("= %d to %d", damageTypeHitMin, damageTypeHitMax))
|
|
end
|
|
|
|
-- Beginning of Leech Calculation for this DamageType
|
|
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 + damageTypeHitAvg * 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 + damageTypeHitAvg * 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 + damageTypeHitAvg * 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 + damageTypeHitAvg * manaLeech / 100
|
|
end
|
|
end
|
|
end
|
|
else
|
|
if breakdown then
|
|
breakdown[damageType] = {
|
|
"You can't deal "..damageType.." damage"
|
|
}
|
|
end
|
|
end
|
|
if pass == 1 then
|
|
output[damageType.."CritAverage"] = damageTypeHitAvg
|
|
totalCritAvg = totalCritAvg + damageTypeHitAvg
|
|
totalCritMin = totalCritMin + damageTypeHitMin
|
|
totalCritMax = totalCritMax + damageTypeHitMax
|
|
else
|
|
if env.mode == "CALCS" then
|
|
output[damageType.."Min"] = damageTypeHitMin
|
|
output[damageType.."Max"] = damageTypeHitMax
|
|
end
|
|
output[damageType.."HitAverage"] = damageTypeHitAvg
|
|
totalHitAvg = totalHitAvg + damageTypeHitAvg
|
|
totalHitMin = totalHitMin + damageTypeHitMin
|
|
totalHitMax = totalHitMax + damageTypeHitMax
|
|
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"] / totalHitAvg * 100))
|
|
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 / data.misc.LeechRateBase
|
|
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")
|
|
output.EnergyShieldOnHit = skillModList:Sum("BASE", cfg, "EnergyShieldOnHit") + enemyDB:Sum("BASE", cfg, "SelfEnergyShieldOnHit")
|
|
output.ManaOnHit = skillModList:Sum("BASE", cfg, "ManaOnHit") + enemyDB:Sum("BASE", cfg, "SelfManaOnHit")
|
|
end
|
|
output.LifeOnHitRate = output.LifeOnHit * hitRate
|
|
output.EnergyShieldOnHitRate = output.EnergyShieldOnHit * hitRate
|
|
output.ManaOnHitRate = output.ManaOnHit * hitRate
|
|
|
|
-- Calculate average damage and final DPS
|
|
output.AverageHit = totalHitAvg * (1 - output.CritChance / 100) + totalCritAvg * 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 = { }
|
|
if skillModList:Flag(skillCfg, "LuckyHits") then
|
|
t_insert(breakdown.AverageHit, s_format("(1/3) x %d + (2/3) x %d = %.1f ^8(average from non-crits)", totalHitMin, totalHitMax, totalHitAvg))
|
|
end
|
|
if skillModList:Flag(skillCfg, "CritLucky") or skillModList:Flag(skillCfg, "LuckyHits") then
|
|
t_insert(breakdown.AverageHit, s_format("(1/3) x %d + (2/3) x %d = %.1f ^8(average from crits)", totalCritMin, totalCritMax, totalCritAvg))
|
|
t_insert(breakdown.AverageHit, "")
|
|
end
|
|
t_insert(breakdown.AverageHit, s_format("%.1f x (1 - %.4f) ^8(damage from non-crits)", totalHitAvg, output.CritChance / 100))
|
|
t_insert(breakdown.AverageHit, s_format("+ %.1f x %.4f ^8(damage from crits)", totalCritAvg, output.CritChance / 100))
|
|
t_insert(breakdown.AverageHit, 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 * data.misc.LeechRateBase * calcLib.mod(skillModList, skillCfg, "LifeLeechRate")
|
|
output.LifeLeechRate = output.LifeLeechInstantRate + m_min(output.LifeLeechInstances * output.LifeLeechInstanceRate, output.MaxLifeLeechRate) * output.LifeRecoveryRateMod
|
|
output.LifeLeechPerHit = output.LifeLeechInstant + m_min(output.LifeLeechInstanceRate, output.MaxLifeLeechRate) * output.LifeLeechDuration * output.LifeRecoveryRateMod
|
|
output.EnergyShieldLeechInstanceRate = output.EnergyShield * data.misc.LeechRateBase * calcLib.mod(skillModList, skillCfg, "EnergyShieldLeechRate")
|
|
output.EnergyShieldLeechRate = output.EnergyShieldLeechInstantRate + m_min(output.EnergyShieldLeechInstances * output.EnergyShieldLeechInstanceRate, output.MaxEnergyShieldLeechRate) * output.EnergyShieldRecoveryRateMod
|
|
output.EnergyShieldLeechPerHit = output.EnergyShieldLeechInstant + m_min(output.EnergyShieldLeechInstanceRate, output.MaxEnergyShieldLeechRate) * output.EnergyShieldLeechDuration * output.EnergyShieldRecoveryRateMod
|
|
output.ManaLeechInstanceRate = output.Mana * data.misc.LeechRateBase * calcLib.mod(skillModList, skillCfg, "ManaLeechRate")
|
|
output.ManaLeechRate = output.ManaLeechInstantRate + m_min(output.ManaLeechInstances * output.ManaLeechInstanceRate, output.MaxManaLeechRate) * output.ManaRecoveryRateMod
|
|
output.ManaLeechPerHit = output.ManaLeechInstant + 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
|
|
|
|
skillFlags.bleed = false
|
|
skillFlags.poison = false
|
|
skillFlags.ignite = false
|
|
skillFlags.igniteCanStack = skillModList:Flag(skillCfg, "IgniteCanStack")
|
|
skillFlags.shock = false
|
|
skillFlags.freeze = false
|
|
skillFlags.impale = false
|
|
skillFlags.chill = false
|
|
skillFlags.scorch = false
|
|
skillFlags.brittle = false
|
|
skillFlags.sap = 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, "CannotChill") then
|
|
output.ChillChanceOnCrit = 0
|
|
else
|
|
output.ChillChanceOnCrit = 100
|
|
end
|
|
if skillModList:Flag(cfg, "CritAlwaysAltAilments") and not skillModList:Flag(cfg, "NeverCrit") then
|
|
skillFlags.inflictScorch = true
|
|
skillFlags.inflictBrittle = true
|
|
skillFlags.inflictSap = true
|
|
end
|
|
if skillModList:Flag(cfg, "CritAlwaysAltAilments") and not skillModList:Flag(cfg, "NeverCrit") and skillFlags.hit then
|
|
output.ScorchChanceOnCrit = 100
|
|
output.BrittleChanceOnCrit = 100
|
|
output.SapChanceOnCrit = 100
|
|
else
|
|
output.ScorchChanceOnCrit = 0
|
|
output.BrittleChanceOnCrit = 0
|
|
output.SapChanceOnCrit = 0
|
|
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, "CannotChill") then
|
|
output.ChillChanceOnHit = 0
|
|
else
|
|
output.ChillChanceOnHit = 100
|
|
end
|
|
if not skillFlags.hit or skillModList:Flag(cfg, "CannotKnockback") then
|
|
output.KnockbackChanceOnHit = 0
|
|
else
|
|
output.KnockbackChanceOnHit = skillModList:Sum("BASE", cfg, "EnemyKnockbackChance")
|
|
end
|
|
if skillModList:Sum("BASE", cfg, "ScorchChance") > 0 then
|
|
skillFlags.inflictScorch = true
|
|
end
|
|
if skillModList:Sum("BASE", cfg, "ScorchChance") > 0 and skillFlags.hit then
|
|
output.ScorchChanceOnHit = m_min(100, skillModList:Sum("BASE", cfg, "ScorchChance"))
|
|
else
|
|
output.ScorchChanceOnHit = 0
|
|
end
|
|
if skillModList:Sum("BASE", cfg, "BrittleChance") > 0 then
|
|
skillFlags.inflictBrittle = true
|
|
end
|
|
if skillModList:Sum("BASE", cfg, "BrittleChance") > 0 and skillFlags.hit then
|
|
output.BrittleChanceOnHit = m_min(100, skillModList:Sum("BASE", cfg, "BrittleChance"))
|
|
else
|
|
output.BrittleChanceOnHit = 0
|
|
end
|
|
if skillModList:Sum("BASE", cfg, "SapChance") > 0 then
|
|
skillFlags.inflictSap = true
|
|
end
|
|
if skillModList:Sum("BASE", cfg, "SapChance") > 0 and skillFlags.hit then
|
|
output.SapChanceOnHit = m_min(100, skillModList:Sum("BASE", cfg, "SapChance"))
|
|
else
|
|
output.SapChanceOnHit = 0
|
|
end
|
|
if not skillFlags.attack then
|
|
output.ImpaleChance = 0
|
|
else
|
|
output.ImpaleChance = m_min(100, skillModList:Sum("BASE", cfg, "ImpaleChance"))
|
|
end
|
|
if skillModList:Sum("BASE", cfg, "FireExposureChance") > 0 then
|
|
skillFlags.applyFireExposure = true
|
|
end
|
|
if skillModList:Sum("BASE", cfg, "ColdExposureChance") > 0 then
|
|
skillFlags.applyColdExposure = true
|
|
end
|
|
if skillModList:Sum("BASE", cfg, "LightningExposureChance") > 0 then
|
|
skillFlags.applyLightningExposure = true
|
|
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
|
|
local scorchMult = (1 - enemyDB:Sum("BASE", nil, "AvoidScorch") / 100)
|
|
output.ScorchChanceOnHit = output.ScorchChanceOnHit * scorchMult
|
|
output.ScorchChanceOnCrit = output.ScorchChanceOnCrit * scorchMult
|
|
local brittleMult = (1 - enemyDB:Sum("BASE", nil, "AvoidBrittle") / 100)
|
|
output.BrittleChanceOnHit = output.BrittleChanceOnHit * brittleMult
|
|
output.BrittleChanceOnCrit = output.BrittleChanceOnCrit * brittleMult
|
|
local sapMult = (1 - enemyDB:Sum("BASE", nil, "AvoidSap") / 100)
|
|
output.SapChanceOnHit = output.SapChanceOnHit * sapMult
|
|
output.SapChanceOnCrit = output.SapChanceOnCrit * sapMult
|
|
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
|
|
local sourceMult = skillModList:More(nil, type.."AsThoughDealing")
|
|
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))
|
|
if sourceMult > 1 then
|
|
t_insert(breakdownDPS, s_format("x %.2f ^8(inflicting as though dealing more damage)", sourceMult))
|
|
t_insert(breakdownDPS, s_format("= %.1f", baseVal * sourceMult))
|
|
end
|
|
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)))
|
|
if sourceMult == 1 or baseFromCrit ~= 0 then
|
|
t_insert(breakdownDPS, s_format("= %.1f", baseFromHit))
|
|
end
|
|
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)))
|
|
if sourceMult == 1 or baseFromHit ~= 0 then
|
|
t_insert(breakdownDPS, s_format("= %.1f", baseFromCrit))
|
|
end
|
|
end
|
|
if baseFromHit > 0 and baseFromCrit > 0 then
|
|
t_insert(breakdownDPS, "Total damage:")
|
|
t_insert(breakdownDPS, s_format("%.1f + %.1f", baseFromHit, baseFromCrit))
|
|
if sourceMult == 1 then
|
|
t_insert(breakdownDPS, s_format("= %.1f", baseVal))
|
|
end
|
|
end
|
|
if sourceMult > 1 then
|
|
t_insert(breakdownDPS, s_format("x %.2f ^8(inflicting as though dealing more damage)", sourceMult))
|
|
t_insert(breakdownDPS, s_format("= %.1f", baseVal * sourceMult))
|
|
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(cfg.flags, ModFlag.WeaponMask), band(cfg.flags, ModFlag.Melee) ~= 0 and ModFlag.MeleeHit or 0),
|
|
keywordFlags = bor(band(cfg.keywordFlags, bnot(KeywordFlag.Hit)), KeywordFlag.Bleed, KeywordFlag.Ailment, KeywordFlag.PhysicalDot),
|
|
skillCond = setmetatable({["CriticalStrike"] = true }, { __index = skillCfg.skillCond } ),
|
|
}
|
|
end
|
|
local dotCfg = activeSkill.bleedCfg
|
|
local sourceHitDmg, sourceCritDmg
|
|
if breakdown then
|
|
breakdown.BleedPhysical = { damageTypes = { } }
|
|
end
|
|
for pass = 1, 2 do
|
|
if skillModList:Flag(dotCfg, "AilmentsAreNeverFromCrit") or pass == 1 then
|
|
dotCfg.skillCond["CriticalStrike"] = false
|
|
else
|
|
dotCfg.skillCond["CriticalStrike"] = true
|
|
end
|
|
local min, max = calcAilmentSourceDamage(activeSkill, output, dotCfg, pass == 1 and breakdown and breakdown.BleedPhysical, "Physical", 0)
|
|
output.BleedPhysicalMin = min
|
|
output.BleedPhysicalMax = max
|
|
if pass == 2 then
|
|
globalOutput.CritBleedDotMulti = 1 + skillModList:Sum("BASE", dotCfg, "DotMultiplier", "PhysicalDotMultiplier") / 100
|
|
sourceCritDmg = (min + max) / 2 * globalOutput.CritBleedDotMulti
|
|
else
|
|
globalOutput.BleedDotMulti = 1 + skillModList:Sum("BASE", dotCfg, "DotMultiplier", "PhysicalDotMultiplier") / 100
|
|
sourceHitDmg = (min + max) / 2 * globalOutput.BleedDotMulti
|
|
end
|
|
end
|
|
local igniteMode = env.configInput.igniteMode or "AVERAGE"
|
|
if igniteMode == "CRIT" then
|
|
output.BleedChanceOnHit = 0
|
|
end
|
|
if globalBreakdown then
|
|
globalBreakdown.BleedDPS = {
|
|
s_format("Ailment mode: %s ^8(can be changed in the Configuration tab)", igniteMode == "CRIT" and "Crits Only" or "Average Damage")
|
|
}
|
|
end
|
|
local basePercent = skillData.bleedBasePercent or data.misc.BleedPercentBase
|
|
local baseVal = calcAilmentDamage("Bleed", sourceHitDmg, sourceCritDmg) * basePercent / 100 * output.RuthlessBlowEffect * output.FistOfWarAilmentEffect * globalOutput.AilmentWarcryEffect
|
|
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 takenInc = enemyDB:Sum("INC", dotCfg, "DamageTaken", "DamageTakenOverTime", "PhysicalDamageTaken", "PhysicalDamageTakenOverTime")
|
|
local takenMore = enemyDB:More(dotCfg, "DamageTaken", "DamageTakenOverTime", "PhysicalDamageTaken", "PhysicalDamageTakenOverTime")
|
|
effMult = (1 - resist / 100) * (1 + takenInc / 100) * takenMore
|
|
globalOutput["BleedEffMult"] = effMult
|
|
if breakdown and effMult ~= 1 then
|
|
globalBreakdown.BleedEffMult = breakdown.effMult("Physical", resist, 0, takenInc, effMult, takenMore, sourceRes)
|
|
end
|
|
end
|
|
local mult = skillModList:Sum("BASE", dotCfg, "PhysicalDotMultiplier", "BleedMultiplier")
|
|
local effectMod = calcLib.mod(skillModList, dotCfg, "AilmentEffect")
|
|
local rateMod = calcLib.mod(skillModList, cfg, "BleedFaster") + enemyDB:Sum("INC", nil, "SelfBleedFaster") / 100
|
|
local maxStacks = skillModList:Override(cfg, "BleedStacksMax") or skillModList:Sum("BASE", cfg, "BleedStacksMax")
|
|
local configStacks = enemyDB:Sum("BASE", nil, "Multiplier:BleedStacks")
|
|
local bleedStacks = configStacks > 0 and m_min(configStacks, maxStacks) or maxStacks
|
|
output.BaseBleedDPS = baseVal * effectMod * rateMod * effMult
|
|
output.BleedDPS = (baseVal * effectMod * rateMod * effMult) * bleedStacks
|
|
local durationBase
|
|
if skillData.bleedDurationIsSkillDuration then
|
|
durationBase = skillData.duration
|
|
else
|
|
durationBase = data.misc.BleedDurationBase
|
|
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
|
|
globalOutput.BleedDamage = output.BaseBleedDPS * globalOutput.BleedDuration
|
|
globalOutput.BleedStacksMax = maxStacks
|
|
globalOutput.BleedStacks = bleedStacks
|
|
if breakdown then
|
|
if globalOutput.CritBleedDotMulti and (globalOutput.CritBleedDotMulti ~= globalOutput.BleedDotMulti) then
|
|
local chanceFromHit = output.BleedChanceOnHit / 100 * (1 - globalOutput.CritChance / 100)
|
|
local chanceFromCrit = output.BleedChanceOnCrit / 100 * output.CritChance / 100
|
|
local totalFromHit = chanceFromHit / (chanceFromHit + chanceFromCrit)
|
|
local totalFromCrit = chanceFromCrit / (chanceFromHit + chanceFromCrit)
|
|
globalBreakdown.BleedDotMulti = breakdown.critDot(globalOutput.BleedDotMulti, globalOutput.CritBleedDotMulti, totalFromHit, totalFromCrit)
|
|
globalOutput.BleedDotMulti = (globalOutput.BleedDotMulti * totalFromHit) + (globalOutput.CritBleedDotMulti * totalFromCrit)
|
|
end
|
|
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 ~= 1 then
|
|
t_insert(breakdown.BleedDPS, s_format("x %.2f ^8(ruthless blow effect modifier)", output.RuthlessBlowEffect))
|
|
end
|
|
if output.FistOfWarAilmentEffect ~= 1 then
|
|
t_insert(breakdown.BleedDPS, s_format("x %.2f ^8(fist of war effect modifier)", output.FistOfWarAilmentEffect))
|
|
end
|
|
if globalOutput.AilmentWarcryEffect > 1 then
|
|
t_insert(breakdown.BleedDPS, s_format("x %.2f ^8(combined ailment warcry effect modifier)", globalOutput.AilmentWarcryEffect))
|
|
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(cfg.flags, ModFlag.WeaponMask), band(cfg.flags, ModFlag.Melee) ~= 0 and ModFlag.MeleeHit or 0),
|
|
keywordFlags = bor(band(cfg.keywordFlags, bnot(KeywordFlag.Hit)), KeywordFlag.Poison, KeywordFlag.Ailment, KeywordFlag.ChaosDot),
|
|
skillCond = setmetatable({["CriticalStrike"] = true }, { __index = skillCfg.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
|
|
if skillModList:Flag(dotCfg, "AilmentsAreNeverFromCrit") or pass == 1 then
|
|
dotCfg.skillCond["CriticalStrike"] = false
|
|
else
|
|
dotCfg.skillCond["CriticalStrike"] = true
|
|
end
|
|
local totalMin, totalMax = 0, 0
|
|
do
|
|
local min, max = calcAilmentSourceDamage(activeSkill, output, dotCfg, pass == 1 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 == 2) 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 == 1 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 == 1 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 == 1 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 == 1 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 == 2 then
|
|
globalOutput.CritPoisonDotMulti = 1 + skillModList:Sum("BASE", dotCfg, "DotMultiplier", "ChaosDotMultiplier") / 100
|
|
sourceCritDmg = (totalMin + totalMax) / 2 * globalOutput.CritPoisonDotMulti
|
|
else
|
|
globalOutput.PoisonDotMulti = 1 + skillModList:Sum("BASE", dotCfg, "DotMultiplier", "ChaosDotMultiplier") / 100
|
|
sourceHitDmg = (totalMin + totalMax) / 2 * globalOutput.PoisonDotMulti
|
|
end
|
|
end
|
|
local igniteMode = env.configInput.igniteMode or "AVERAGE"
|
|
if igniteMode == "CRIT" then
|
|
output.PoisonChanceOnHit = 0
|
|
end
|
|
if globalBreakdown then
|
|
globalBreakdown.PoisonDPS = {
|
|
s_format("Ailment mode: %s ^8(can be changed in the Configuration tab)", igniteMode == "CRIT" and "Crits Only" or "Average Damage")
|
|
}
|
|
end
|
|
local baseVal = calcAilmentDamage("Poison", sourceHitDmg, sourceCritDmg) * data.misc.PoisonPercentBase * output.FistOfWarAilmentEffect * globalOutput.AilmentWarcryEffect
|
|
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") * calcLib.mod(enemyDB, nil, "ChaosResist"), data.misc.EnemyMaxResist)
|
|
local takenInc = enemyDB:Sum("INC", dotCfg, "DamageTaken", "DamageTakenOverTime", "ChaosDamageTaken", "ChaosDamageTakenOverTime")
|
|
local takenMore = enemyDB:More(dotCfg, "DamageTaken", "DamageTakenOverTime", "ChaosDamageTaken", "ChaosDamageTakenOverTime")
|
|
effMult = (1 - resist / 100) * (1 + takenInc / 100) * takenMore
|
|
globalOutput["PoisonEffMult"] = effMult
|
|
if breakdown and effMult ~= 1 then
|
|
globalBreakdown.PoisonEffMult = breakdown.effMult("Chaos", resist, 0, takenInc, effMult, takenMore, sourceRes)
|
|
end
|
|
end
|
|
local effectMod = calcLib.mod(skillModList, dotCfg, "AilmentEffect")
|
|
local rateMod = calcLib.mod(skillModList, cfg, "PoisonFaster") + enemyDB:Sum("INC", nil, "SelfPoisonFaster") / 100
|
|
output.PoisonDPS = baseVal * effectMod * rateMod * effMult
|
|
local durationBase
|
|
if skillData.poisonDurationIsSkillDuration then
|
|
durationBase = skillData.duration
|
|
else
|
|
durationBase = data.misc.PoisonDurationBase
|
|
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
|
|
output.TotalPoisonDPS = output.PoisonDPS
|
|
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
|
|
if globalOutput.CritPoisonDotMulti and (globalOutput.CritPoisonDotMulti ~= globalOutput.PoisonDotMulti) then
|
|
local chanceFromHit = output.PoisonChanceOnHit / 100 * (1 - globalOutput.CritChance / 100)
|
|
local chanceFromCrit = output.PoisonChanceOnCrit / 100 * output.CritChance / 100
|
|
local totalFromHit = chanceFromHit / (chanceFromHit + chanceFromCrit)
|
|
local totalFromCrit = chanceFromCrit / (chanceFromHit + chanceFromCrit)
|
|
globalBreakdown.PoisonDotMulti = breakdown.critDot(globalOutput.PoisonDotMulti, globalOutput.CritPoisonDotMulti, totalFromHit, totalFromCrit)
|
|
globalOutput.PoisonDotMulti = (globalOutput.PoisonDotMulti * totalFromHit) + (globalOutput.CritPoisonDotMulti * totalFromCrit)
|
|
end
|
|
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(cfg.flags, ModFlag.WeaponMask), band(cfg.flags, ModFlag.Melee) ~= 0 and ModFlag.MeleeHit or 0),
|
|
keywordFlags = bor(band(cfg.keywordFlags, bnot(KeywordFlag.Hit)), KeywordFlag.Ignite, KeywordFlag.Ailment, KeywordFlag.FireDot),
|
|
skillCond = setmetatable({["CriticalStrike"] = true }, { __index = skillCfg.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
|
|
if skillModList:Flag(dotCfg, "AilmentsAreNeverFromCrit") or pass == 1 then
|
|
dotCfg.skillCond["CriticalStrike"] = false
|
|
else
|
|
dotCfg.skillCond["CriticalStrike"] = true
|
|
end
|
|
local totalMin, totalMax = 0, 0
|
|
if canDeal.Physical and skillModList:Flag(cfg, "PhysicalCanIgnite") then
|
|
local min, max = calcAilmentSourceDamage(activeSkill, output, dotCfg, pass == 1 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 == 1 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 == 1 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 == 1 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 == 1 and breakdown and breakdown.IgniteChaos, "Chaos", dmgTypeFlags.Fire)
|
|
output.IgniteChaosMin = min
|
|
output.IgniteChaosMax = max
|
|
totalMin = totalMin + min
|
|
totalMax = totalMax + max
|
|
end
|
|
if pass == 2 then
|
|
globalOutput.CritIgniteDotMulti = 1 + skillModList:Sum("BASE", dotCfg, "DotMultiplier", "FireDotMultiplier") / 100
|
|
sourceCritDmg = (totalMin + totalMax) / 2 * globalOutput.CritIgniteDotMulti
|
|
else
|
|
globalOutput.IgniteDotMulti = 1 + skillModList:Sum("BASE", dotCfg, "DotMultiplier", "FireDotMultiplier") / 100
|
|
sourceHitDmg = (totalMin + totalMax) / 2 * globalOutput.IgniteDotMulti
|
|
end
|
|
end
|
|
local igniteMode = env.configInput.igniteMode or "AVERAGE"
|
|
if igniteMode == "CRIT" then
|
|
output.IgniteChanceOnHit = 0
|
|
end
|
|
if globalBreakdown then
|
|
globalBreakdown.IgniteDPS = {
|
|
s_format("Ailment mode: %s ^8(can be changed in the Configuration tab)", igniteMode == "CRIT" and "Crits Only" or "Average Damage")
|
|
}
|
|
end
|
|
local baseVal = calcAilmentDamage("Ignite", sourceHitDmg, sourceCritDmg) * data.misc.IgnitePercentBase * output.FistOfWarAilmentEffect * globalOutput.AilmentWarcryEffect
|
|
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") * calcLib.mod(enemyDB, nil, "FireResist", "ElementalResist"), data.misc.EnemyMaxResist)
|
|
local takenInc = enemyDB:Sum("INC", dotCfg, "DamageTaken", "DamageTakenOverTime", "FireDamageTaken", "FireDamageTakenOverTime", "ElementalDamageTaken")
|
|
local takenMore = enemyDB:More(dotCfg, "DamageTaken", "DamageTakenOverTime", "FireDamageTaken", "FireDamageTakenOverTime", "ElementalDamageTaken")
|
|
effMult = (1 - resist / 100) * (1 + takenInc / 100) * takenMore
|
|
globalOutput["IgniteEffMult"] = effMult
|
|
if breakdown and effMult ~= 1 then
|
|
globalBreakdown.IgniteEffMult = breakdown.effMult("Fire", resist, 0, takenInc, effMult, takenMore, sourceRes)
|
|
end
|
|
end
|
|
local effectMod = calcLib.mod(skillModList, dotCfg, "AilmentEffect")
|
|
local rateMod = (calcLib.mod(skillModList, cfg, "IgniteBurnFaster") + enemyDB:Sum("INC", nil, "SelfIgniteBurnFaster") / 100) / 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 = data.misc.IgniteDurationBase * (1 + incDur / 100) * moreDur / rateMod * debuffDurationMult
|
|
globalOutput.IgniteDamage = output.IgniteDPS * globalOutput.IgniteDuration
|
|
if skillFlags.igniteCanStack then
|
|
output.IgniteDamage = output.IgniteDPS * globalOutput.IgniteDuration
|
|
output.TotalIgniteStacks = 1 + skillModList:Sum("BASE", cfg, "IgniteStacks")
|
|
output.TotalIgniteDPS = output.IgniteDPS * output.TotalIgniteStacks
|
|
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 globalOutput.CritIgniteDotMulti and (globalOutput.CritIgniteDotMulti ~= globalOutput.IgniteDotMulti) then
|
|
local chanceFromHit = output.IgniteChanceOnHit / 100 * (1 - globalOutput.CritChance / 100)
|
|
local chanceFromCrit = output.IgniteChanceOnCrit / 100 * output.CritChance / 100
|
|
local totalFromHit = chanceFromHit / (chanceFromHit + chanceFromCrit)
|
|
local totalFromCrit = chanceFromCrit / (chanceFromHit + chanceFromCrit)
|
|
globalBreakdown.IgniteDotMulti = breakdown.critDot(globalOutput.IgniteDotMulti, globalOutput.CritIgniteDotMulti, totalFromHit, totalFromCrit)
|
|
globalOutput.IgniteDotMulti = (globalOutput.IgniteDotMulti * totalFromHit) + (globalOutput.CritIgniteDotMulti * totalFromCrit)
|
|
end
|
|
if skillFlags.igniteCanStack then
|
|
breakdown.IgniteDamage = { }
|
|
if isAttack then
|
|
t_insert(breakdown.IgniteDamage, pass.label..":")
|
|
end
|
|
t_insert(breakdown.IgniteDamage, s_format("%.1f ^8(damage per second)", output.IgniteDPS))
|
|
t_insert(breakdown.IgniteDamage, s_format("x %.2fs ^8(ignite duration)", globalOutput.IgniteDuration))
|
|
t_insert(breakdown.IgniteDamage, s_format("= %.1f ^8damage per ignite stack", output.IgniteDamage))
|
|
end
|
|
if incDur ~= 0 or moreDur ~= 1 or rateMod ~= 1 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
|
|
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 igniteMode = env.configInput.igniteMode or "AVERAGE"
|
|
if igniteMode == "CRIT" then
|
|
output.ShockChanceOnHit = 0
|
|
end
|
|
if globalBreakdown then
|
|
globalBreakdown.ShockDurationMod = {
|
|
s_format("Ailment mode: %s ^8(can be changed in the Configuration tab)", igniteMode == "CRIT" and "Crits Only" or "Average Damage")
|
|
}
|
|
end
|
|
local baseVal = calcAilmentDamage("Shock", sourceHitDmg, sourceCritDmg) * skillModList:More(cfg, "ShockAsThoughDealing")
|
|
if baseVal > 0 then
|
|
skillFlags.shock = true
|
|
output.ShockDurationMod = 1 + skillModList:Sum("INC", cfg, "EnemyShockDuration") / 100 + enemyDB:Sum("INC", nil, "SelfShockDuration") / 100
|
|
output.ShockEffectMod = skillModList:Sum("INC", cfg, "EnemyShockEffect")
|
|
local maximum = skillModList:Override(nil, "ShockMax") or 50
|
|
local current = m_min(globalOutput.CurrentShock or 0, maximum)
|
|
local desired = m_min(enemyDB:Sum("BASE", nil, "DesiredShockVal"), maximum)
|
|
local enemyThreshold = enemyDB:Sum("BASE", nil, "AilmentThreshold") * enemyDB:More(nil, "Life")
|
|
local effList = { 5, 15, 50 }
|
|
if enemyThreshold > 0 then
|
|
local bossEffect = 100 * 0.5 * ((baseVal / enemyThreshold) ^ (0.4)) * (1 + output.ShockEffectMod / 100)
|
|
t_insert(effList, bossEffect)
|
|
end
|
|
if maximum ~= 50 then
|
|
t_insert(effList, maximum)
|
|
end
|
|
if current > 5 and current ~= (15 or 50 or maximum) and current < maximum then
|
|
t_insert(effList, current)
|
|
end
|
|
if desired > 5 and desired ~= (15 or 50 or current or maximum) and desired < maximum and current == 0 then
|
|
t_insert(effList, desired)
|
|
end
|
|
table.sort(effList)
|
|
if breakdown then
|
|
if current > 0 then
|
|
breakdown.ShockDPS.label = s_format("To Shock for %.1f seconds ^8(with a ^7%s%% ^8shock on the enemy)^7", 2 * output.ShockDurationMod, current)
|
|
else
|
|
breakdown.ShockDPS.label = s_format("To Shock for %.1f seconds", 2 * output.ShockDurationMod)
|
|
end
|
|
breakdown.ShockDPS.footer = s_format("^8(ailment threshold is about equal to life, except on bosses where it is about half their life)")
|
|
breakdown.ShockDPS.rowList = { }
|
|
breakdown.ShockDPS.colList = {
|
|
{ label = "Shock Effect", key = "effect" },
|
|
{ label = "Ailment Threshold", key = "thresh" },
|
|
}
|
|
for _, value in ipairs(effList) do
|
|
local thresh = (((100 + output.ShockEffectMod)^(2.5)) * baseVal) / ((2 * value) ^ (2.5))
|
|
local decCheck = value / m_floor(value)
|
|
value = m_floor(value)
|
|
local threshString = ""
|
|
if m_floor(thresh + 0.5) == m_floor(enemyThreshold + 0.5) then
|
|
threshString = s_format("%.0f ^8(AL%.0f %s)", thresh, skillModList:Sum("BASE", nil, "AwakeningLevel"), env.configInput.enemyIsBoss)
|
|
else
|
|
threshString = s_format("%.0f", thresh)
|
|
end
|
|
if decCheck ~= 1 then -- don't put a label on the calculated boss effect
|
|
t_insert(breakdown.ShockDPS.rowList, {
|
|
effect = s_format("%s%%", value),
|
|
thresh = threshString,
|
|
})
|
|
elseif current > 0 and value == current then
|
|
t_insert(breakdown.ShockDPS.rowList, {
|
|
effect = s_format("%s%% ^8(current)", value),
|
|
thresh = threshString,
|
|
})
|
|
elseif value == desired then
|
|
t_insert(breakdown.ShockDPS.rowList, {
|
|
effect = s_format("%s%% ^8(desired)", value),
|
|
thresh = threshString,
|
|
})
|
|
elseif value == maximum then
|
|
t_insert(breakdown.ShockDPS.rowList, {
|
|
effect = s_format("%s%% ^8(maximum)", value),
|
|
thresh = threshString,
|
|
})
|
|
elseif value == 5 then
|
|
t_insert(breakdown.ShockDPS.rowList, {
|
|
effect = s_format("%s%% ^8(minimum)", value),
|
|
thresh = threshString,
|
|
})
|
|
else
|
|
t_insert(breakdown.ShockDPS.rowList, {
|
|
effect = s_format("%s%%", value),
|
|
thresh = threshString,
|
|
})
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if (output.ChillChanceOnHit + output.ChillChanceOnCrit) > 0 or (activeSkill.skillTypes[SkillType.ChillingArea] or activeSkill.skillTypes[SkillType.ChillNotHit]) then
|
|
local sourceHitDmg = 0
|
|
local sourceCritDmg = 0
|
|
if canDeal.Cold and not skillModList:Flag(cfg, "ColdCannotChill") then
|
|
sourceHitDmg = sourceHitDmg + output.ColdHitAverage
|
|
sourceCritDmg = sourceCritDmg + output.ColdCritAverage
|
|
end
|
|
if canDeal.Physical and skillModList:Flag(cfg, "PhysicalCanChill") then
|
|
sourceHitDmg = sourceHitDmg + output.PhysicalHitAverage
|
|
sourceCritDmg = sourceCritDmg + output.PhysicalCritAverage
|
|
end
|
|
if canDeal.Lightning and skillModList:Flag(cfg, "LightningCanChill") then
|
|
sourceHitDmg = sourceHitDmg + output.LightningHitAverage
|
|
sourceCritDmg = sourceCritDmg + output.LightningCritAverage
|
|
end
|
|
if canDeal.Fire and skillModList:Flag(cfg, "FireCanChill") then
|
|
sourceHitDmg = sourceHitDmg + output.FireHitAverage
|
|
sourceCritDmg = sourceCritDmg + output.FireCritAverage
|
|
end
|
|
if canDeal.Chaos and skillModList:Flag(cfg, "ChaosCanChill") then
|
|
sourceHitDmg = sourceHitDmg + output.ChaosHitAverage
|
|
sourceCritDmg = sourceCritDmg + output.ChaosCritAverage
|
|
end
|
|
local igniteMode = env.configInput.igniteMode or "AVERAGE"
|
|
if igniteMode == "CRIT" then
|
|
output.ChillChanceOnHit = 0
|
|
end
|
|
if globalBreakdown then
|
|
globalBreakdown.ChillDurationMod = {
|
|
s_format("Ailment mode: %s ^8(can be changed in the Configuration tab)", igniteMode == "CRIT" and "Crits Only" or "Average Damage")
|
|
}
|
|
end
|
|
local baseVal = calcAilmentDamage("Chill", sourceHitDmg, sourceCritDmg) * skillModList:More(cfg, "ChillAsThoughDealing")
|
|
if baseVal > 0 then
|
|
skillFlags.chill = true
|
|
output.ChillEffectMod = skillModList:Sum("INC", cfg, "EnemyChillEffect")
|
|
output.ChillDurationMod = 1 + (skillModList:Sum("INC", cfg, "EnemyChillDuration") + enemyDB:Sum("INC", nil, "SelfChillDuration")) / 100
|
|
local enemyThreshold = enemyDB:Sum("BASE", nil, "AilmentThreshold") * enemyDB:More(nil, "Life")
|
|
effList = { 5, 10, 30 }
|
|
local desired = skillModList:Sum("BASE", nil, "DesiredBonechillEffect") or 0
|
|
if output.BonechillEffect then
|
|
t_insert(effList, output.BonechillEffect)
|
|
end
|
|
if not output.BonechillEffect and desired ~= (0 or 5 or 10 or 30 or output.BonechillEffect) and desired > 5 and desired < 30 then
|
|
t_insert(effList, desired)
|
|
end
|
|
if enemyThreshold > 0 then
|
|
local bossEffect = 100 * 0.5 * ((baseVal / enemyThreshold) ^ (0.4)) * (1 + output.ChillEffectMod / 100)
|
|
t_insert(effList, bossEffect)
|
|
end
|
|
table.sort(effList)
|
|
if breakdown then
|
|
breakdown.ChillDPS.label = s_format("To Chill for %.1f seconds", 2 * output.ChillDurationMod)
|
|
if output.BonechillEffect then
|
|
breakdown.ChillDPS.label = s_format("To Chill for %.1f seconds ^8(with a ^7%s%% ^8Bonechill effect on the enemy)^7", 2 * output.ChillDurationMod, output.BonechillEffect)
|
|
else
|
|
breakdown.ChillDPS.label = s_format("To Chill for %.1f seconds", 2 * output.ChillDurationMod)
|
|
end
|
|
breakdown.ChillDPS.rowList = { }
|
|
breakdown.ChillDPS.colList = {
|
|
{ label = "Chill Effect", key = "effect" },
|
|
{ label = "Ailment Threshold", key = "thresh" },
|
|
}
|
|
breakdown.ChillDPS.footer = s_format("^8(ailment threshold is about equal to life, except on bosses where it is about half their life)")
|
|
for _, value in ipairs(effList) do
|
|
local thresh = (((100 + output.ChillEffectMod)^(2.5)) * baseVal) / ((2 * value) ^ (2.5))
|
|
local decCheck = value / m_floor(value)
|
|
value = m_floor(value)
|
|
if m_floor(thresh + 0.5) == m_floor(enemyThreshold + 0.5) then
|
|
threshString = s_format("%.0f ^8(AL%.0f %s)", thresh, skillModList:Sum("BASE", nil, "AwakeningLevel"), env.configInput.enemyIsBoss)
|
|
else
|
|
threshString = s_format("%.0f", thresh)
|
|
end
|
|
if decCheck ~= 1 then -- don't put a label on the calculated boss effect
|
|
t_insert(breakdown.ChillDPS.rowList, {
|
|
effect = s_format("%s%%", value),
|
|
thresh = threshString,
|
|
})
|
|
elseif value == output.BonechillEffect then
|
|
t_insert(breakdown.ChillDPS.rowList, {
|
|
effect = s_format("%s%% ^8(current)", value),
|
|
thresh = threshString,
|
|
})
|
|
elseif value == desired then
|
|
t_insert(breakdown.ChillDPS.rowList, {
|
|
effect = s_format("%s%% ^8(desired)", value),
|
|
thresh = threshString,
|
|
})
|
|
elseif value == 30 then
|
|
t_insert(breakdown.ChillDPS.rowList, {
|
|
effect = s_format("%s%% ^8(maximum)", value),
|
|
thresh = threshString,
|
|
})
|
|
elseif value == 5 then
|
|
t_insert(breakdown.ChillDPS.rowList, {
|
|
effect = s_format("%s%% ^8(minimum)", value),
|
|
thresh = threshString,
|
|
})
|
|
else
|
|
t_insert(breakdown.ChillDPS.rowList, {
|
|
effect = s_format("%s%%", value),
|
|
thresh = threshString,
|
|
})
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if activeSkill.skillTypes[SkillType.ChillingArea] or activeSkill.skillTypes[SkillType.NonHitChill] then
|
|
skillFlags.chill = true
|
|
output.ChillEffectMod = skillModList:Sum("INC", cfg, "EnemyChillEffect")
|
|
output.ChillDurationMod = 1 + skillModList:Sum("INC", cfg, "EnemyChillDuration") / 100
|
|
output.ChillSourceEffect = m_min(30, m_floor(10 * (1 + output.ChillEffectMod / 100)))
|
|
if breakdown then
|
|
breakdown.DotChill = { }
|
|
breakdown.multiChain(breakdown.DotChill, {
|
|
label = "Effect of Chill: ^8(capped at 30%)",
|
|
base = "10% ^8(base)",
|
|
{ "%.2f ^8(increased effect of chill)", 1 + output.ChillEffectMod / 100},
|
|
total = s_format("= %.0f%%", output.ChillSourceEffect)
|
|
})
|
|
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.Physical and skillModList:Flag(cfg, "PhysicalCanFreeze") then
|
|
sourceHitDmg = sourceHitDmg + output.PhysicalHitAverage
|
|
sourceCritDmg = sourceCritDmg + output.PhysicalCritAverage
|
|
end
|
|
if canDeal.Lightning and skillModList:Flag(cfg, "LightningCanFreeze") then
|
|
sourceHitDmg = sourceHitDmg + output.LightningHitAverage
|
|
sourceCritDmg = sourceCritDmg + output.LightningCritAverage
|
|
end
|
|
if canDeal.Cold and skillModList:Flag(cfg, "ColdCanFreeze") then
|
|
sourceHitDmg = sourceHitDmg + output.ColdHitAverage
|
|
sourceCritDmg = sourceCritDmg + output.ColdCritAverage
|
|
end
|
|
if canDeal.Fire and skillModList:Flag(cfg, "FireCanFreeze") then
|
|
sourceHitDmg = sourceHitDmg + output.FireHitAverage
|
|
sourceCritDmg = sourceCritDmg + output.FireCritAverage
|
|
end
|
|
if canDeal.Chaos and skillModList:Flag(cfg, "ChaosCanFreeze") then
|
|
sourceHitDmg = sourceHitDmg + output.ChaosHitAverage
|
|
sourceCritDmg = sourceCritDmg + output.ChaosCritAverage
|
|
end
|
|
local igniteMode = env.configInput.igniteMode or "AVERAGE"
|
|
if igniteMode == "CRIT" then
|
|
output.FreezeChanceOnHit = 0
|
|
end
|
|
if globalBreakdown then
|
|
globalBreakdown.FreezeDurationMod = {
|
|
s_format("Ailment mode: %s ^8(can be changed in the Configuration tab)", igniteMode == "CRIT" and "Crits Only" or "Average Damage")
|
|
}
|
|
end
|
|
local baseVal = calcAilmentDamage("Freeze", sourceHitDmg, sourceCritDmg) * skillModList:More(cfg, "FreezeAsThoughDealing")
|
|
if baseVal > 0 then
|
|
skillFlags.freeze = true
|
|
skillFlags.chill = 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 for the minimum of 0.3 seconds, target must have no more than %.0f Ailment Threshold.", baseVal * 20 * output.FreezeDurationMod))
|
|
t_insert(breakdown.FreezeDPS, s_format("^8(Ailment Threshold is about equal to Life except on bosses where it is about half of their life)"))
|
|
end
|
|
end
|
|
end
|
|
if (output.ScorchChanceOnHit + output.ScorchChanceOnCrit) > 0 then
|
|
local sourceHitDmg = 0
|
|
local sourceCritDmg = 0
|
|
if output.ScorchChanceOnCrit == 0 and output.ScorchChanceOnHit > 0 then
|
|
output.ScorchChanceOnCrit = output.ScorchChanceOnHit
|
|
end
|
|
if canDeal.Fire then
|
|
sourceHitDmg = sourceHitDmg + output.FireHitAverage
|
|
sourceCritDmg = sourceCritDmg + output.FireCritAverage
|
|
end
|
|
local igniteMode = env.configInput.igniteMode or "AVERAGE"
|
|
if igniteMode == "CRIT" then
|
|
output.ScorchChanceOnHit = 0
|
|
end
|
|
local baseVal = calcAilmentDamage("Scorch", sourceHitDmg, sourceCritDmg)
|
|
if baseVal > 0 then
|
|
skillFlags.scorch = true
|
|
output.ScorchEffectMod = skillModList:Sum("INC", cfg, "EnemyScorchEffect")
|
|
output.ScorchDurationMod = 1 + skillModList:Sum("INC", cfg, "EnemyScorchDuration") / 100 + enemyDB:Sum("INC", nil, "SelfScorchDuration") / 100
|
|
end
|
|
end
|
|
if (output.BrittleChanceOnHit + output.BrittleChanceOnCrit) > 0 then
|
|
local sourceHitDmg = 0
|
|
local sourceCritDmg = 0
|
|
if output.BrittleChanceOnCrit == 0 and output.BrittleChanceOnHit > 0 then
|
|
output.BrittleChanceOnCrit = output.BrittleChanceOnHit
|
|
end
|
|
if canDeal.Cold then
|
|
sourceHitDmg = sourceHitDmg + output.ColdHitAverage
|
|
sourceCritDmg = sourceCritDmg + output.ColdCritAverage
|
|
end
|
|
local igniteMode = env.configInput.igniteMode or "AVERAGE"
|
|
if igniteMode == "CRIT" then
|
|
output.BrittleChanceOnHit = 0
|
|
end
|
|
local baseVal = calcAilmentDamage("Brittle", sourceHitDmg, sourceCritDmg)
|
|
if baseVal > 0 then
|
|
skillFlags.brittle = true
|
|
output.BrittleEffectMod = skillModList:Sum("INC", cfg, "EnemyBrittleEffect")
|
|
output.BrittleDurationMod = 1 + skillModList:Sum("INC", cfg, "EnemyBrittleDuration") / 100 + enemyDB:Sum("INC", nil, "SelfBrittleDuration") / 100
|
|
end
|
|
end
|
|
if (output.SapChanceOnHit + output.SapChanceOnCrit) > 0 then
|
|
local sourceHitDmg = 0
|
|
local sourceCritDmg = 0
|
|
if output.SapChanceOnCrit == 0 and output.SapChanceOnHit > 0 then
|
|
output.SapChanceOnCrit = output.SapChanceOnHit
|
|
end
|
|
if canDeal.Lightning then
|
|
sourceHitDmg = sourceHitDmg + output.LightningHitAverage
|
|
sourceCritDmg = sourceCritDmg + output.LightningCritAverage
|
|
end
|
|
local igniteMode = env.configInput.igniteMode or "AVERAGE"
|
|
if igniteMode == "CRIT" then
|
|
output.SapChanceOnHit = 0
|
|
end
|
|
local baseVal = calcAilmentDamage("Sap", sourceHitDmg, sourceCritDmg)
|
|
if baseVal > 0 then
|
|
skillFlags.sap = true
|
|
output.SapEffectMod = skillModList:Sum("INC", cfg, "EnemySapEffect")
|
|
output.SapDurationMod = 1 + skillModList:Sum("INC", cfg, "EnemySapDuration") / 100 + enemyDB:Sum("INC", nil, "SelfSapDuration") / 100
|
|
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 = skillModList:Sum("BASE", cfg, "ImpaleStacksMax") -- magic number: base stacks duration
|
|
local configStacks = enemyDB:Sum("BASE", cfg, "Multiplier:ImpaleStacks")
|
|
local impaleStacks = m_min(maxStacks, configStacks)
|
|
|
|
local baseStoredDamage = data.misc.ImpaleStoredDamageBase
|
|
local storedExpectedDamageIncOnBleed = skillModList:Sum("INC", cfg, "ImpaleEffectOnBleed")*skillModList:Sum("BASE", cfg, "BleedChance")/100
|
|
local storedExpectedDamageInc = (skillModList:Sum("INC", cfg, "ImpaleEffect") + storedExpectedDamageIncOnBleed)/100
|
|
local storedExpectedDamageMore = round(skillModList:More(cfg, "ImpaleEffect"), 2)
|
|
local storedExpectedDamageModifier = (1 + storedExpectedDamageInc) * storedExpectedDamageMore
|
|
local impaleStoredDamage = baseStoredDamage * storedExpectedDamageModifier
|
|
local impaleHitDamageMod = impaleStoredDamage * impaleStacks -- Source: https://www.reddit.com/r/pathofexile/comments/chgqqt/impale_and_armor_interaction/
|
|
|
|
local enemyArmour = calcLib.val(enemyDB, "Armour")
|
|
local impaleArmourReduction = calcs.armourReductionF(enemyArmour, impaleHitDamageMod * output.impaleStoredHitAvg)
|
|
local impaleResist = m_max(0, enemyDB:Sum("BASE", nil, "PhysicalDamageReduction") + skillModList:Sum("BASE", cfg, "EnemyImpalePhysicalDamageReduction") + impaleArmourReduction)
|
|
|
|
local impaleDMGModifier = impaleHitDamageMod * (1 - impaleResist / 100) * impaleChance
|
|
|
|
globalOutput.ImpaleStacksMax = maxStacks
|
|
globalOutput.ImpaleStacks = impaleStacks
|
|
--ImpaleStoredDamage should be named ImpaleEffect or similar
|
|
--Using the variable name ImpaleEffect breaks the calculations sidebar (?!)
|
|
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)", storedExpectedDamageModifier))
|
|
t_insert(breakdown.ImpaleStoredDamage, s_format("= %.1f%%", output.ImpaleStoredDamage))
|
|
|
|
breakdown.ImpaleModifier = {}
|
|
t_insert(breakdown.ImpaleModifier, s_format("%d ^8(number 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("x %.2f ^8(impale enemy physical damage reduction)", (1 - impaleResist / 100)))
|
|
t_insert(breakdown.ImpaleModifier, s_format("= %.3f ^8(impale damage multiplier)", 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("TotalPoisonDPS", "DPS")
|
|
combineStat("PoisonDamage", "CHANCE", "PoisonChance")
|
|
if skillData.showAverage then
|
|
combineStat("TotalPoisonAverageDamage", "DPS")
|
|
else
|
|
combineStat("TotalPoisonStacks", "DPS")
|
|
end
|
|
combineStat("IgniteChance", "AVERAGE")
|
|
combineStat("IgniteDPS", "CHANCE", "IgniteChance")
|
|
if skillFlags.igniteCanStack then
|
|
combineStat("IgniteDamage", "CHANCE", "IgniteChance")
|
|
if skillData.showAverage then
|
|
combineStat("TotalIgniteAverageDamage", "DPS")
|
|
combineStat("TotalIgniteStacks", "DPS")
|
|
combineStat("TotalIgniteDPS", "DPS")
|
|
else
|
|
combineStat("TotalIgniteStacks", "DPS")
|
|
combineStat("TotalIgniteDPS", "DPS")
|
|
end
|
|
end
|
|
combineStat("ChillEffectMod", "AVERAGE")
|
|
combineStat("ChillDurationMod", "AVERAGE")
|
|
combineStat("ShockChance", "AVERAGE")
|
|
combineStat("ShockDurationMod", "AVERAGE")
|
|
combineStat("ShockEffectMod", "AVERAGE")
|
|
combineStat("FreezeChance", "AVERAGE")
|
|
combineStat("FreezeDurationMod", "AVERAGE")
|
|
combineStat("ScorchChance", "AVERAGE")
|
|
combineStat("ScorchEffectMod", "AVERAGE")
|
|
combineStat("ScorchDurationMod", "AVERAGE")
|
|
combineStat("BrittleChance", "AVERAGE")
|
|
combineStat("BrittleEffectMod", "AVERAGE")
|
|
combineStat("BrittleDurationMod", "AVERAGE")
|
|
combineStat("SapChance", "AVERAGE")
|
|
combineStat("SapEffectMod", "AVERAGE")
|
|
combineStat("SapDurationMod", "AVERAGE")
|
|
combineStat("BrittleChance", "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") * calcLib.mod(enemyDB, nil, "ChaosResist"), data.misc.EnemyMaxResist)
|
|
local takenInc = enemyDB:Sum("INC", nil, "DamageTaken", "DamageTakenOverTime", "ChaosDamageTaken", "ChaosDamageTakenOverTime")
|
|
local takenMore = enemyDB:More(nil, "DamageTaken", "DamageTakenOverTime", "ChaosDamageTaken", "ChaosDamageTakenOverTime")
|
|
effMult = (1 - resist / 100) * (1 + takenInc / 100) * takenMore
|
|
output["DecayEffMult"] = effMult
|
|
if breakdown and effMult ~= 1 then
|
|
breakdown.DecayEffMult = breakdown.effMult("Chaos", resist, 0, takenInc, effMult, takenMore, sourceRes)
|
|
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, "DotMultiplier", "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, 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 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)),
|
|
}
|
|
|
|
-- spell_damage_modifiers_apply_to_skill_dot does not apply to enemy damage taken
|
|
local dotTakenCfg = copyTable(dotCfg, true)
|
|
if (skillData.dotIsSpell) then
|
|
dotTakenCfg.flags = band(dotTakenCfg.flags, bnot(ModFlag.Spell))
|
|
end
|
|
|
|
activeSkill.dotCfg = dotCfg
|
|
output.TotalDotInstance = 0
|
|
|
|
runSkillFunc("preDotFunc")
|
|
|
|
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 or (output[damageType.."Dot"] or 0) > 0 then
|
|
skillFlags.dot = true
|
|
local effMult = 1
|
|
if env.mode_effective then
|
|
local resist = 0
|
|
local takenInc = enemyDB:Sum("INC", dotTakenCfg, "DamageTaken", "DamageTakenOverTime", damageType.."DamageTaken", damageType.."DamageTakenOverTime")
|
|
local takenMore = enemyDB:More(dotTakenCfg, "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
|
|
local base = resist + enemyDB:Sum("BASE", dotTypeCfg, "ElementalResist")
|
|
resist = base * calcLib.mod(enemyDB, nil, damageType.."Resist")
|
|
takenInc = takenInc + enemyDB:Sum("INC", dotTypeCfg, "ElementalDamageTaken")
|
|
end
|
|
resist = m_min(resist, data.misc.EnemyMaxResist)
|
|
end
|
|
effMult = (1 - resist / 100) * (1 + takenInc / 100) * takenMore
|
|
output[damageType.."DotEffMult"] = effMult
|
|
if breakdown and effMult ~= 1 then
|
|
breakdown[damageType.."DotEffMult"] = breakdown.effMult(damageType, resist, 0, takenInc, effMult, takenMore, sourceRes)
|
|
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, "DotMultiplier", damageType.."DotMultiplier")
|
|
local aura = activeSkill.skillTypes[SkillType.Aura] and calcLib.mod(skillModList, dotTypeCfg, "AuraEffect")
|
|
local total = baseVal * (1 + inc/100) * more * (1 + mult/100) * (aura or 1) * effMult
|
|
if output[damageType.."Dot"] == 0 then
|
|
output[damageType.."Dot"] = total
|
|
end
|
|
output.TotalDotInstance = output.TotalDotInstance + total + (output[damageType.."Dot"] or 0)
|
|
if breakdown then
|
|
breakdown[damageType.."Dot"] = { }
|
|
breakdown.dot(breakdown[damageType.."Dot"], baseVal, inc, more, mult, nil, aura, effMult, total)
|
|
end
|
|
end
|
|
end
|
|
if skillModList:Flag(nil, "DotCanStack") then
|
|
output.TotalDot = output.TotalDotInstance * output.Speed * output.Duration * (skillData.dpsMultiplier or 1)
|
|
if breakdown then
|
|
breakdown.TotalDot = {
|
|
s_format("%.1f ^8(Damage per Instance)", output.TotalDotInstance),
|
|
s_format("x %.2f ^8(hits per second)", output.Speed),
|
|
s_format("x %.2f ^8(skill duration)", output.Duration),
|
|
}
|
|
if skillData.dpsMultiplier then
|
|
t_insert(breakdown.TotalDot, s_format("x %g ^8(DPS multiplier for this skill)", skillData.dpsMultiplier))
|
|
end
|
|
t_insert(breakdown.TotalDot, s_format("= %.1f", output.TotalDot))
|
|
end
|
|
else
|
|
output.TotalDot = output.TotalDotInstance
|
|
end
|
|
|
|
-- Calculate combined DPS estimate, including DoTs
|
|
local baseDPS = output[(skillData.showAverage and "AverageDamage") or "TotalDPS"]
|
|
output.CombinedDPS = baseDPS
|
|
output.CombinedAvg = baseDPS
|
|
if skillFlags.dot then
|
|
output.CombinedDPS = output.CombinedDPS + (output.TotalDot or 0)
|
|
output.WithDotDPS = baseDPS + (output.TotalDot or 0)
|
|
end
|
|
if skillData.showAverage then
|
|
output.CombinedDPS = output.CombinedDPS + (output.TotalPoisonDPS or 0)
|
|
output.CombinedAvg = output.CombinedAvg + (output.PoisonDamage or 0)
|
|
output.WithPoisonDPS = 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.TotalIgniteDPS
|
|
output.CombinedAvg = output.CombinedDPS + output.IgniteDamage
|
|
else
|
|
output.CombinedDPS = output.CombinedDPS + output.TotalIgniteDPS
|
|
output.WithIgniteDPS = baseDPS + output.TotalIgniteDPS
|
|
end
|
|
elseif skillData.showAverage then
|
|
output.WithIgniteDPS = baseDPS + output.IgniteDamage
|
|
output.CombinedDPS = output.CombinedDPS + output.IgniteDPS
|
|
output.CombinedAvg = output.CombinedAvg + output.IgniteDamage
|
|
else
|
|
output.WithIgniteDPS = baseDPS + output.IgniteDPS
|
|
output.CombinedDPS = output.CombinedDPS + output.IgniteDPS
|
|
end
|
|
else
|
|
output.WithIgniteDPS = baseDPS
|
|
end
|
|
if skillFlags.bleed then
|
|
if skillData.showAverage then
|
|
output.WithBleedDPS = baseDPS + output.BleedDamage
|
|
output.CombinedDPS = output.CombinedDPS + output.BleedDPS
|
|
output.CombinedAvg = output.CombinedAvg + output.BleedDamage
|
|
else
|
|
output.WithBleedDPS = baseDPS + output.BleedDPS
|
|
output.CombinedDPS = output.CombinedDPS + output.BleedDPS
|
|
end
|
|
else
|
|
output.WithBleedDPS = baseDPS
|
|
end
|
|
if skillFlags.decay then
|
|
output.CombinedDPS = output.CombinedDPS + output.DecayDPS
|
|
end
|
|
output.TotalDotDPS = (output.TotalDot or 0) + (output.TotalPoisonDPS or 0) + (output.TotalIgniteDPS or output.IgniteDPS or 0) + (output.BleedDPS or 0) + (output.DecayDPS or 0)
|
|
if skillFlags.impale then
|
|
output.ImpaleHit = ((output.MainHand.PhysicalHitAverage or output.OffHand.PhysicalHitAverage) + (output.OffHand.PhysicalHitAverage or output.MainHand.PhysicalHitAverage)) / 2 * (1-output.CritChance/100) + ((output.MainHand.PhysicalCritAverage or output.OffHand.PhysicalCritAverage) + (output.OffHand.PhysicalCritAverage or output.MainHand.PhysicalCritAverage)) / 2 * (output.CritChance/100)
|
|
if skillData.doubleHitsWhenDualWielding and skillFlags.bothWeaponAttack then
|
|
output.ImpaleHit = output.ImpaleHit * 2
|
|
end
|
|
output.ImpaleDPS = output.ImpaleHit * ((output.ImpaleModifier or 1) - 1) * output.HitChance / 100 * (skillData.dpsMultiplier or 1)
|
|
if skillData.showAverage then
|
|
output.WithImpaleDPS = output.AverageDamage + output.ImpaleDPS
|
|
else
|
|
skillFlags.notAverage = true
|
|
output.ImpaleDPS = output.ImpaleDPS * (output.HitSpeed or output.Speed)
|
|
output.WithImpaleDPS = output.TotalDPS + output.ImpaleDPS
|
|
end
|
|
output.CombinedDPS = output.CombinedDPS + output.ImpaleDPS
|
|
if breakdown then
|
|
breakdown.ImpaleDPS = {}
|
|
t_insert(breakdown.ImpaleDPS, s_format("%.2f ^8(average physical hit)", output.ImpaleHit))
|
|
t_insert(breakdown.ImpaleDPS, s_format("x %.2f ^8(chance to hit)", output.HitChance / 100))
|
|
if skillFlags.notAverage then
|
|
t_insert(breakdown.ImpaleDPS, output.HitSpeed and s_format("x %.2f ^8(hit rate)", output.HitSpeed) or s_format("x %.2f ^8(attack rate)", output.Speed))
|
|
end
|
|
t_insert(breakdown.ImpaleDPS, s_format("x %.2f ^8(impale damage multiplier)", ((output.ImpaleModifier or 1) - 1)))
|
|
if skillData.dpsMultiplier then
|
|
t_insert(breakdown.ImpaleDPS, s_format("x %g ^8(dps multiplier for this skill)", skillData.dpsMultiplier))
|
|
end
|
|
t_insert(breakdown.ImpaleDPS, s_format("= %.1f", output.ImpaleDPS))
|
|
end
|
|
end
|
|
end
|