- Added skill part to Barrage - Added crit overcap info to crit breakdown - Default gem level now varies according to the gem's max level - Fixed quality bug introduced in 1.3.5
3138 lines
116 KiB
Lua
3138 lines
116 KiB
Lua
-- Path of Building
|
|
--
|
|
-- Module: Calcs
|
|
-- Performs all the offense and defense calculations.
|
|
-- Here be dragons!
|
|
-- This file is 3100 lines long, over half of which is in one function...
|
|
--
|
|
|
|
local pairs = pairs
|
|
local ipairs = ipairs
|
|
local t_insert = table.insert
|
|
local t_remove = table.remove
|
|
local m_abs = math.abs
|
|
local m_ceil = math.ceil
|
|
local m_floor = math.floor
|
|
local m_min = math.min
|
|
local m_max = math.max
|
|
local s_format = string.format
|
|
local band = bit.band
|
|
local bor = bit.bor
|
|
local bnot = bit.bnot
|
|
|
|
-- List of all damage types, ordered according to the conversion sequence
|
|
local dmgTypeList = {"Physical", "Lightning", "Cold", "Fire", "Chaos"}
|
|
|
|
local resistTypeList = { "Fire", "Cold", "Lightning", "Chaos" }
|
|
|
|
local isElemental = { Fire = true, Cold = true, Lightning = true }
|
|
|
|
-- Calculate and combine INC/MORE modifiers for the given modifier names
|
|
local function calcMod(modDB, cfg, ...)
|
|
return (1 + (modDB:Sum("INC", cfg, ...)) / 100) * modDB:Sum("MORE", cfg, ...)
|
|
end
|
|
|
|
-- Calculate value, optionally adding additional base
|
|
local function calcVal(modDB, name, cfg, base)
|
|
local baseVal = modDB:Sum("BASE", cfg, name) + (base or 0)
|
|
if baseVal ~= 0 then
|
|
return baseVal * calcMod(modDB, cfg, name)
|
|
else
|
|
return 0
|
|
end
|
|
end
|
|
|
|
-- Calculate hit chance
|
|
local function calcHitChance(evasion, accuracy)
|
|
local rawChance = accuracy / (accuracy + (evasion / 4) ^ 0.8) * 100
|
|
return m_max(m_min(m_floor(rawChance + 0.5), 95), 5)
|
|
end
|
|
|
|
-- Merge gem modifiers with given mod list
|
|
local function mergeGemMods(modList, gem)
|
|
modList:AddList(gem.data.baseMods)
|
|
if gem.quality > 0 then
|
|
for i = 1, #gem.data.qualityMods do
|
|
local scaledMod = copyTable(gem.data.qualityMods[i])
|
|
scaledMod.value = m_floor(scaledMod.value * gem.quality)
|
|
modList:AddMod(scaledMod)
|
|
end
|
|
end
|
|
gem.level = m_max(gem.level, 1)
|
|
if not gem.data.levels[gem.level] then
|
|
gem.level = m_min(gem.level, #gem.data.levels)
|
|
end
|
|
local levelData = gem.data.levels[gem.level]
|
|
for col, mod in pairs(gem.data.levelMods) do
|
|
if levelData[col] then
|
|
local newMod = copyTable(mod)
|
|
if type(newMod.value) == "table" then
|
|
newMod.value.value = levelData[col]
|
|
else
|
|
newMod.value = levelData[col]
|
|
end
|
|
modList:AddMod(newMod)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Check if given support gem can support the given active skill
|
|
-- Global function, as GemSelectControl needs to use it too
|
|
function gemCanSupport(gem, activeSkill)
|
|
if gem.data.unsupported then
|
|
return false
|
|
end
|
|
for _, skillType in pairs(gem.data.excludeSkillTypes) do
|
|
if activeSkill.skillTypes[skillType] then
|
|
return false
|
|
end
|
|
end
|
|
if not gem.data.requireSkillTypes[1] then
|
|
return true
|
|
end
|
|
for _, skillType in pairs(gem.data.requireSkillTypes) do
|
|
if activeSkill.skillTypes[skillType] then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Check if given gem is of the given type ("all", "strength", "melee", etc)
|
|
-- Global function, as ModDBClass and ModListClass need to use it too
|
|
function gemIsType(gem, type)
|
|
return type == "all" or (type == "elemental" and (gem.data.fire or gem.data.cold or gem.data.lightning)) or gem.data[type]
|
|
end
|
|
|
|
-- Create an active skill using the given active gem and list of support gems
|
|
-- It will determine the base flag set, and check which of the support gems can support this skill
|
|
local function createActiveSkill(activeGem, supportList)
|
|
local activeSkill = { }
|
|
activeSkill.activeGem = {
|
|
name = activeGem.name,
|
|
data = activeGem.data,
|
|
level = activeGem.level,
|
|
quality = activeGem.quality,
|
|
fromItem = activeGem.fromItem,
|
|
srcGem = activeGem,
|
|
}
|
|
activeSkill.gemList = { activeSkill.activeGem }
|
|
|
|
activeSkill.skillTypes = copyTable(activeGem.data.skillTypes)
|
|
|
|
-- Initialise skill flag set ('attack', 'projectile', etc)
|
|
local skillFlags = copyTable(activeGem.data.baseFlags)
|
|
activeSkill.skillFlags = skillFlags
|
|
skillFlags.hit = activeSkill.skillTypes[SkillType.Attack] or activeSkill.skillTypes[SkillType.Hit]
|
|
|
|
for _, gem in ipairs(supportList) do
|
|
if gemCanSupport(gem, activeSkill) then
|
|
if gem.data.addFlags then
|
|
-- Support gem adds flags to supported skills (eg. Remote Mine adds 'mine')
|
|
for k in pairs(gem.data.addFlags) do
|
|
skillFlags[k] = true
|
|
end
|
|
end
|
|
for _, skillType in pairs(gem.data.addSkillTypes) do
|
|
activeSkill.skillTypes[skillType] = true
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Process support gems
|
|
for _, gem in ipairs(supportList) do
|
|
if gemCanSupport(gem, activeSkill) then
|
|
t_insert(activeSkill.gemList, {
|
|
name = gem.name,
|
|
data = gem.data,
|
|
level = gem.level,
|
|
quality = gem.quality,
|
|
fromItem = gem.fromItem,
|
|
srcGem = gem,
|
|
})
|
|
if gem.isSupporting then
|
|
gem.isSupporting[activeGem.name] = true
|
|
end
|
|
end
|
|
end
|
|
|
|
return activeSkill
|
|
end
|
|
|
|
local function getWeaponFlags(weaponData, weaponTypes)
|
|
local info = data.weaponTypeInfo[weaponData.type]
|
|
if not info then
|
|
return
|
|
end
|
|
if weaponTypes and not weaponTypes[weaponData.type] and
|
|
(not weaponData.countsAsAll1H or not (weaponTypes["Claw"] or weaponTypes["Dagger"] or weaponTypes["One Handed Axe"] or weaponTypes["One Handed Mace"] or weaponTypes["One Handed Sword"])) then
|
|
return
|
|
end
|
|
local flags = info.flag
|
|
if weaponData.countsAsAll1H then
|
|
flags = bor(ModFlag.Axe, ModFlag.Claw, ModFlag.Dagger, ModFlag.Mace, ModFlag.Sword)
|
|
end
|
|
if weaponData.type ~= "None" then
|
|
flags = bor(flags, ModFlag.Weapon)
|
|
if info.oneHand then
|
|
flags = bor(flags, ModFlag.Weapon1H)
|
|
else
|
|
flags = bor(flags, ModFlag.Weapon2H)
|
|
end
|
|
if info.melee then
|
|
flags = bor(flags, ModFlag.WeaponMelee)
|
|
else
|
|
flags = bor(flags, ModFlag.WeaponRanged)
|
|
end
|
|
end
|
|
return flags, info
|
|
end
|
|
|
|
-- Build list of modifiers for given active skill
|
|
local function buildActiveSkillModList(env, activeSkill)
|
|
local skillTypes = activeSkill.skillTypes
|
|
local skillFlags = activeSkill.skillFlags
|
|
|
|
-- Handle multipart skills
|
|
local activeGemParts = activeSkill.activeGem.data.parts
|
|
if activeGemParts then
|
|
if activeSkill == env.mainSkill then
|
|
activeSkill.skillPart = m_min(#activeGemParts, env.skillPart or activeSkill.activeGem.srcGem.skillPart or 1)
|
|
else
|
|
activeSkill.skillPart = m_min(#activeGemParts, activeSkill.activeGem.srcGem.skillPart or 1)
|
|
end
|
|
local part = activeGemParts[activeSkill.skillPart]
|
|
for k, v in pairs(part) do
|
|
if v == true then
|
|
skillFlags[k] = true
|
|
elseif v == false then
|
|
skillFlags[k] = nil
|
|
end
|
|
end
|
|
activeSkill.skillPartName = part.name
|
|
skillFlags.multiPart = #activeGemParts > 1
|
|
end
|
|
|
|
if skillTypes[SkillType.Shield] and (not env.itemList["Weapon 2"] or env.itemList["Weapon 2"].type ~= "Shield") then
|
|
-- Skill requires a shield to be equipped
|
|
skillFlags.disable = true
|
|
end
|
|
|
|
if skillFlags.attack then
|
|
-- Set weapon flags
|
|
local weaponTypes = activeSkill.activeGem.data.weaponTypes
|
|
local weapon1Flags, weapon1Info = getWeaponFlags(env.weaponData1, weaponTypes)
|
|
if weapon1Flags then
|
|
activeSkill.weapon1Flags = weapon1Flags
|
|
skillFlags.weapon1Attack = true
|
|
if weapon1Info.melee and skillFlags.melee then
|
|
skillFlags.projectile = nil
|
|
elseif not weapon1Info.melee and skillFlags.projectile then
|
|
skillFlags.melee = nil
|
|
end
|
|
elseif skillTypes[SkillType.DualWield] or not skillTypes[SkillType.CanDualWield] or skillTypes[SkillType.MainHandOnly] then
|
|
-- Skill requires a compatible main hand weapon
|
|
skillFlags.disable = true
|
|
end
|
|
if skillTypes[SkillType.DualWield] or skillTypes[SkillType.CanDualWield] then
|
|
if not skillTypes[SkillType.MainHandOnly] then
|
|
local weapon2Flags = getWeaponFlags(env.weaponData2, weaponTypes)
|
|
if weapon2Flags then
|
|
activeSkill.weapon2Flags = weapon2Flags
|
|
skillFlags.weapon2Attack = true
|
|
elseif skillTypes[SkillType.DualWield] or not skillFlags.weapon1Attack then
|
|
-- Skill requires a compatible off hand weapon
|
|
skillFlags.disable = true
|
|
end
|
|
end
|
|
elseif env.weaponData2.type then
|
|
-- Skill cannot be used while dual wielding
|
|
skillFlags.disable = true
|
|
end
|
|
skillFlags.bothWeaponAttack = skillFlags.weapon1Attack and skillFlags.weapon2Attack
|
|
end
|
|
|
|
-- Build skill mod flag set
|
|
local skillModFlags = 0
|
|
if skillFlags.hit then
|
|
skillModFlags = bor(skillModFlags, ModFlag.Hit)
|
|
end
|
|
if skillFlags.attack then
|
|
skillModFlags = bor(skillModFlags, ModFlag.Attack)
|
|
else
|
|
skillModFlags = bor(skillModFlags, ModFlag.Cast)
|
|
if skillFlags.spell then
|
|
skillModFlags = bor(skillModFlags, ModFlag.Spell)
|
|
end
|
|
end
|
|
if skillFlags.melee then
|
|
skillModFlags = bor(skillModFlags, ModFlag.Melee)
|
|
elseif skillFlags.projectile then
|
|
skillModFlags = bor(skillModFlags, ModFlag.Projectile)
|
|
end
|
|
if skillFlags.area then
|
|
skillModFlags = bor(skillModFlags, ModFlag.Area)
|
|
end
|
|
|
|
-- Build skill keyword flag set
|
|
local skillKeywordFlags = 0
|
|
if skillFlags.aura then
|
|
skillKeywordFlags = bor(skillKeywordFlags, KeywordFlag.Aura)
|
|
end
|
|
if skillFlags.curse then
|
|
skillKeywordFlags = bor(skillKeywordFlags, KeywordFlag.Curse)
|
|
end
|
|
if skillFlags.warcry then
|
|
skillKeywordFlags = bor(skillKeywordFlags, KeywordFlag.Warcry)
|
|
end
|
|
if skillFlags.movement then
|
|
skillKeywordFlags = bor(skillKeywordFlags, KeywordFlag.Movement)
|
|
end
|
|
if skillFlags.vaal then
|
|
skillKeywordFlags = bor(skillKeywordFlags, KeywordFlag.Vaal)
|
|
end
|
|
if skillFlags.lightning then
|
|
skillKeywordFlags = bor(skillKeywordFlags, KeywordFlag.Lightning)
|
|
end
|
|
if skillFlags.cold then
|
|
skillKeywordFlags = bor(skillKeywordFlags, KeywordFlag.Cold)
|
|
end
|
|
if skillFlags.fire then
|
|
skillKeywordFlags = bor(skillKeywordFlags, KeywordFlag.Fire)
|
|
end
|
|
if skillFlags.chaos then
|
|
skillKeywordFlags = bor(skillKeywordFlags, KeywordFlag.Chaos)
|
|
end
|
|
if skillFlags.minion then
|
|
skillKeywordFlags = bor(skillKeywordFlags, KeywordFlag.Minion)
|
|
elseif skillFlags.totem then
|
|
skillKeywordFlags = bor(skillKeywordFlags, KeywordFlag.Totem)
|
|
elseif skillFlags.trap then
|
|
skillKeywordFlags = bor(skillKeywordFlags, KeywordFlag.Trap)
|
|
elseif skillFlags.mine then
|
|
skillKeywordFlags = bor(skillKeywordFlags, KeywordFlag.Mine)
|
|
end
|
|
|
|
-- Get skill totem ID for totem skills
|
|
-- This is used to calculate totem life
|
|
if skillFlags.totem then
|
|
activeSkill.skillTotemId = activeSkill.activeGem.data.skillTotemId
|
|
if not activeSkill.skillTotemId then
|
|
if activeSkill.activeGem.data.color == 2 then
|
|
activeSkill.skillTotemId = 2
|
|
elseif activeSkill.activeGem.data.color == 3 then
|
|
activeSkill.skillTotemId = 3
|
|
else
|
|
activeSkill.skillTotemId = 1
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Build config structure for modifier searches
|
|
activeSkill.skillCfg = {
|
|
flags = bor(skillModFlags, activeSkill.weapon1Flags or activeSkill.weapon2Flags or 0),
|
|
keywordFlags = skillKeywordFlags,
|
|
skillName = activeSkill.activeGem.name:gsub("^Vaal ",""), -- This allows modifiers that target specific skills to also apply to their Vaal counterpart
|
|
skillGem = activeSkill.activeGem,
|
|
skillPart = activeSkill.skillPart,
|
|
skillTypes = activeSkill.skillTypes,
|
|
skillCond = { },
|
|
slotName = activeSkill.slotName,
|
|
}
|
|
if skillFlags.weapon1Attack then
|
|
activeSkill.weapon1Cfg = copyTable(activeSkill.skillCfg, true)
|
|
activeSkill.weapon1Cfg.skillCond = { ["MainHandAttack"] = true }
|
|
activeSkill.weapon1Cfg.flags = bor(skillModFlags, activeSkill.weapon1Flags)
|
|
end
|
|
if skillFlags.weapon2Attack then
|
|
activeSkill.weapon2Cfg = copyTable(activeSkill.skillCfg, true)
|
|
activeSkill.weapon2Cfg.skillCond = { ["OffHandAttack"] = true }
|
|
activeSkill.weapon2Cfg.flags = bor(skillModFlags, activeSkill.weapon2Flags)
|
|
end
|
|
|
|
-- Apply gem property modifiers from the item this skill is socketed into
|
|
for _, value in ipairs(env.modDB:Sum("LIST", activeSkill.skillCfg, "GemProperty")) do
|
|
for _, gem in pairs(activeSkill.gemList) do
|
|
if not gem.fromItem and gemIsType(gem, value.keyword) then
|
|
gem[value.key] = (gem[value.key] or 0) + value.value
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Initialise skill modifier list
|
|
local skillModList = common.New("ModList")
|
|
activeSkill.skillModList = skillModList
|
|
|
|
if skillFlags.disable then
|
|
wipeTable(skillFlags)
|
|
skillFlags.disable = true
|
|
return
|
|
end
|
|
|
|
-- Add support gem modifiers to skill mod list
|
|
for _, gem in pairs(activeSkill.gemList) do
|
|
if gem.data.support then
|
|
mergeGemMods(skillModList, gem)
|
|
end
|
|
end
|
|
|
|
-- Apply gem/quality modifiers from support gems
|
|
if not activeSkill.activeGem.fromItem then
|
|
for _, value in ipairs(skillModList:Sum("LIST", activeSkill.skillCfg, "GemProperty")) do
|
|
if value.keyword == "active_skill" then
|
|
activeSkill.activeGem[value.key] = activeSkill.activeGem[value.key] + value.value
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Add active gem modifiers
|
|
mergeGemMods(skillModList, activeSkill.activeGem)
|
|
|
|
-- Extract skill data
|
|
activeSkill.skillData = { }
|
|
for _, value in ipairs(skillModList:Sum("LIST", activeSkill.skillCfg, "Misc")) do
|
|
if value.type == "SkillData" then
|
|
activeSkill.skillData[value.key] = value.value
|
|
end
|
|
end
|
|
|
|
-- Separate global effect modifiers (mods that can affect defensive stats or other skills)
|
|
local i = 1
|
|
while skillModList[i] do
|
|
local destList
|
|
for _, tag in ipairs(skillModList[i].tagList) do
|
|
if tag.type == "GlobalEffect" then
|
|
if tag.effectType == "Buff" then
|
|
destList = "buffModList"
|
|
elseif tag.effectType == "Aura" then
|
|
destList = "auraModList"
|
|
elseif tag.effectType == "Debuff" then
|
|
destList = "debuffModList"
|
|
elseif tag.effectType == "Curse" then
|
|
destList = "curseModList"
|
|
end
|
|
break
|
|
end
|
|
end
|
|
if destList then
|
|
if not activeSkill[destList] then
|
|
activeSkill[destList] = { }
|
|
end
|
|
t_insert(activeSkill[destList], skillModList[i])
|
|
t_remove(skillModList, i)
|
|
else
|
|
i = i + 1
|
|
end
|
|
end
|
|
|
|
if activeSkill.buffModList or activeSkill.auraModList or activeSkill.debuffModList or activeSkill.curseModList then
|
|
-- Add to auxillary skill list
|
|
t_insert(env.auxSkillList, activeSkill)
|
|
end
|
|
end
|
|
|
|
-- Build list of modifiers from the listed tree nodes
|
|
local function buildNodeModList(env, nodeList, finishJewels)
|
|
-- Initialise radius jewels
|
|
for _, rad in pairs(env.radiusJewelList) do
|
|
wipeTable(rad.data)
|
|
end
|
|
|
|
-- Add node modifers
|
|
local modList = common.New("ModList")
|
|
for _, node in pairs(nodeList) do
|
|
-- Merge with output list
|
|
if node.type == "keystone" then
|
|
modList:AddMod(node.keystoneMod)
|
|
else
|
|
modList:AddList(node.modList)
|
|
end
|
|
|
|
-- Run radius jewels
|
|
for _, rad in pairs(env.radiusJewelList) do
|
|
if rad.nodes[node.id] then
|
|
rad.func(node.modList, modList, rad.data)
|
|
end
|
|
end
|
|
end
|
|
|
|
if finishJewels then
|
|
-- Finalise radius jewels
|
|
for _, rad in pairs(env.radiusJewelList) do
|
|
rad.func(nil, modList, rad.data)
|
|
if env.mode == "MAIN" then
|
|
if not rad.item.jewelRadiusData then
|
|
rad.item.jewelRadiusData = { }
|
|
end
|
|
rad.item.jewelRadiusData[rad.nodeId] = rad.data
|
|
end
|
|
end
|
|
end
|
|
|
|
return modList
|
|
end
|
|
|
|
-- Calculate min/max damage of a hit for the given damage type
|
|
local function calcHitDamage(env, source, cfg, breakdown, damageType, ...)
|
|
local modDB = env.modDB
|
|
|
|
local damageTypeMin = damageType.."Min"
|
|
local damageTypeMax = damageType.."Max"
|
|
|
|
-- Calculate base values
|
|
local damageEffectiveness = source.damageEffectiveness or 1
|
|
local addedMin = modDB:Sum("BASE", cfg, damageTypeMin)
|
|
local addedMax = modDB:Sum("BASE", cfg, damageTypeMax)
|
|
local baseMin = (source[damageTypeMin] or 0) + addedMin * damageEffectiveness
|
|
local baseMax = (source[damageTypeMax] or 0) + addedMax * damageEffectiveness
|
|
|
|
if breakdown and not (...) and baseMin ~= 0 and baseMax ~= 0 then
|
|
t_insert(breakdown, "Base damage:")
|
|
local plus = ""
|
|
if (source[damageTypeMin] or 0) ~= 0 or (source[damageTypeMax] or 0) ~= 0 then
|
|
t_insert(breakdown, s_format("%d to %d ^8(base damage from %s)", source[damageTypeMin], source[damageTypeMax], env.mode_skillType == "ATTACK" and "weapon" or "skill"))
|
|
plus = "+ "
|
|
end
|
|
if addedMin ~= 0 or addedMax ~= 0 then
|
|
if damageEffectiveness ~= 1 then
|
|
t_insert(breakdown, s_format("%s(%d to %d) x %.2f ^8(added damage multiplied by damage effectiveness)", plus, addedMin, addedMax, damageEffectiveness))
|
|
else
|
|
t_insert(breakdown, s_format("%s%d to %d ^8(added damage)", plus, addedMin, addedMax))
|
|
end
|
|
end
|
|
t_insert(breakdown, s_format("= %.1f to %.1f", baseMin, baseMax))
|
|
end
|
|
|
|
-- Calculate conversions
|
|
local addMin, addMax = 0, 0
|
|
local conversionTable = env.conversionTable
|
|
for _, otherType in ipairs(dmgTypeList) do
|
|
if otherType == damageType then
|
|
-- Damage can only be converted from damage types that preceed this one in the conversion sequence, so stop here
|
|
break
|
|
end
|
|
local convMult = conversionTable[otherType][damageType]
|
|
if convMult > 0 then
|
|
-- Damage is being converted/gained from the other damage type
|
|
local min, max = calcHitDamage(env, source, cfg, breakdown, otherType, damageType, ...)
|
|
addMin = addMin + min * convMult
|
|
addMax = addMax + max * convMult
|
|
end
|
|
end
|
|
if addMin ~= 0 and addMax ~= 0 then
|
|
addMin = round(addMin)
|
|
addMax = round(addMax)
|
|
end
|
|
|
|
if baseMin == 0 and baseMax == 0 then
|
|
-- No base damage for this type, don't need to calculate modifiers
|
|
if breakdown and (addMin ~= 0 or addMax ~= 0) then
|
|
t_insert(breakdown.damageComponents, {
|
|
source = damageType,
|
|
convSrc = (addMin ~= 0 or addMax ~= 0) and (addMin .. " to " .. addMax),
|
|
total = addMin .. " to " .. addMax,
|
|
convDst = (...) and s_format("%d%% to %s", conversionTable[damageType][...] * 100, ...),
|
|
})
|
|
end
|
|
return addMin, addMax
|
|
end
|
|
|
|
-- Build lists of applicable modifier names
|
|
local addElemental = isElemental[damageType]
|
|
local modNames = { damageType.."Damage", "Damage" }
|
|
for i = 1, select('#', ...) do
|
|
local dstElem = select(i, ...)
|
|
-- Add modifiers for damage types to which this damage is being converted
|
|
addElemental = addElemental or isElemental[dstElem]
|
|
t_insert(modNames, dstElem.."Damage")
|
|
end
|
|
if addElemental then
|
|
-- Damage is elemental or is being converted to elemental damage, add global elemental modifiers
|
|
t_insert(modNames, "ElementalDamage")
|
|
end
|
|
|
|
-- Combine modifiers
|
|
local inc = 1 + modDB:Sum("INC", cfg, unpack(modNames)) / 100
|
|
local more = m_floor(modDB:Sum("MORE", cfg, unpack(modNames)) * 100 + 0.50000001) / 100
|
|
|
|
if breakdown then
|
|
t_insert(breakdown.damageComponents, {
|
|
source = damageType,
|
|
base = baseMin .. " to " .. baseMax,
|
|
inc = (inc ~= 1 and "x "..inc),
|
|
more = (more ~= 1 and "x "..more),
|
|
convSrc = (addMin ~= 0 or addMax ~= 0) and (addMin .. " to " .. addMax),
|
|
total = (round(baseMin * inc * more) + addMin) .. " to " .. (round(baseMax * inc * more) + addMax),
|
|
convDst = (...) and s_format("%d%% to %s", conversionTable[damageType][...] * 100, ...),
|
|
})
|
|
end
|
|
|
|
return (round(baseMin * inc * more) + addMin),
|
|
(round(baseMax * inc * more) + addMax)
|
|
end
|
|
|
|
--
|
|
-- The following functions perform various steps in the calculations process.
|
|
-- Depending on what is being done with the output, other code may run inbetween steps, however the steps must always be performed in order:
|
|
-- 1. Initialise environment (initEnv)
|
|
-- 2. Run calculations (performCalcs)
|
|
--
|
|
-- Thus a basic calculation pass would look like this:
|
|
--
|
|
-- local env = initEnv(build, mode)
|
|
-- performCalcs(env)
|
|
--
|
|
|
|
local tempTable1 = { }
|
|
local tempTable2 = { }
|
|
local tempTable3 = { }
|
|
|
|
-- Initialise environment:
|
|
-- 1. Initialises the modifier databases
|
|
-- 2. Merges modifiers for all items
|
|
-- 3. Builds a list of jewels with radius functions
|
|
-- 4. Merges modifiers for all allocated passive nodes
|
|
-- 5. Builds a list of active skills and their supports
|
|
-- 6. Builds modifier lists for all active skills
|
|
local function initEnv(build, mode, override)
|
|
override = override or { }
|
|
|
|
local env = { }
|
|
env.build = build
|
|
env.configInput = build.configTab.input
|
|
env.calcsInput = build.calcsTab.input
|
|
env.mode = mode
|
|
env.spec = override.spec or build.spec
|
|
env.classId = env.spec.curClassId
|
|
|
|
-- Initialise modifier database with base values
|
|
local modDB = common.New("ModDB")
|
|
env.modDB = modDB
|
|
local classStats = build.tree.characterData[env.classId]
|
|
for _, stat in pairs({"Str","Dex","Int"}) do
|
|
modDB:NewMod(stat, "BASE", classStats["base_"..stat:lower()], "Base")
|
|
end
|
|
modDB.multipliers["Level"] = m_max(1, m_min(100, build.characterLevel))
|
|
modDB:NewMod("Life", "BASE", 12, "Base", { type = "Multiplier", var = "Level", base = 38 })
|
|
modDB:NewMod("Mana", "BASE", 6, "Base", { type = "Multiplier", var = "Level", base = 34 })
|
|
modDB:NewMod("ManaRegen", "BASE", 0.0175, "Base", { type = "PerStat", stat = "Mana", div = 1 })
|
|
modDB:NewMod("Evasion", "BASE", 3, "Base", { type = "Multiplier", var = "Level", base = 53 })
|
|
modDB:NewMod("Accuracy", "BASE", 2, "Base", { type = "Multiplier", var = "Level", base = -2 })
|
|
modDB:NewMod("FireResistMax", "BASE", 75, "Base")
|
|
modDB:NewMod("FireResist", "BASE", -60, "Base")
|
|
modDB:NewMod("ColdResistMax", "BASE", 75, "Base")
|
|
modDB:NewMod("ColdResist", "BASE", -60, "Base")
|
|
modDB:NewMod("LightningResistMax", "BASE", 75, "Base")
|
|
modDB:NewMod("LightningResist", "BASE", -60, "Base")
|
|
modDB:NewMod("ChaosResistMax", "BASE", 75, "Base")
|
|
modDB:NewMod("ChaosResist", "BASE", -60, "Base")
|
|
modDB:NewMod("BlockChanceMax", "BASE", 75, "Base")
|
|
modDB:NewMod("PowerChargesMax", "BASE", 3, "Base")
|
|
modDB:NewMod("CritChance", "INC", 50, "Base", { type = "Multiplier", var = "PowerCharge" })
|
|
modDB:NewMod("FrenzyChargesMax", "BASE", 3, "Base")
|
|
modDB:NewMod("Speed", "INC", 4, "Base", { type = "Multiplier", var = "FrenzyCharge" })
|
|
modDB:NewMod("Damage", "MORE", 4, "Base", { type = "Multiplier", var = "FrenzyCharge" })
|
|
modDB:NewMod("EnduranceChargesMax", "BASE", 3, "Base")
|
|
modDB:NewMod("ElementalResist", "BASE", 4, "Base", { type = "Multiplier", var = "EnduranceCharge" })
|
|
modDB:NewMod("ActiveTrapLimit", "BASE", 3, "Base")
|
|
modDB:NewMod("ActiveMineLimit", "BASE", 5, "Base")
|
|
modDB:NewMod("ActiveTotemLimit", "BASE", 1, "Base")
|
|
modDB:NewMod("ProjectileCount", "BASE", 1, "Base")
|
|
modDB:NewMod("Speed", "MORE", 10, "Base", ModFlag.Attack, { type = "Condition", var = "DualWielding" })
|
|
modDB:NewMod("PhysicalDamage", "MORE", 20, "Base", ModFlag.Attack, { type = "Condition", var = "DualWielding" })
|
|
modDB:NewMod("BlockChance", "BASE", 15, "Base", { type = "Condition", var = "DualWielding" })
|
|
modDB:NewMod("LifeRegenPercent", "BASE", 4, "Base", { type = "Condition", var = "OnConsecratedGround" })
|
|
modDB:NewMod("Misc", "LIST", { type = "EnemyModifier", mod = modLib.createMod("DamageTaken", "INC", 50, "Shock") }, "Base", { type = "Condition", var = "EnemyShocked" })
|
|
modDB:NewMod("Misc", "LIST", { type = "EnemyModifier", mod = modLib.createMod("HitChance", "MORE", -50, "Blind") }, "Base", { type = "Condition", var = "EnemyBlinded" })
|
|
|
|
-- Add bandit mods
|
|
if build.banditNormal == "Alira" then
|
|
modDB:NewMod("Mana", "BASE", 60, "Bandit")
|
|
elseif build.banditNormal == "Kraityn" then
|
|
modDB:NewMod("ElementalResist", "BASE", 10, "Bandit")
|
|
elseif build.banditNormal == "Oak" then
|
|
modDB:NewMod("Life", "BASE", 40, "Bandit")
|
|
else
|
|
modDB:NewMod("ExtraPoints", "BASE", 1, "Bandit")
|
|
end
|
|
if build.banditCruel == "Alira" then
|
|
modDB:NewMod("Speed", "INC", 5, "Bandit", ModFlag.Spell)
|
|
elseif build.banditCruel == "Kraityn" then
|
|
modDB:NewMod("Speed", "INC", 8, "Bandit", ModFlag.Attack)
|
|
elseif build.banditCruel == "Oak" then
|
|
modDB:NewMod("PhysicalDamage", "INC", 16, "Bandit")
|
|
else
|
|
modDB:NewMod("ExtraPoints", "BASE", 1, "Bandit")
|
|
end
|
|
if build.banditMerciless == "Alira" then
|
|
modDB:NewMod("PowerChargesMax", "BASE", 1, "Bandit")
|
|
elseif build.banditMerciless == "Kraityn" then
|
|
modDB:NewMod("FrenzyChargesMax", "BASE", 1, "Bandit")
|
|
elseif build.banditMerciless == "Oak" then
|
|
modDB:NewMod("EnduranceChargesMax", "BASE", 1, "Bandit")
|
|
else
|
|
modDB:NewMod("ExtraPoints", "BASE", 1, "Bandit")
|
|
end
|
|
|
|
-- Initialise enemy modifier database
|
|
local enemyDB = common.New("ModDB")
|
|
env.enemyDB = enemyDB
|
|
env.enemyLevel = m_max(1, m_min(100, env.configInput.enemyLevel and env.configInput.enemyLevel or m_min(env.build.characterLevel, 84)))
|
|
enemyDB:NewMod("Accuracy", "BASE", data.monsterAccuracyTable[env.enemyLevel], "Base")
|
|
enemyDB:NewMod("Evasion", "BASE", data.monsterEvasionTable[env.enemyLevel], "Base")
|
|
|
|
-- Add mods from the config tab
|
|
modDB:AddList(build.configTab.modList)
|
|
enemyDB:AddList(build.configTab.enemyModList)
|
|
|
|
-- Build list of passive nodes
|
|
local nodes
|
|
if override.addNodes or override.removeNodes then
|
|
nodes = { }
|
|
if override.addNodes then
|
|
for node in pairs(override.addNodes) do
|
|
nodes[node.id] = node
|
|
end
|
|
end
|
|
for _, node in pairs(env.spec.allocNodes) do
|
|
if not override.removeNodes or not override.removeNodes[node] then
|
|
nodes[node.id] = node
|
|
end
|
|
end
|
|
else
|
|
nodes = env.spec.allocNodes
|
|
end
|
|
|
|
-- Build and merge item modifiers, and create list of radius jewels
|
|
env.radiusJewelList = wipeTable(env.radiusJewelList)
|
|
env.itemList = { }
|
|
env.flasks = { }
|
|
env.modDB.conditions["UsingAllCorruptedItems"] = true
|
|
for slotName, slot in pairs(build.itemsTab.slots) do
|
|
local item
|
|
if slotName == override.repSlotName then
|
|
item = override.repItem
|
|
elseif slot.nodeId and override.spec then
|
|
item = build.itemsTab.list[env.spec.jewels[slot.nodeId]]
|
|
else
|
|
item = build.itemsTab.list[slot.selItemId]
|
|
end
|
|
if slot.nodeId then
|
|
-- Slot is a jewel socket, check if socket is allocated
|
|
if not nodes[slot.nodeId] then
|
|
item = nil
|
|
elseif item and item.jewelRadiusIndex then
|
|
-- Jewel has a radius, add it to the list
|
|
local funcList = item.jewelData.funcList or { function(nodeMods, out, data)
|
|
-- Default function just tallies all stats in radius
|
|
if nodeMods then
|
|
for _, stat in pairs({"Str","Dex","Int"}) do
|
|
data[stat] = (data[stat] or 0) + nodeMods:Sum("BASE", nil, stat)
|
|
end
|
|
end
|
|
end }
|
|
for _, func in ipairs(funcList) do
|
|
local node = build.spec.nodes[slot.nodeId]
|
|
t_insert(env.radiusJewelList, {
|
|
nodes = node.nodesInRadius[item.jewelRadiusIndex],
|
|
func = func,
|
|
item = item,
|
|
nodeId = slot.nodeId,
|
|
data = { }
|
|
})
|
|
end
|
|
end
|
|
end
|
|
if item and item.type == "Flask" then
|
|
if slot.active then
|
|
env.flasks[item] = true
|
|
end
|
|
item = nil
|
|
end
|
|
env.itemList[slotName] = item
|
|
if item then
|
|
-- Merge mods for this item
|
|
local srcList = item.modList or item.slotModList[slot.slotNum]
|
|
env.modDB:AddList(srcList)
|
|
if item.type ~= "Jewel" and item.type ~= "Flask" then
|
|
-- Update item counts
|
|
local key
|
|
if item.rarity == "UNIQUE" then
|
|
key = "UniqueItem"
|
|
elseif item.rarity == "RARE" then
|
|
key = "RareItem"
|
|
elseif item.rarity == "MAGIC" then
|
|
key = "MagicItem"
|
|
else
|
|
key = "NormalItem"
|
|
end
|
|
env.modDB.multipliers[key] = (env.modDB.multipliers[key] or 0) + 1
|
|
if item.corrupted then
|
|
env.modDB.multipliers.CorruptedItem = (env.modDB.multipliers.CorruptedItem or 0) + 1
|
|
else
|
|
env.modDB.conditions["UsingAllCorruptedItems"] = false
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if override.toggleFlask then
|
|
if env.flasks[override.toggleFlask] then
|
|
env.flasks[override.toggleFlask] = nil
|
|
else
|
|
env.flasks[override.toggleFlask] = true
|
|
end
|
|
end
|
|
|
|
if env.mode == "MAIN" then
|
|
-- Process extra skills granted by items
|
|
local markList = { }
|
|
for _, mod in ipairs(env.modDB.mods["ExtraSkill"] or { }) do
|
|
-- Extract the name of the slot containing the item this skill was granted by
|
|
local slotName
|
|
for _, tag in ipairs(mod.tagList) do
|
|
if tag.type == "SocketedIn" then
|
|
slotName = tag.slotName
|
|
break
|
|
end
|
|
end
|
|
|
|
-- Check if a matching group already exists
|
|
local group
|
|
for index, socketGroup in pairs(build.skillsTab.socketGroupList) do
|
|
if socketGroup.source == mod.source and socketGroup.slot == slotName then
|
|
if socketGroup.gemList[1] and socketGroup.gemList[1].nameSpec == mod.value.name then
|
|
group = socketGroup
|
|
markList[socketGroup] = true
|
|
break
|
|
end
|
|
end
|
|
end
|
|
if not group then
|
|
-- Create a new group for this skill
|
|
group = { label = "", enabled = true, gemList = { }, source = mod.source, slot = slotName }
|
|
t_insert(build.skillsTab.socketGroupList, group)
|
|
markList[group] = true
|
|
end
|
|
|
|
-- Update the group
|
|
group.sourceItem = build.itemsTab.list[tonumber(mod.source:match("Item:(%d+):"))]
|
|
wipeTable(group.gemList)
|
|
t_insert(group.gemList, {
|
|
nameSpec = mod.value.name,
|
|
level = mod.value.level,
|
|
quality = 0,
|
|
enabled = true,
|
|
fromItem = true,
|
|
})
|
|
if mod.value.noSupports then
|
|
group.noSupports = true
|
|
else
|
|
for _, socketGroup in pairs(build.skillsTab.socketGroupList) do
|
|
-- Look for other groups that are socketed in the item
|
|
if socketGroup.slot == slotName and not socketGroup.source then
|
|
-- Add all support gems to the skill's group
|
|
for _, gem in ipairs(socketGroup.gemList) do
|
|
if gem.data and gem.data.support then
|
|
t_insert(group.gemList, gem)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
build.skillsTab:ProcessSocketGroup(group)
|
|
end
|
|
|
|
-- Remove any socket groups that no longer have a matching item
|
|
local i = 1
|
|
while build.skillsTab.socketGroupList[i] do
|
|
local socketGroup = build.skillsTab.socketGroupList[i]
|
|
if socketGroup.source and not markList[socketGroup] then
|
|
t_remove(build.skillsTab.socketGroupList, i)
|
|
if build.skillsTab.displayGroup == socketGroup then
|
|
build.skillsTab.displayGroup = nil
|
|
end
|
|
else
|
|
i = i + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Get the weapon data tables for the equipped weapons
|
|
env.weaponData1 = env.itemList["Weapon 1"] and env.itemList["Weapon 1"].weaponData and env.itemList["Weapon 1"].weaponData[1] or copyTable(data.unarmedWeaponData[env.classId])
|
|
if env.weaponData1.countsAsDualWielding then
|
|
env.weaponData2 = env.itemList["Weapon 1"].weaponData[2]
|
|
else
|
|
env.weaponData2 = env.itemList["Weapon 2"] and env.itemList["Weapon 2"].weaponData and env.itemList["Weapon 2"].weaponData[2] or { }
|
|
end
|
|
|
|
-- Build and merge modifiers for allocated passives
|
|
env.modDB:AddList(buildNodeModList(env, nodes, true))
|
|
|
|
-- Determine main skill group
|
|
if env.mode == "CALCS" then
|
|
env.calcsInput.skill_number = m_min(m_max(#build.skillsTab.socketGroupList, 1), env.calcsInput.skill_number or 1)
|
|
env.mainSocketGroup = env.calcsInput.skill_number
|
|
env.skillPart = env.calcsInput.skill_part or 1
|
|
env.buffMode = env.calcsInput.misc_buffMode
|
|
else
|
|
build.mainSocketGroup = m_min(m_max(#build.skillsTab.socketGroupList, 1), build.mainSocketGroup or 1)
|
|
env.mainSocketGroup = build.mainSocketGroup
|
|
env.buffMode = "EFFECTIVE"
|
|
end
|
|
|
|
-- Build list of active skills
|
|
env.activeSkillList = { }
|
|
local groupCfg = wipeTable(tempTable1)
|
|
for index, socketGroup in pairs(build.skillsTab.socketGroupList) do
|
|
local socketGroupSkillList = { }
|
|
if socketGroup.enabled or index == env.mainSocketGroup then
|
|
-- Build list of supports for this socket group
|
|
local supportList = wipeTable(tempTable2)
|
|
if not socketGroup.source then
|
|
groupCfg.slotName = socketGroup.slot
|
|
for _, value in ipairs(env.modDB:Sum("LIST", groupCfg, "ExtraSupport")) do
|
|
-- Add extra supports from the item this group is socketed in
|
|
local gemData = data.gems[value.name]
|
|
if gemData then
|
|
t_insert(supportList, {
|
|
name = value.name,
|
|
data = gemData,
|
|
level = value.level,
|
|
quality = 0,
|
|
enabled = true,
|
|
fromItem = true
|
|
})
|
|
end
|
|
end
|
|
end
|
|
for _, gem in ipairs(socketGroup.gemList) do
|
|
if gem.enabled and gem.data and gem.data.support then
|
|
-- Add support gems from this group
|
|
local add = true
|
|
for _, otherGem in pairs(supportList) do
|
|
-- Check if there's another support with the same name already present
|
|
if gem.data == otherGem.data then
|
|
add = false
|
|
if gem.level > otherGem.level then
|
|
otherGem.level = gem.level
|
|
otherGem.quality = gem.quality
|
|
elseif gem.level == otherGem.level then
|
|
otherGem.quality = m_max(gem.quality, otherGem.quality)
|
|
end
|
|
break
|
|
end
|
|
end
|
|
if add then
|
|
gem.isSupporting = { }
|
|
t_insert(supportList, gem)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Create active skills
|
|
for _, gem in ipairs(socketGroup.gemList) do
|
|
if gem.enabled and gem.data and not gem.data.support and not gem.data.unsupported then
|
|
local activeSkill = createActiveSkill(gem, supportList)
|
|
activeSkill.slotName = socketGroup.slot
|
|
t_insert(socketGroupSkillList, activeSkill)
|
|
t_insert(env.activeSkillList, activeSkill)
|
|
end
|
|
end
|
|
|
|
if index == env.mainSocketGroup and #socketGroupSkillList > 0 then
|
|
-- Select the main skill from this socket group
|
|
local activeSkillIndex
|
|
if env.mode == "CALCS" then
|
|
env.calcsInput.skill_activeNumber = m_min(#socketGroupSkillList, env.calcsInput.skill_activeNumber or 1)
|
|
activeSkillIndex = env.calcsInput.skill_activeNumber
|
|
else
|
|
socketGroup.mainActiveSkill = m_min(#socketGroupSkillList, socketGroup.mainActiveSkill or 1)
|
|
activeSkillIndex = socketGroup.mainActiveSkill
|
|
end
|
|
env.mainSkill = socketGroupSkillList[activeSkillIndex]
|
|
end
|
|
end
|
|
|
|
if env.mode == "MAIN" then
|
|
-- Create display label for the socket group if the user didn't specify one
|
|
if socketGroup.label and socketGroup.label:match("%S") then
|
|
socketGroup.displayLabel = socketGroup.label
|
|
else
|
|
socketGroup.displayLabel = nil
|
|
for _, gem in ipairs(socketGroup.gemList) do
|
|
if gem.enabled and gem.data and not gem.data.support then
|
|
socketGroup.displayLabel = (socketGroup.displayLabel and socketGroup.displayLabel..", " or "") .. gem.name
|
|
end
|
|
end
|
|
socketGroup.displayLabel = socketGroup.displayLabel or "<No active skills>"
|
|
end
|
|
|
|
-- Save the active skill list for display in the socket group tooltip
|
|
socketGroup.displaySkillList = socketGroupSkillList
|
|
end
|
|
end
|
|
|
|
if not env.mainSkill then
|
|
-- Add a default main skill if none are specified
|
|
local defaultGem = {
|
|
name = "Default Attack",
|
|
level = 1,
|
|
quality = 0,
|
|
enabled = true,
|
|
data = data.gems._default
|
|
}
|
|
env.mainSkill = createActiveSkill(defaultGem, { })
|
|
t_insert(env.activeSkillList, env.mainSkill)
|
|
end
|
|
|
|
-- Build skill modifier lists
|
|
env.auxSkillList = { }
|
|
for _, activeSkill in pairs(env.activeSkillList) do
|
|
buildActiveSkillModList(env, activeSkill)
|
|
end
|
|
|
|
return env
|
|
end
|
|
|
|
-- Finalise environment and perform the calculations
|
|
-- This function is 1800 lines long. Enjoy!
|
|
local function performCalcs(env)
|
|
local modDB = env.modDB
|
|
local enemyDB = env.enemyDB
|
|
|
|
local output = { }
|
|
env.output = output
|
|
modDB.stats = output
|
|
local breakdown
|
|
if env.mode == "CALCS" then
|
|
breakdown = { }
|
|
env.breakdown = breakdown
|
|
end
|
|
|
|
-- Set modes
|
|
if env.buffMode == "EFFECTIVE" then
|
|
env.mode_buffs = true
|
|
env.mode_combat = true
|
|
env.mode_effective = true
|
|
elseif env.buffMode == "COMBAT" then
|
|
env.mode_buffs = true
|
|
env.mode_combat = true
|
|
env.mode_effective = false
|
|
elseif env.buffMode == "BUFFED" then
|
|
env.mode_buffs = true
|
|
env.mode_combat = false
|
|
env.mode_effective = false
|
|
else
|
|
env.mode_buffs = false
|
|
env.mode_combat = false
|
|
env.mode_effective = false
|
|
end
|
|
|
|
-- Merge keystone modifiers
|
|
do
|
|
local keystoneList = wipeTable(tempTable1)
|
|
for _, name in ipairs(modDB:Sum("LIST", nil, "Keystone")) do
|
|
keystoneList[name] = true
|
|
end
|
|
for name in pairs(keystoneList) do
|
|
modDB:AddList(env.build.tree.keystoneMap[name].modList)
|
|
end
|
|
end
|
|
|
|
-- Merge flask modifiers
|
|
if env.mode_combat then
|
|
local effectInc = modDB:Sum("INC", nil, "FlaskEffect")
|
|
for item in pairs(env.flasks) do
|
|
modDB.conditions["UsingFlask"] = true
|
|
modDB:ScaleAddList(item.modList, 1 + (effectInc + item.flaskData.effectInc) / 100)
|
|
end
|
|
end
|
|
|
|
-- Set conditions
|
|
local condList = modDB.conditions
|
|
if env.weaponData1.type == "Staff" then
|
|
condList["UsingStaff"] = true
|
|
end
|
|
if env.weaponData1.type == "Bow" then
|
|
condList["UsingBow"] = true
|
|
end
|
|
if env.itemList["Weapon 2"] and env.itemList["Weapon 2"].type == "Shield" then
|
|
condList["UsingShield"] = true
|
|
end
|
|
if env.weaponData1.type and env.weaponData2.type then
|
|
condList["DualWielding"] = true
|
|
end
|
|
if env.weaponData1.type == "None" then
|
|
condList["Unarmed"] = true
|
|
end
|
|
if (modDB.multipliers["NormalItem"] or 0) > 0 then
|
|
condList["UsingNormalItem"] = true
|
|
end
|
|
if (modDB.multipliers["MagicItem"] or 0) > 0 then
|
|
condList["UsingMagicItem"] = true
|
|
end
|
|
if (modDB.multipliers["RareItem"] or 0) > 0 then
|
|
condList["UsingRareItem"] = true
|
|
end
|
|
if (modDB.multipliers["UniqueItem"] or 0) > 0 then
|
|
condList["UsingUniqueItem"] = true
|
|
end
|
|
if (modDB.multipliers["CorruptedItem"] or 0) > 0 then
|
|
condList["UsingCorruptedItem"] = true
|
|
else
|
|
condList["NotUsingCorruptedItem"] = true
|
|
end
|
|
if env.mode_buffs then
|
|
condList["Buffed"] = true
|
|
end
|
|
if env.mode_combat then
|
|
condList["Combat"] = true
|
|
if not modDB:Sum("FLAG", nil, "NeverCrit") then
|
|
condList["CritInPast8Sec"] = true
|
|
end
|
|
if env.mainSkill.skillFlags.attack then
|
|
condList["AttackedRecently"] = true
|
|
elseif env.mainSkill.skillFlags.spell then
|
|
condList["CastSpellRecently"] = true
|
|
end
|
|
if not env.mainSkill.skillFlags.trap and not env.mainSkill.skillFlags.mine and not env.mainSkill.skillFlags.totem then
|
|
condList["HitRecently"] = true
|
|
end
|
|
if env.mainSkill.skillFlags.movement then
|
|
condList["UsedMovementSkillRecently"] = true
|
|
end
|
|
if env.mainSkill.skillFlags.totem then
|
|
condList["HaveTotem"] = true
|
|
condList["SummonedTotemRecently"] = true
|
|
end
|
|
if env.mainSkill.skillFlags.mine then
|
|
condList["DetonatedMinesRecently"] = true
|
|
end
|
|
end
|
|
if env.mode_effective then
|
|
condList["Effective"] = true
|
|
end
|
|
|
|
-- Check for extra curses
|
|
for _, value in ipairs(modDB:Sum("LIST", nil, "ExtraCurse")) do
|
|
local modList = common.New("ModList")
|
|
mergeGemMods(modList, {
|
|
level = value.level,
|
|
quality = 0,
|
|
data = data.gems[value.name],
|
|
})
|
|
for _, mod in ipairs(modList) do
|
|
for _, tag in ipairs(mod.tagList) do
|
|
if tag.type == "GlobalEffect" and tag.effectType == "Curse" then
|
|
enemyDB:AddMod(mod)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Check for extra modifiers to apply to aura skills
|
|
local extraAuraModList = { }
|
|
if modDB.mods.ExtraAuraEffect then
|
|
for _, mod in ipairs(modDB.mods.ExtraAuraEffect) do
|
|
mod.value.source = mod.source
|
|
t_insert(extraAuraModList, mod.value)
|
|
end
|
|
end
|
|
|
|
-- Merge auxillary skill modifiers and calculate skill life and mana reservations
|
|
env.reserved_LifeBase = 0
|
|
env.reserved_LifePercent = 0
|
|
env.reserved_ManaBase = 0
|
|
env.reserved_ManaPercent = 0
|
|
if breakdown then
|
|
breakdown.LifeReserved = { reservations = { } }
|
|
breakdown.ManaReserved = { reservations = { } }
|
|
end
|
|
for _, activeSkill in pairs(env.activeSkillList) do
|
|
local skillModList = activeSkill.skillModList
|
|
local skillCfg = activeSkill.skillCfg
|
|
|
|
-- Merge auxillary modifiers
|
|
if env.mode_buffs then
|
|
if activeSkill.buffModList and (not activeSkill.skillFlags.totem or activeSkill.skillData.allowTotemBuff) and (not activeSkill.skillData.offering or modDB:Sum("FLAG", nil, "OfferingsAffectPlayer")) then
|
|
activeSkill.buffSkill = true
|
|
local inc = modDB:Sum("INC", skillCfg, "BuffEffect")
|
|
if activeSkill.activeGem.data.golem and modDB:Sum("FLAG", skillCfg, "LiegeOfThePrimordial") and (activeSkill.activeGem.data.fire or activeSkill.activeGem.data.cold or activeSkill.activeGem.data.lightning) then
|
|
inc = inc + 100
|
|
end
|
|
modDB:ScaleAddList(activeSkill.buffModList, 1 + inc / 100)
|
|
end
|
|
if activeSkill.auraModList then
|
|
activeSkill.buffSkill = true
|
|
local inc = modDB:Sum("INC", skillCfg, "AuraEffect") + skillModList:Sum("INC", skillCfg, "AuraEffect") + modDB:Sum("INC", skillCfg, "BuffEffect")
|
|
local more = modDB:Sum("MORE", skillCfg, "AuraEffect") * skillModList:Sum("MORE", skillCfg, "AuraEffect")
|
|
modDB:ScaleAddList(activeSkill.auraModList, (1 + inc / 100) * more)
|
|
modDB:ScaleAddList(extraAuraModList, (1 + inc / 100) * more)
|
|
condList["HaveAuraActive"] = true
|
|
end
|
|
end
|
|
if env.mode_effective then
|
|
if activeSkill.debuffModList then
|
|
activeSkill.debuffSkill = true
|
|
enemyDB:ScaleAddList(activeSkill.debuffModList, activeSkill.skillData.stackCount or 1)
|
|
end
|
|
if activeSkill.curseModList then
|
|
activeSkill.debuffSkill = true
|
|
condList["EnemyCursed"] = true
|
|
modDB.multipliers["CurseOnEnemy"] = (modDB.multipliers["CurseOnEnemy"] or 0) + 1
|
|
local inc = modDB:Sum("INC", skillCfg, "CurseEffect") + enemyDB:Sum("INC", nil, "CurseEffect") + skillModList:Sum("INC", skillCfg, "CurseEffect")
|
|
local more = modDB:Sum("MORE", skillCfg, "CurseEffect") * enemyDB:Sum("MORE", nil, "CurseEffect") * skillModList:Sum("MORE", skillCfg, "CurseEffect")
|
|
enemyDB:ScaleAddList(activeSkill.curseModList, (1 + inc / 100) * more)
|
|
end
|
|
end
|
|
|
|
-- Calculate reservations
|
|
if activeSkill.skillTypes[SkillType.ManaCostReserved] and not activeSkill.skillFlags.totem then
|
|
local baseVal = activeSkill.skillData.manaCostOverride or activeSkill.skillData.manaCost
|
|
local suffix = activeSkill.skillTypes[SkillType.ManaCostPercent] and "Percent" or "Base"
|
|
local mult = skillModList:Sum("MORE", skillCfg, "ManaCost")
|
|
local more = modDB:Sum("MORE", skillCfg, "ManaReserved") * skillModList:Sum("MORE", skillCfg, "ManaReserved")
|
|
local inc = modDB:Sum("INC", skillCfg, "ManaReserved") + skillModList:Sum("INC", skillCfg, "ManaReserved")
|
|
local base = m_floor(baseVal * mult)
|
|
local cost = base - m_floor(base * -m_floor((100 + inc) * more - 100) / 100)
|
|
local pool
|
|
if modDB:Sum("FLAG", skillCfg, "BloodMagic", "SkillBloodMagic") or skillModList:Sum("FLAG", skillCfg, "SkillBloodMagic") then
|
|
pool = "Life"
|
|
else
|
|
pool = "Mana"
|
|
end
|
|
env["reserved_"..pool..suffix] = env["reserved_"..pool..suffix] + cost
|
|
if breakdown then
|
|
t_insert(breakdown[pool.."Reserved"].reservations, {
|
|
skillName = activeSkill.activeGem.name,
|
|
base = baseVal .. (activeSkill.skillTypes[SkillType.ManaCostPercent] and "%" or ""),
|
|
mult = mult ~= 1 and ("x "..mult),
|
|
more = more ~= 1 and ("x "..more),
|
|
inc = inc ~= 0 and ("x "..(1 + inc/100)),
|
|
total = cost .. (activeSkill.skillTypes[SkillType.ManaCostPercent] and "%" or ""),
|
|
})
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Process misc modifiers
|
|
for _, value in ipairs(modDB:Sum("LIST", nil, "Misc")) do
|
|
if value.type == "Condition" then
|
|
condList[value.var] = true
|
|
elseif value.type == "EnemyCondition" then
|
|
enemyDB.conditions[value.var] = true
|
|
elseif value.type == "Multiplier" then
|
|
modDB.multipliers[value.var] = (modDB.multipliers[value.var] or 0) + value.value
|
|
end
|
|
end
|
|
-- Process enemy modifiers last in case they depend on conditions that were set by misc modifiers
|
|
for _, value in ipairs(modDB:Sum("LIST", nil, "Misc")) do
|
|
if value.type == "EnemyModifier" then
|
|
enemyDB:AddMod(value.mod)
|
|
end
|
|
end
|
|
|
|
-- Process conditions that can depend on other conditions
|
|
if condList["EnemyIgnited"] then
|
|
condList["EnemyBurning"] = true
|
|
end
|
|
|
|
-- Calculate current and maximum charges
|
|
output.PowerChargesMax = modDB:Sum("BASE", nil, "PowerChargesMax")
|
|
output.FrenzyChargesMax = modDB:Sum("BASE", nil, "FrenzyChargesMax")
|
|
output.EnduranceChargesMax = modDB:Sum("BASE", nil, "EnduranceChargesMax")
|
|
if env.configInput.usePowerCharges and env.mode_combat then
|
|
output.PowerCharges = output.PowerChargesMax
|
|
else
|
|
output.PowerCharges = 0
|
|
end
|
|
if env.configInput.useFrenzyCharges and env.mode_combat then
|
|
output.FrenzyCharges = output.FrenzyChargesMax
|
|
else
|
|
output.FrenzyCharges = 0
|
|
end
|
|
if env.configInput.useEnduranceCharges and env.mode_combat then
|
|
output.EnduranceCharges = output.EnduranceChargesMax
|
|
else
|
|
output.EnduranceCharges = 0
|
|
end
|
|
modDB.multipliers["PowerCharge"] = output.PowerCharges
|
|
modDB.multipliers["FrenzyCharge"] = output.FrenzyCharges
|
|
modDB.multipliers["EnduranceCharge"] = output.EnduranceCharges
|
|
if output.PowerCharges == output.PowerChargesMax then
|
|
condList["AtMaxPowerCharges"] = true
|
|
end
|
|
if output.FrenzyCharges == output.FrenzyChargesMax then
|
|
condList["AtMaxFrenzyCharges"] = true
|
|
end
|
|
if output.EnduranceCharges == output.EnduranceChargesMax then
|
|
condList["AtMaxEnduranceCharges"] = true
|
|
end
|
|
|
|
-- Add misc buffs
|
|
if env.mode_combat then
|
|
if condList["Onslaught"] then
|
|
local effect = m_floor(20 * (1 + modDB:Sum("INC", nil, "OnslaughtEffect", "BuffEffect") / 100))
|
|
modDB:NewMod("Speed", "INC", effect, "Onslaught")
|
|
modDB:NewMod("MovementSpeed", "INC", effect, "Onslaught")
|
|
end
|
|
if condList["UnholyMight"] then
|
|
local effect = m_floor(30 * (1 + modDB:Sum("INC", nil, "BuffEffect") / 100))
|
|
modDB:NewMod("PhysicalDamageGainAsChaos", "BASE", effect, "Unholy Might")
|
|
end
|
|
end
|
|
|
|
-- Helper functions for stat breakdowns
|
|
local simpleBreakdown, modBreakdown, slotBreakdown, effMultBreakdown, dotBreakdown
|
|
if breakdown then
|
|
simpleBreakdown = function(extraBase, cfg, total, ...)
|
|
extraBase = extraBase or 0
|
|
local base = modDB:Sum("BASE", cfg, (...))
|
|
if (base + extraBase) ~= 0 then
|
|
local inc = modDB:Sum("INC", cfg, ...)
|
|
local more = modDB:Sum("MORE", cfg, ...)
|
|
if inc ~= 0 or more ~= 1 or (base ~= 0 and extraBase ~= 0) then
|
|
local out = { }
|
|
if base ~= 0 and extraBase ~= 0 then
|
|
out[1] = s_format("(%g + %g) ^8(base)", extraBase, base)
|
|
else
|
|
out[1] = s_format("%g ^8(base)", base + extraBase)
|
|
end
|
|
if inc ~= 0 then
|
|
t_insert(out, s_format("x %.2f", 1 + inc/100).." ^8(increased/reduced)")
|
|
end
|
|
if more ~= 1 then
|
|
t_insert(out, s_format("x %.2f", more).." ^8(more/less)")
|
|
end
|
|
t_insert(out, s_format("= %g", total))
|
|
return out
|
|
end
|
|
end
|
|
end
|
|
modBreakdown = function(cfg, ...)
|
|
local inc = modDB:Sum("INC", cfg, ...)
|
|
local more = modDB:Sum("MORE", cfg, ...)
|
|
if inc ~= 0 and more ~= 1 then
|
|
return {
|
|
s_format("%.2f", 1 + inc/100).." ^8(increased/reduced)",
|
|
s_format("x %.2f", more).." ^8(more/less)",
|
|
s_format("= %.2f", (1 + inc/100) * more),
|
|
}
|
|
end
|
|
end
|
|
slotBreakdown = function(source, sourceName, cfg, base, total, ...)
|
|
local inc = modDB:Sum("INC", cfg, ...)
|
|
local more = modDB:Sum("MORE", cfg, ...)
|
|
t_insert(breakdown[...].slots, {
|
|
base = base,
|
|
inc = (inc ~= 0) and s_format(" x %.2f", 1 + inc/100),
|
|
more = (more ~= 1) and s_format(" x %.2f", more),
|
|
total = s_format("%.2f", total or (base * (1 + inc / 100) * more)),
|
|
source = source,
|
|
sourceName = sourceName,
|
|
item = env.itemList[source],
|
|
})
|
|
end
|
|
effMultBreakdown = function(damageType, resist, pen, taken, mult)
|
|
local out = { }
|
|
local resistForm = (damageType == "Physical") and "physical damage reduction" or "resistance"
|
|
if resist ~= 0 then
|
|
t_insert(out, s_format("Enemy %s: %d%%", resistForm, resist))
|
|
end
|
|
if pen ~= 0 then
|
|
t_insert(out, "Effective resistance:")
|
|
t_insert(out, s_format("%d%% ^8(resistance)", resist))
|
|
t_insert(out, s_format("- %d%% ^8(penetration)", pen))
|
|
t_insert(out, s_format("= %d%%", resist - pen))
|
|
end
|
|
if (resist - pen) ~= 0 and taken ~= 0 then
|
|
t_insert(out, "Effective DPS modifier:")
|
|
t_insert(out, s_format("%.2f ^8(%s)", 1 - (resist - pen) / 100, resistForm))
|
|
t_insert(out, s_format("x %.2f ^8(increased/reduced damage taken)", 1 + taken / 100))
|
|
t_insert(out, s_format("= %.3f", mult))
|
|
end
|
|
return out
|
|
end
|
|
dotBreakdown = function(out, baseVal, inc, more, rate, effMult, total)
|
|
t_insert(out, s_format("%.1f ^8(base damage per second)", baseVal))
|
|
if inc ~= 0 then
|
|
t_insert(out, s_format("x %.2f ^8(increased/reduced)", 1 + inc/100))
|
|
end
|
|
if more ~= 1 then
|
|
t_insert(out, s_format("x %.2f ^8(more/less)", more))
|
|
end
|
|
if rate and rate ~= 1 then
|
|
t_insert(out, s_format("x %.2f ^8(rate modifier)", rate))
|
|
end
|
|
if effMult ~= 1 then
|
|
t_insert(out, s_format("x %.3f ^8(effective DPS modifier)", effMult))
|
|
end
|
|
t_insert(out, s_format("= %.1f ^8per second", total))
|
|
end
|
|
end
|
|
|
|
-- Calculate attributes
|
|
for _, stat in pairs({"Str","Dex","Int"}) do
|
|
output[stat] = round(calcVal(modDB, stat))
|
|
if breakdown then
|
|
breakdown[stat] = simpleBreakdown(nil, nil, output[stat], stat)
|
|
end
|
|
end
|
|
|
|
-- Add attribute bonuses
|
|
modDB:NewMod("Life", "BASE", m_floor(output.Str / 2), "Strength")
|
|
local strDmgBonus = round((output.Str + modDB:Sum("BASE", nil, "DexIntToMeleeBonus")) / 5)
|
|
modDB:NewMod("PhysicalDamage", "INC", strDmgBonus, "Strength", ModFlag.Melee)
|
|
modDB:NewMod("Accuracy", "BASE", output.Dex * 2, "Dexterity")
|
|
if not modDB:Sum("FLAG", nil, "IronReflexes") then
|
|
modDB:NewMod("Evasion", "INC", round(output.Dex / 5), "Dexterity")
|
|
end
|
|
modDB:NewMod("Mana", "BASE", round(output.Int / 2), "Intelligence")
|
|
modDB:NewMod("EnergyShield", "INC", round(output.Int / 5), "Intelligence")
|
|
|
|
-- ---------------------- --
|
|
-- Defensive Calculations --
|
|
-- ---------------------- --
|
|
|
|
-- Life/mana pools
|
|
if modDB:Sum("FLAG", nil, "ChaosInoculation") then
|
|
output.Life = 1
|
|
condList["FullLife"] = true
|
|
else
|
|
local base = modDB:Sum("BASE", cfg, "Life")
|
|
local inc = modDB:Sum("INC", cfg, "Life")
|
|
local more = modDB:Sum("MORE", cfg, "Life")
|
|
local conv = modDB:Sum("BASE", nil, "LifeConvertToEnergyShield")
|
|
output.Life = round(base * (1 + inc/100) * more * (1 - conv/100))
|
|
if breakdown then
|
|
if inc ~= 0 or more ~= 1 or conv ~= 0 then
|
|
breakdown.Life = { }
|
|
breakdown.Life[1] = s_format("%g ^8(base)", base)
|
|
if inc ~= 0 then
|
|
t_insert(breakdown.Life, s_format("x %.2f ^8(increased/reduced)", 1 + inc/100))
|
|
end
|
|
if more ~= 1 then
|
|
t_insert(breakdown.Life, s_format("x %.2f ^8(more/less)", more))
|
|
end
|
|
if conv ~= 0 then
|
|
t_insert(breakdown.Life, s_format("x %.2f ^8(converted to Energy Shield)", 1 - conv/100))
|
|
end
|
|
t_insert(breakdown.Life, s_format("= %g", output.Life))
|
|
end
|
|
end
|
|
end
|
|
output.Mana = round(calcVal(modDB, "Mana"))
|
|
output.ManaRegen = round((modDB:Sum("BASE", nil, "ManaRegen") + output.Mana * modDB:Sum("BASE", nil, "ManaRegenPercent") / 100) * calcMod(modDB, nil, "ManaRegen", "ManaRecovery"), 1)
|
|
if breakdown then
|
|
breakdown.Mana = simpleBreakdown(nil, nil, output.Mana, "Mana")
|
|
breakdown.ManaRegen = simpleBreakdown(nil, nil, output.ManaRegen, "ManaRegen", "ManaRecovery")
|
|
end
|
|
|
|
-- Life/mana reservation
|
|
for _, pool in pairs({"Life", "Mana"}) do
|
|
local max = output[pool]
|
|
local reserved = env["reserved_"..pool.."Base"] + m_ceil(max * env["reserved_"..pool.."Percent"] / 100)
|
|
output[pool.."Reserved"] = reserved
|
|
output[pool.."ReservedPercent"] = reserved / max * 100
|
|
output[pool.."Unreserved"] = max - reserved
|
|
output[pool.."UnreservedPercent"] = (max - reserved) / max * 100
|
|
if (max - reserved) / max <= 0.35 then
|
|
condList["Low"..pool] = true
|
|
end
|
|
if reserved == 0 then
|
|
condList["No"..pool.."Reserved"] = true
|
|
end
|
|
end
|
|
|
|
-- Resistances
|
|
for _, elem in ipairs(resistTypeList) do
|
|
local max, total
|
|
if elem == "Chaos" and modDB:Sum("FLAG", nil, "ChaosInoculation") then
|
|
max = 100
|
|
total = 100
|
|
else
|
|
max = modDB:Sum("BASE", nil, elem.."ResistMax")
|
|
total = modDB:Sum("BASE", nil, elem.."Resist", isElemental[elem] and "ElementalResist")
|
|
end
|
|
output[elem.."Resist"] = m_min(total, max)
|
|
output[elem.."ResistTotal"] = total
|
|
output[elem.."ResistOverCap"] = m_max(0, total - max)
|
|
if breakdown then
|
|
breakdown[elem.."Resist"] = {
|
|
"Max: "..max.."%",
|
|
"Total: "..total.."%",
|
|
"In hideout: "..(total + 60).."%",
|
|
}
|
|
end
|
|
end
|
|
|
|
-- Primary defences: Energy shield, evasion and armour
|
|
do
|
|
local ironReflexes = modDB:Sum("FLAG", nil, "IronReflexes")
|
|
local energyShield = 0
|
|
local armour = 0
|
|
local evasion = 0
|
|
if breakdown then
|
|
breakdown.EnergyShield = { slots = { } }
|
|
breakdown.Armour = { slots = { } }
|
|
breakdown.Evasion = { slots = { } }
|
|
end
|
|
local energyShieldBase = modDB:Sum("BASE", nil, "EnergyShield")
|
|
if energyShieldBase > 0 then
|
|
energyShield = energyShield + energyShieldBase * calcMod(modDB, nil, "EnergyShield", "Defences")
|
|
if breakdown then
|
|
slotBreakdown("Global", nil, nil, energyShieldBase, nil, "EnergyShield", "Defences")
|
|
end
|
|
end
|
|
local armourBase = modDB:Sum("BASE", nil, "Armour", "ArmourAndEvasion")
|
|
if armourBase > 0 then
|
|
armour = armour + armourBase * calcMod(modDB, nil, "Armour", "ArmourAndEvasion", "Defences")
|
|
if breakdown then
|
|
slotBreakdown("Global", nil, nil, armourBase, nil, "Armour", "ArmourAndEvasion", "Defences")
|
|
end
|
|
end
|
|
local evasionBase = modDB:Sum("BASE", nil, "Evasion", "ArmourAndEvasion")
|
|
if evasionBase > 0 then
|
|
if ironReflexes then
|
|
armour = armour + evasionBase * calcMod(modDB, nil, "Armour", "Evasion", "ArmourAndEvasion", "Defences")
|
|
if breakdown then
|
|
slotBreakdown("Conversion", "Evasion to Armour", nil, evasionBase, nil, "Armour", "Evasion", "ArmourAndEvasion", "Defences")
|
|
end
|
|
else
|
|
evasion = evasion + evasionBase * calcMod(modDB, nil, "Evasion", "ArmourAndEvasion", "Defences")
|
|
if breakdown then
|
|
slotBreakdown("Global", nil, nil, evasionBase, nil, "Evasion", "ArmourAndEvasion", "Defences")
|
|
end
|
|
end
|
|
end
|
|
local gearEnergyShield = 0
|
|
local gearArmour = 0
|
|
local gearEvasion = 0
|
|
local slotCfg = wipeTable(tempTable1)
|
|
for _, slot in pairs({"Helmet","Body Armour","Gloves","Boots","Weapon 2"}) do
|
|
local armourData = env.itemList[slot] and env.itemList[slot].armourData
|
|
if armourData then
|
|
slotCfg.slotName = slot
|
|
energyShieldBase = armourData.EnergyShield or 0
|
|
if energyShieldBase > 0 then
|
|
energyShield = energyShield + energyShieldBase * calcMod(modDB, slotCfg, "EnergyShield", "Defences")
|
|
gearEnergyShield = gearEnergyShield + energyShieldBase
|
|
if breakdown then
|
|
slotBreakdown(slot, nil, slotCfg, energyShieldBase, nil, "EnergyShield", "Defences")
|
|
end
|
|
end
|
|
armourBase = armourData.Armour or 0
|
|
if armourBase > 0 then
|
|
if slot == "Body Armour" and modDB:Sum("FLAG", nil, "Unbreakable") then
|
|
armourBase = armourBase * 2
|
|
end
|
|
armour = armour + armourBase * calcMod(modDB, slotCfg, "Armour", "ArmourAndEvasion", "Defences")
|
|
gearArmour = gearArmour + armourBase
|
|
if breakdown then
|
|
slotBreakdown(slot, nil, slotCfg, armourBase, nil, "Armour", "ArmourAndEvasion", "Defences")
|
|
end
|
|
end
|
|
evasionBase = armourData.Evasion or 0
|
|
if evasionBase > 0 then
|
|
if ironReflexes then
|
|
armour = armour + evasionBase * calcMod(modDB, slotCfg, "Armour", "Evasion", "ArmourAndEvasion", "Defences")
|
|
gearArmour = gearArmour + evasionBase
|
|
if breakdown then
|
|
slotBreakdown(slot, nil, slotCfg, evasionBase, nil, "Armour", "Evasion", "ArmourAndEvasion", "Defences")
|
|
end
|
|
else
|
|
evasion = evasion + evasionBase * calcMod(modDB, slotCfg, "Evasion", "ArmourAndEvasion", "Defences")
|
|
gearEvasion = gearEvasion + evasionBase
|
|
if breakdown then
|
|
slotBreakdown(slot, nil, slotCfg, evasionBase, nil, "Evasion", "ArmourAndEvasion", "Defences")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
local convManaToES = modDB:Sum("BASE", nil, "ManaGainAsEnergyShield")
|
|
if convManaToES > 0 then
|
|
energyShieldBase = modDB:Sum("BASE", nil, "Mana") * convManaToES / 100
|
|
energyShield = energyShield + energyShieldBase * calcMod(modDB, nil, "Mana", "EnergyShield", "Defences")
|
|
if breakdown then
|
|
slotBreakdown("Conversion", "Mana to Energy Shield", nil, energyShieldBase, nil, "EnergyShield", "Defences", "Mana")
|
|
end
|
|
end
|
|
local convLifeToES = modDB:Sum("BASE", nil, "LifeConvertToEnergyShield", "LifeGainAsEnergyShield")
|
|
if convLifeToES > 0 then
|
|
energyShieldBase = modDB:Sum("BASE", nil, "Life") * convLifeToES / 100
|
|
local total
|
|
if modDB:Sum("FLAG", nil, "ChaosInoculation") then
|
|
total = 1
|
|
else
|
|
total = energyShieldBase * calcMod(modDB, nil, "Life", "EnergyShield", "Defences")
|
|
end
|
|
energyShield = energyShield + total
|
|
if breakdown then
|
|
slotBreakdown("Conversion", "Life to Energy Shield", nil, energyShieldBase, total, "EnergyShield", "Defences", "Life")
|
|
end
|
|
end
|
|
output.EnergyShield = round(energyShield)
|
|
output.Armour = round(armour)
|
|
output.Evasion = round(evasion)
|
|
output.LowestOfArmourAndEvasion = m_min(output.Armour, output.Evasion)
|
|
output["Gear:EnergyShield"] = gearEnergyShield
|
|
output["Gear:Armour"] = gearArmour
|
|
output["Gear:Evasion"] = gearEvasion
|
|
output.EnergyShieldRecharge = round(output.EnergyShield * 0.2 * calcMod(modDB, nil, "EnergyShieldRecharge", "EnergyShieldRecovery"), 1)
|
|
output.EnergyShieldRechargeDelay = 2 / (1 + modDB:Sum("INC", nil, "EnergyShieldRechargeFaster") / 100)
|
|
if breakdown then
|
|
breakdown.EnergyShieldRecharge = simpleBreakdown(output.EnergyShield * 0.2, nil, output.EnergyShieldRecharge, "EnergyShieldRecharge", "EnergyShieldRecovery")
|
|
if output.EnergyShieldRechargeDelay ~= 2 then
|
|
breakdown.EnergyShieldRechargeDelay = {
|
|
"2.00s ^8(base)",
|
|
s_format("/ %.2f ^8(faster start)", 1 + modDB:Sum("INC", nil, "EnergyShieldRechargeFaster") / 100),
|
|
s_format("= %.2fs", output.EnergyShieldRechargeDelay)
|
|
}
|
|
end
|
|
end
|
|
if modDB:Sum("FLAG", nil, "CannotEvade") then
|
|
output.EvadeChance = 0
|
|
else
|
|
local enemyAccuracy = round(calcVal(enemyDB, "Accuracy"))
|
|
output.EvadeChance = 100 - calcHitChance(output.Evasion, enemyAccuracy) * calcMod(enemyDB, nil, "HitChance")
|
|
if breakdown then
|
|
breakdown.EvadeChance = {
|
|
s_format("Enemy level: %d ^8(%s the Configuration tab)", env.enemyLevel, env.configInput.enemyLevel and "overridden from" or "can be overridden in"),
|
|
s_format("Average enemy accuracy: %d", enemyAccuracy),
|
|
s_format("Approximate evade chance: %d%%", output.EvadeChance),
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Life and energy shield regen
|
|
do
|
|
if modDB:Sum("FLAG", nil, "NoLifeRegen") then
|
|
output.LifeRegen = 0
|
|
elseif modDB:Sum("FLAG", nil, "ZealotsOath") then
|
|
output.LifeRegen = 0
|
|
local lifeBase = modDB:Sum("BASE", nil, "LifeRegen")
|
|
if lifeBase > 0 then
|
|
modDB:NewMod("EnergyShieldRegen", "BASE", lifeBase, "Zealot's Oath")
|
|
end
|
|
local lifePercent = modDB:Sum("BASE", nil, "LifeRegenPercent")
|
|
if lifePercent > 0 then
|
|
modDB:NewMod("EnergyShieldRegenPercent", "BASE", lifePercent, "Zealot's Oath")
|
|
end
|
|
else
|
|
local lifeBase = modDB:Sum("BASE", nil, "LifeRegen")
|
|
local lifePercent = modDB:Sum("BASE", nil, "LifeRegenPercent")
|
|
if lifePercent > 0 then
|
|
lifeBase = lifeBase + output.Life * lifePercent / 100
|
|
end
|
|
if lifeBase > 0 then
|
|
output.LifeRegen = lifeBase * calcMod(modDB, nil, "LifeRecovery")
|
|
output.LifeRegenPercent = round(lifeBase / output.Life * 100, 1)
|
|
else
|
|
output.LifeRegen = 0
|
|
end
|
|
end
|
|
local esBase = modDB:Sum("BASE", nil, "EnergyShieldRegen")
|
|
local esPercent = modDB:Sum("BASE", nil, "EnergyShieldRegenPercent")
|
|
if esPercent > 0 then
|
|
esBase = esBase + output.EnergyShield * esPercent / 100
|
|
end
|
|
if esBase > 0 then
|
|
output.EnergyShieldRegen = esBase * calcMod(modDB, nil, "EnergyShieldRecovery")
|
|
output.EnergyShieldRegenPercent = round(esBase / output.EnergyShield * 100, 1)
|
|
else
|
|
output.EnergyShieldRegen = 0
|
|
end
|
|
end
|
|
|
|
-- Other defences: block, dodge, stun recovery/avoidance
|
|
do
|
|
output.MovementSpeedMod = calcMod(modDB, nil, "MovementSpeed")
|
|
if modDB:Sum("FLAG", nil, "MovementSpeedCannotBeBelowBase") then
|
|
output.MovementSpeedMod = m_max(output.MovementSpeedMod, 1)
|
|
end
|
|
output.BlockChanceMax = modDB:Sum("BASE", nil, "BlockChanceMax")
|
|
local shieldData = env.itemList["Weapon 2"] and env.itemList["Weapon 2"].armourData
|
|
output.BlockChance = m_min(((shieldData and shieldData.BlockChance or 0) + modDB:Sum("BASE", nil, "BlockChance")) * calcMod(modDB, nil, "BlockChance"), output.BlockChanceMax)
|
|
output.SpellBlockChance = m_min(modDB:Sum("BASE", nil, "SpellBlockChance") * calcMod(modDB, nil, "SpellBlockChance") + output.BlockChance * modDB:Sum("BASE", nil, "BlockChanceConv") / 100, output.BlockChanceMax)
|
|
if breakdown then
|
|
breakdown.BlockChance = simpleBreakdown(shieldData and shieldData.BlockChance, nil, output.BlockChance, "BlockChance")
|
|
breakdown.SpellBlockChance = simpleBreakdown(output.BlockChance * modDB:Sum("BASE", nil, "BlockChanceConv") / 100, nil, output.SpellBlockChance, "SpellBlockChance")
|
|
end
|
|
if modDB:Sum("FLAG", nil, "CannotBlockAttacks") then
|
|
output.BlockChance = 0
|
|
end
|
|
output.AttackDodgeChance = m_min(modDB:Sum("BASE", nil, "AttackDodgeChance"), 75)
|
|
output.SpellDodgeChance = m_min(modDB:Sum("BASE", nil, "SpellDodgeChance"), 75)
|
|
local stunChance = 100 - modDB:Sum("BASE", nil, "AvoidStun")
|
|
if output.EnergyShield > output.Life * 2 then
|
|
stunChance = stunChance * 0.5
|
|
end
|
|
output.StunAvoidChance = 100 - stunChance
|
|
if output.StunAvoidChance >= 100 then
|
|
output.StunDuration = 0
|
|
output.BlockDuration = 0
|
|
else
|
|
output.StunDuration = 0.35 / (1 + modDB:Sum("INC", nil, "StunRecovery") / 100)
|
|
output.BlockDuration = 0.35 / (1 + modDB:Sum("INC", nil, "StunRecovery", "BlockRecovery") / 100)
|
|
if breakdown then
|
|
breakdown.StunDuration = {
|
|
"0.35s ^8(base)",
|
|
s_format("/ %.2f ^8(increased/reduced recovery)", 1 + modDB:Sum("INC", nil, "StunRecovery") / 100),
|
|
s_format("= %.2fs", output.StunDuration)
|
|
}
|
|
breakdown.BlockDuration = {
|
|
"0.35s ^8(base)",
|
|
s_format("/ %.2f ^8(increased/reduced recovery)", 1 + modDB:Sum("INC", nil, "StunRecovery", "BlockRecovery") / 100),
|
|
s_format("= %.2fs", output.BlockDuration)
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ---------------------- --
|
|
-- Offensive Calculations --
|
|
-- ---------------------- --
|
|
|
|
if env.mainSkill.skillFlags.disable then
|
|
-- Skill is disabled
|
|
output.CombinedDPS = 0
|
|
return
|
|
end
|
|
|
|
-- Merge main skill mods
|
|
modDB:AddList(env.mainSkill.skillModList)
|
|
|
|
local skillData = env.mainSkill.skillData
|
|
local skillFlags = env.mainSkill.skillFlags
|
|
local skillCfg = env.mainSkill.skillCfg
|
|
if env.mainSkill.skillFlags.attack then
|
|
env.mode_skillType = "ATTACK"
|
|
else
|
|
env.mode_skillType = "SPELL"
|
|
end
|
|
if env.mainSkill.skillData.showAverage then
|
|
env.mode_average = true
|
|
else
|
|
skillFlags.notAverage = true
|
|
end
|
|
if env.mode_buffs then
|
|
skillFlags.buffs = true
|
|
end
|
|
if env.mode_combat then
|
|
skillFlags.combat = true
|
|
end
|
|
if env.mode_effective then
|
|
skillFlags.effective = true
|
|
end
|
|
|
|
-- Update skill data
|
|
for _, value in ipairs(modDB:Sum("LIST", skillCfg, "Misc")) do
|
|
if value.type == "SkillData" then
|
|
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
|
|
end
|
|
|
|
env.modDB.conditions["SkillIsTriggered"] = skillData.triggered
|
|
|
|
-- Add addition stat bonuses
|
|
if modDB:Sum("FLAG", nil, "IronGrip") then
|
|
modDB:NewMod("PhysicalDamage", "INC", strDmgBonus, "Strength", bor(ModFlag.Attack, ModFlag.Projectile))
|
|
end
|
|
if modDB:Sum("FLAG", nil, "IronWill") then
|
|
modDB:NewMod("Damage", "INC", strDmgBonus, "Strength", ModFlag.Spell)
|
|
end
|
|
|
|
if modDB:Sum("FLAG", nil, "MinionDamageAppliesToPlayer") then
|
|
-- Minion Damage conversion from The Scourge
|
|
for _, mod in ipairs(modDB.mods.Damage or { }) do
|
|
if mod.type == "INC" and mod.keywordFlags == KeywordFlag.Minion then
|
|
modDB:NewMod("Damage", "INC", mod.value, mod.source, 0, 0, unpack(mod.tagList))
|
|
end
|
|
end
|
|
end
|
|
if modDB:Sum("FLAG", nil, "SpellDamageAppliesToAttacks") then
|
|
-- Spell Damage conversion from Crown of Eyes
|
|
for i, mod in ipairs(modDB.mods.Damage or { }) do
|
|
if mod.type == "INC" and band(mod.flags, ModFlag.Spell) ~= 0 then
|
|
modDB:NewMod("Damage", "INC", mod.value, mod.source, bor(band(mod.flags, bnot(ModFlag.Spell)), ModFlag.Attack), mod.keywordFlags, unpack(mod.tagList))
|
|
end
|
|
end
|
|
end
|
|
|
|
local isAttack = (env.mode_skillType == "ATTACK")
|
|
|
|
-- Calculate skill type stats
|
|
if skillFlags.projectile then
|
|
output.ProjectileCount = modDB:Sum("BASE", skillCfg, "ProjectileCount")
|
|
output.PierceChance = m_min(100, modDB:Sum("BASE", skillCfg, "PierceChance"))
|
|
output.ProjectileSpeedMod = calcMod(modDB, skillCfg, "ProjectileSpeed")
|
|
if breakdown then
|
|
breakdown.ProjectileSpeedMod = modBreakdown(skillCfg, "ProjectileSpeed")
|
|
end
|
|
end
|
|
if skillFlags.area then
|
|
output.AreaRadiusMod = calcMod(modDB, skillCfg, "AreaRadius")
|
|
if breakdown then
|
|
breakdown.AreaRadiusMod = modBreakdown(skillCfg, "AreaRadius")
|
|
end
|
|
end
|
|
if skillFlags.trap then
|
|
output.ActiveTrapLimit = modDB:Sum("BASE", skillCfg, "ActiveTrapLimit")
|
|
output.TrapCooldown = (skillData.trapCooldown or 4) / calcMod(modDB, skillCfg, "TrapCooldownRecovery")
|
|
if breakdown then
|
|
breakdown.TrapCooldown = {
|
|
s_format("%.2fs ^8(base)", skillData.trapCooldown or 4),
|
|
s_format("/ %.2f ^8(increased/reduced cooldown recovery)", 1 + modDB:Sum("INC", skillCfg, "TrapCooldownRecovery") / 100),
|
|
s_format("= %.2fs", output.TrapCooldown)
|
|
}
|
|
end
|
|
end
|
|
if skillFlags.mine then
|
|
output.ActiveMineLimit = modDB:Sum("BASE", skillCfg, "ActiveMineLimit")
|
|
end
|
|
if skillFlags.totem then
|
|
output.ActiveTotemLimit = modDB:Sum("BASE", skillCfg, "ActiveTotemLimit")
|
|
output.TotemLifeMod = calcMod(modDB, skillCfg, "TotemLife")
|
|
output.TotemLife = round(data.monsterLifeTable[skillData.totemLevel] * data.totemLifeMult[env.mainSkill.skillTotemId] * output.TotemLifeMod)
|
|
if breakdown then
|
|
breakdown.TotemLifeMod = modBreakdown(skillCfg, "TotemLife")
|
|
breakdown.TotemLife = {
|
|
"Totem level: "..skillData.totemLevel,
|
|
data.monsterLifeTable[skillData.totemLevel].." ^8(base life for a level "..skillData.totemLevel.." monster)",
|
|
"x "..data.totemLifeMult[env.mainSkill.skillTotemId].." ^8(life multiplier for this totem type)",
|
|
"x "..output.TotemLifeMod.." ^8(totem life modifier)",
|
|
"= "..output.TotemLife,
|
|
}
|
|
end
|
|
end
|
|
|
|
-- Skill duration
|
|
local debuffDurationMult
|
|
if env.mode_effective then
|
|
debuffDurationMult = 1 / calcMod(enemyDB, skillCfg, "BuffExpireFaster")
|
|
else
|
|
debuffDurationMult = 1
|
|
end
|
|
do
|
|
output.DurationMod = calcMod(modDB, skillCfg, "Duration")
|
|
if breakdown then
|
|
breakdown.DurationMod = modBreakdown(skillCfg, "Duration")
|
|
end
|
|
local durationBase = skillData.duration or 0
|
|
if durationBase > 0 then
|
|
output.Duration = durationBase * output.DurationMod
|
|
if skillData.debuff then
|
|
output.Duration = output.Duration * debuffDurationMult
|
|
end
|
|
if breakdown and output.Duration ~= durationBase then
|
|
breakdown.Duration = {
|
|
s_format("%.2fs ^8(base)", durationBase),
|
|
}
|
|
if output.DurationMod ~= 1 then
|
|
t_insert(breakdown.Duration, s_format("x %.2f ^8(duration modifier)", output.DurationMod))
|
|
end
|
|
if skillData.debuff and debuffDurationMult ~= 1 then
|
|
t_insert(breakdown.Duration, s_format("/ %.2f ^8(debuff expires slower/faster)", 1 / debuffDurationMult))
|
|
end
|
|
t_insert(breakdown.Duration, s_format("= %.2fs", output.Duration))
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Run skill setup function
|
|
do
|
|
local setupFunc = env.mainSkill.activeGem.data.setupFunc
|
|
if setupFunc then
|
|
setupFunc(env, output)
|
|
end
|
|
end
|
|
|
|
-- Cache global damage disabling flags
|
|
local canDeal = { }
|
|
for _, damageType in pairs(dmgTypeList) do
|
|
canDeal[damageType] = not modDB:Sum("FLAG", skillCfg, "DealNo"..damageType)
|
|
end
|
|
|
|
-- Calculate damage conversion percentages
|
|
env.conversionTable = wipeTable(env.conversionTable)
|
|
for damageTypeIndex = 1, 4 do
|
|
local damageType = dmgTypeList[damageTypeIndex]
|
|
local globalConv = wipeTable(tempTable1)
|
|
local skillConv = wipeTable(tempTable2)
|
|
local add = wipeTable(tempTable3)
|
|
local globalTotal, skillTotal = 0, 0
|
|
for otherTypeIndex = damageTypeIndex + 1, 5 do
|
|
-- For all possible destination types, check for global and skill conversions
|
|
otherType = dmgTypeList[otherTypeIndex]
|
|
globalConv[otherType] = modDB:Sum("BASE", skillCfg, damageType.."DamageConvertTo"..otherType, isElemental[damageType] and "ElementalDamageConvertTo"..otherType or nil)
|
|
globalTotal = globalTotal + globalConv[otherType]
|
|
skillConv[otherType] = modDB:Sum("BASE", skillCfg, "Skill"..damageType.."DamageConvertTo"..otherType)
|
|
skillTotal = skillTotal + skillConv[otherType]
|
|
add[otherType] = modDB:Sum("BASE", skillCfg, damageType.."DamageGainAs"..otherType, isElemental[damageType] and "ElementalDamageGainAs"..otherType or nil)
|
|
end
|
|
if skillTotal > 100 then
|
|
-- Skill conversion exceeds 100%, scale it down and remove non-skill conversions
|
|
local factor = 100 / skillTotal
|
|
for type, val in pairs(skillConv) do
|
|
-- The game currently doesn't scale this down even though it is supposed to
|
|
--skillConv[type] = val * factor
|
|
end
|
|
for type, val in pairs(globalConv) do
|
|
globalConv[type] = 0
|
|
end
|
|
elseif globalTotal + skillTotal > 100 then
|
|
-- Conversion exceeds 100%, scale down non-skill conversions
|
|
local factor = (100 - skillTotal) / globalTotal
|
|
for type, val in pairs(globalConv) do
|
|
globalConv[type] = val * factor
|
|
end
|
|
globalTotal = globalTotal * factor
|
|
end
|
|
local dmgTable = { }
|
|
for type, val in pairs(globalConv) do
|
|
dmgTable[type] = (globalConv[type] + skillConv[type] + add[type]) / 100
|
|
end
|
|
dmgTable.mult = 1 - m_min((globalTotal + skillTotal) / 100, 1)
|
|
env.conversionTable[damageType] = dmgTable
|
|
end
|
|
env.conversionTable["Chaos"] = { mult = 1 }
|
|
|
|
-- Calculate mana cost (may be slightly off due to rounding differences)
|
|
do
|
|
local more = m_floor(modDB:Sum("MORE", skillCfg, "ManaCost") * 100 + 0.0001) / 100
|
|
local inc = modDB:Sum("INC", skillCfg, "ManaCost")
|
|
local base = modDB:Sum("BASE", skillCfg, "ManaCost")
|
|
output.ManaCost = m_floor(m_max(0, (skillData.manaCost or 0) * more * (1 + inc / 100) + base))
|
|
if env.mainSkill.skillTypes[SkillType.ManaCostPercent] and skillFlags.totem then
|
|
output.ManaCost = m_floor(output.Mana * output.ManaCost / 100)
|
|
end
|
|
if breakdown and output.ManaCost ~= (skillData.manaCost or 0) then
|
|
breakdown.ManaCost = {
|
|
s_format("%d ^8(base mana cost)", skillData.manaCost or 0)
|
|
}
|
|
if more ~= 1 then
|
|
t_insert(breakdown.ManaCost, s_format("x %.2f ^8(mana cost multiplier)", more))
|
|
end
|
|
if inc ~= 0 then
|
|
t_insert(breakdown.ManaCost, s_format("x %.2f ^8(increased/reduced mana cost)", 1 + inc/100))
|
|
end
|
|
if base ~= 0 then
|
|
t_insert(breakdown.ManaCost, s_format("- %d ^8(- mana cost)", -base))
|
|
end
|
|
t_insert(breakdown.ManaCost, s_format("= %d", output.ManaCost))
|
|
end
|
|
end
|
|
|
|
-- Configure damage passes
|
|
local passList = { }
|
|
if isAttack then
|
|
output.MainHand = { }
|
|
output.OffHand = { }
|
|
if skillFlags.weapon1Attack then
|
|
if breakdown then
|
|
breakdown.MainHand = { }
|
|
end
|
|
env.mainSkill.weapon1Cfg.skillStats = output.MainHand
|
|
t_insert(passList, {
|
|
label = "Main Hand",
|
|
source = env.weaponData1,
|
|
cfg = env.mainSkill.weapon1Cfg,
|
|
output = output.MainHand,
|
|
breakdown = breakdown and breakdown.MainHand,
|
|
})
|
|
end
|
|
if skillFlags.weapon2Attack then
|
|
if breakdown then
|
|
breakdown.OffHand = { }
|
|
end
|
|
env.mainSkill.weapon2Cfg.skillStats = output.OffHand
|
|
t_insert(passList, {
|
|
label = "Off Hand",
|
|
source = env.weaponData2,
|
|
cfg = env.mainSkill.weapon2Cfg,
|
|
output = output.OffHand,
|
|
breakdown = breakdown and breakdown.OffHand,
|
|
})
|
|
end
|
|
else
|
|
t_insert(passList, {
|
|
label = "Skill",
|
|
source = skillData,
|
|
cfg = skillCfg,
|
|
output = output,
|
|
breakdown = breakdown,
|
|
})
|
|
end
|
|
|
|
local function combineStat(stat, mode, ...)
|
|
-- Combine stats from Main Hand and Off Hand according to the mode
|
|
if mode == "OR" or not skillFlags.bothWeaponAttack then
|
|
output[stat] = output.MainHand[stat] or output.OffHand[stat]
|
|
elseif mode == "ADD" then
|
|
output[stat] = (output.MainHand[stat] or 0) + (output.OffHand[stat] or 0)
|
|
elseif mode == "AVERAGE" then
|
|
output[stat] = ((output.MainHand[stat] or 0) + (output.OffHand[stat] or 0)) / 2
|
|
elseif mode == "CHANCE" then
|
|
if output.MainHand[stat] and output.OffHand[stat] then
|
|
local mainChance = output.MainHand[...] * output.MainHand.HitChance
|
|
local offChance = output.OffHand[...] * output.OffHand.HitChance
|
|
local mainPortion = mainChance / (mainChance + offChance)
|
|
local offPortion = offChance / (mainChance + offChance)
|
|
output[stat] = output.MainHand[stat] * mainPortion + output.OffHand[stat] * offPortion
|
|
if breakdown then
|
|
if not breakdown[stat] then
|
|
breakdown[stat] = { }
|
|
end
|
|
t_insert(breakdown[stat], "Contribution from Main Hand:")
|
|
t_insert(breakdown[stat], s_format("%.1f", output.MainHand[stat]))
|
|
t_insert(breakdown[stat], s_format("x %.3f ^8(portion of instances created by main hand)", mainPortion))
|
|
t_insert(breakdown[stat], s_format("= %.1f", output.MainHand[stat] * mainPortion))
|
|
t_insert(breakdown[stat], "Contribution from Off Hand:")
|
|
t_insert(breakdown[stat], s_format("%.1f", output.OffHand[stat]))
|
|
t_insert(breakdown[stat], s_format("x %.3f ^8(portion of instances created by off hand)", offPortion))
|
|
t_insert(breakdown[stat], s_format("= %.1f", output.OffHand[stat] * offPortion))
|
|
t_insert(breakdown[stat], "Total:")
|
|
t_insert(breakdown[stat], s_format("%.1f + %.1f", output.MainHand[stat] * mainPortion, output.OffHand[stat] * offPortion))
|
|
t_insert(breakdown[stat], s_format("= %.1f", output[stat]))
|
|
end
|
|
else
|
|
output[stat] = output.MainHand[stat] or output.OffHand[stat]
|
|
end
|
|
elseif mode == "DPS" then
|
|
output[stat] = (output.MainHand[stat] or 0) + (output.OffHand[stat] or 0)
|
|
if not skillData.doubleHitsWhenDualWielding then
|
|
output[stat] = output[stat] / 2
|
|
end
|
|
end
|
|
end
|
|
|
|
for _, pass in ipairs(passList) do
|
|
local source, output, cfg, breakdown = pass.source, pass.output, pass.cfg, pass.breakdown
|
|
|
|
-- Calculate hit chance
|
|
output.Accuracy = calcVal(modDB, "Accuracy", cfg)
|
|
if breakdown then
|
|
breakdown.Accuracy = simpleBreakdown(nil, cfg, output.Accuracy, "Accuracy")
|
|
end
|
|
if not isAttack or modDB:Sum("FLAG", cfg, "CannotBeEvaded") or skillData.cannotBeEvaded then
|
|
output.HitChance = 100
|
|
else
|
|
local enemyEvasion = round(calcVal(enemyDB, "Evasion"))
|
|
output.HitChance = calcHitChance(enemyEvasion, output.Accuracy)
|
|
if breakdown then
|
|
breakdown.HitChance = {
|
|
"Enemy level: "..env.enemyLevel..(env.configInput.enemyLevel and " ^8(overridden from the Configuration tab" or " ^8(can be overridden in the Configuration tab)"),
|
|
"Average enemy evasion: "..enemyEvasion,
|
|
"Approximate hit chance: "..output.HitChance.."%",
|
|
}
|
|
end
|
|
end
|
|
|
|
-- Calculate attack/cast speed
|
|
if skillData.timeOverride then
|
|
output.Time = skillData.timeOverride
|
|
output.Speed = 1 / output.Time
|
|
else
|
|
local baseSpeed
|
|
if isAttack then
|
|
if skillData.castTimeOverridesAttackTime then
|
|
-- Skill is overriding weapon attack speed
|
|
baseSpeed = 1 / skillData.castTime * (1 + (source.AttackSpeedInc or 0) / 100)
|
|
else
|
|
baseSpeed = source.attackRate or 1
|
|
end
|
|
else
|
|
baseSpeed = 1 / (skillData.castTime or 1)
|
|
end
|
|
output.Speed = baseSpeed * round(calcMod(modDB, cfg, "Speed"), 2)
|
|
output.Time = 1 / output.Speed
|
|
if breakdown then
|
|
breakdown.Speed = simpleBreakdown(baseSpeed, cfg, output.Speed, "Speed")
|
|
end
|
|
end
|
|
if skillData.hitTimeOverride then
|
|
output.HitTime = skillData.hitTimeOverride
|
|
output.HitSpeed = 1 / output.HitTime
|
|
end
|
|
end
|
|
|
|
if isAttack then
|
|
-- Combine hit chance and attack speed
|
|
combineStat("HitChance", "AVERAGE")
|
|
combineStat("Speed", "AVERAGE")
|
|
output.Time = 1 / output.Speed
|
|
if skillFlags.bothWeaponAttack then
|
|
if breakdown then
|
|
breakdown.Speed = {
|
|
"Both weapons:",
|
|
s_format("(%.2f + %.2f) / 2", output.MainHand.Speed, output.OffHand.Speed),
|
|
s_format("= %.2f", output.Speed),
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
for _, pass in ipairs(passList) do
|
|
local globalOutput, globalBreakdown = output, breakdown
|
|
local source, output, cfg, breakdown = pass.source, pass.output, pass.cfg, pass.breakdown
|
|
|
|
-- Calculate crit chance, crit multiplier, and their combined effect
|
|
if modDB:Sum("FLAG", nil, "NeverCrit") then
|
|
output.CritChance = 0
|
|
output.CritMultiplier = 0
|
|
output.CritEffect = 1
|
|
else
|
|
local baseCrit = source.critChance or 0
|
|
if baseCrit == 100 then
|
|
output.CritChance = 100
|
|
else
|
|
local base = modDB:Sum("BASE", cfg, "CritChance")
|
|
local inc = modDB:Sum("INC", cfg, "CritChance")
|
|
local more = modDB:Sum("MORE", cfg, "CritChance")
|
|
output.CritChance = (baseCrit + base) * (1 + inc / 100) * more
|
|
if env.mode_effective then
|
|
output.CritChance = output.CritChance + enemyDB:Sum("BASE", nil, "SelfExtraCritChance")
|
|
end
|
|
local preCapCritChance = output.CritChance
|
|
output.CritChance = m_min(output.CritChance, 95)
|
|
if (baseCrit + base) > 0 then
|
|
output.CritChance = m_max(output.CritChance, 5)
|
|
end
|
|
local preLuckyCritChance = output.CritChance
|
|
if env.mode_effective and modDB:Sum("FLAG", cfg, "CritChanceLucky") then
|
|
output.CritChance = (1 - (1 - output.CritChance / 100) ^ 2) * 100
|
|
end
|
|
local preHitCheckCritChance = output.CritChance
|
|
if env.mode_effective then
|
|
output.CritChance = output.CritChance * output.HitChance / 100
|
|
end
|
|
if breakdown and output.CritChance ~= baseCrit then
|
|
local enemyExtra = enemyDB:Sum("BASE", nil, "SelfExtraCritChance")
|
|
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
|
|
if env.mode_effective and enemyExtra ~= 0 then
|
|
t_insert(breakdown.CritChance, s_format("+ %g ^8(extra chance for enemy to be crit)", enemyExtra))
|
|
end
|
|
t_insert(breakdown.CritChance, s_format("= %g", preLuckyCritChance))
|
|
if preCapCritChance > 95 then
|
|
local overCap = preCapCritChance - 95
|
|
t_insert(breakdown.CritChance, s_format("Crit is overcapped by %.2f%% (%d%% increased Critical Strike Chance)", overCap, overCap / more / (baseCrit + base) * 100))
|
|
end
|
|
if env.mode_effective and modDB:Sum("FLAG", cfg, "CritChanceLucky") then
|
|
t_insert(breakdown.CritChance, "Crit Chance is Lucky:")
|
|
t_insert(breakdown.CritChance, s_format("1 - (1 - %.4f) x (1 - %.4f)", preLuckyCritChance / 100, preLuckyCritChance / 100))
|
|
t_insert(breakdown.CritChance, s_format("= %.2f", preHitCheckCritChance))
|
|
end
|
|
if env.mode_effective and output.HitChance < 100 then
|
|
t_insert(breakdown.CritChance, "Crit confirmation roll:")
|
|
t_insert(breakdown.CritChance, s_format("%.2f", preHitCheckCritChance))
|
|
t_insert(breakdown.CritChance, s_format("x %.2f ^8(chance to hit)", output.HitChance / 100))
|
|
t_insert(breakdown.CritChance, s_format("= %.2f", output.CritChance))
|
|
end
|
|
end
|
|
end
|
|
if modDB:Sum("FLAG", cfg, "NoCritMultiplier") then
|
|
output.CritMultiplier = 1
|
|
else
|
|
local extraDamage = 0.5 + modDB:Sum("BASE", cfg, "CritMultiplier") / 100
|
|
if env.mode_effective then
|
|
extraDamage = round(extraDamage * (1 + enemyDB:Sum("INC", nil, "SelfCritMultiplier") / 100), 2)
|
|
end
|
|
output.CritMultiplier = 1 + m_max(0, extraDamage)
|
|
if breakdown and output.CritMultiplier ~= 1.5 then
|
|
breakdown.CritMultiplier = {
|
|
"50% ^8(base)",
|
|
}
|
|
local base = modDB:Sum("BASE", cfg, "CritMultiplier")
|
|
if base ~= 0 then
|
|
t_insert(breakdown.CritMultiplier, s_format("+ %d%% ^8(additional extra damage)", base))
|
|
end
|
|
local enemyInc = 1 + enemyDB:Sum("INC", nil, "SelfCritMultiplier") / 100
|
|
if env.mode_effective and enemyInc ~= 1 then
|
|
t_insert(breakdown.CritMultiplier, s_format("x %.2f ^8(increased/reduced extra crit damage taken by enemy)", enemyInc))
|
|
end
|
|
t_insert(breakdown.CritMultiplier, s_format("= %d%% ^8(extra crit damage)", extraDamage * 100))
|
|
end
|
|
end
|
|
output.CritEffect = 1 - output.CritChance / 100 + output.CritChance / 100 * output.CritMultiplier
|
|
if breakdown and output.CritEffect ~= 1 then
|
|
breakdown.CritEffect = {
|
|
s_format("(1 - %.4f) ^8(portion of damage from non-crits)", output.CritChance/100),
|
|
s_format("+ (%.4f x %g) ^8(portion of damage from crits)", output.CritChance/100, output.CritMultiplier),
|
|
s_format("= %.3f", output.CritEffect),
|
|
}
|
|
end
|
|
end
|
|
|
|
-- Calculate hit damage for each damage type
|
|
local totalHitMin, totalHitMax = 0, 0
|
|
local totalCritMin, totalCritMax = 0, 0
|
|
for pass = 1, 2 do
|
|
-- Pass 1 is critical strike damage, pass 2 is non-critical strike
|
|
condList["CriticalStrike"] = (pass == 1)
|
|
for _, damageType in ipairs(dmgTypeList) do
|
|
local min, max
|
|
if skillFlags.hit and canDeal[damageType] then
|
|
if breakdown then
|
|
breakdown[damageType] = {
|
|
damageComponents = { }
|
|
}
|
|
end
|
|
min, max = calcHitDamage(env, source, cfg, breakdown and breakdown[damageType], damageType)
|
|
local convMult = env.conversionTable[damageType].mult
|
|
if breakdown then
|
|
t_insert(breakdown[damageType], "Hit damage:")
|
|
t_insert(breakdown[damageType], s_format("%d to %d ^8(total damage)", min, max))
|
|
if convMult ~= 1 then
|
|
t_insert(breakdown[damageType], s_format("x %g ^8(%g%% converted to other damage types)", convMult, (1-convMult)*100))
|
|
end
|
|
end
|
|
min = min * convMult
|
|
max = max * convMult
|
|
if pass == 1 then
|
|
-- Apply crit multiplier
|
|
min = min * output.CritMultiplier
|
|
max = max * output.CritMultiplier
|
|
end
|
|
if (min ~= 0 or max ~= 0) and env.mode_effective then
|
|
-- Apply enemy resistances and damage taken modifiers
|
|
local preMult
|
|
local resist = 0
|
|
local pen = 0
|
|
local taken = enemyDB:Sum("INC", nil, "DamageTaken", damageType.."DamageTaken")
|
|
if damageType == "Physical" then
|
|
resist = enemyDB:Sum("INC", nil, "PhysicalDamageReduction")
|
|
else
|
|
resist = enemyDB:Sum("BASE", nil, damageType.."Resist")
|
|
if isElemental[damageType] then
|
|
resist = resist + enemyDB:Sum("BASE", nil, "ElementalResist")
|
|
pen = modDB:Sum("BASE", cfg, damageType.."Penetration", "ElementalPenetration")
|
|
taken = taken + enemyDB:Sum("INC", nil, "ElementalDamageTaken")
|
|
end
|
|
resist = m_min(resist, 75)
|
|
end
|
|
if skillFlags.projectile then
|
|
taken = taken + enemyDB:Sum("INC", nil, "ProjectileDamageTaken")
|
|
end
|
|
local effMult = (1 + taken / 100)
|
|
if not isElemental[damageType] or not modDB:Sum("FLAG", cfg, "IgnoreElementalResistances") then
|
|
effMult = effMult * (1 - (resist - pen) / 100)
|
|
end
|
|
min = min * effMult
|
|
max = max * effMult
|
|
if env.mode == "CALCS" then
|
|
output[damageType.."EffMult"] = effMult
|
|
end
|
|
if breakdown and effMult ~= 1 then
|
|
t_insert(breakdown[damageType], s_format("x %.3f ^8(effective DPS modifier)", effMult))
|
|
breakdown[damageType.."EffMult"] = effMultBreakdown(damageType, resist, pen, taken, effMult)
|
|
end
|
|
end
|
|
if breakdown then
|
|
t_insert(breakdown[damageType], s_format("= %d to %d", min, max))
|
|
end
|
|
else
|
|
min, max = 0, 0
|
|
if breakdown then
|
|
breakdown[damageType] = {
|
|
"You can't deal "..damageType.." damage"
|
|
}
|
|
end
|
|
end
|
|
if pass == 1 then
|
|
output[damageType.."CritAverage"] = (min + max) / 2
|
|
totalCritMin = totalCritMin + min
|
|
totalCritMax = totalCritMax + max
|
|
else
|
|
if env.mode == "CALCS" then
|
|
output[damageType.."Min"] = min
|
|
output[damageType.."Max"] = max
|
|
end
|
|
output[damageType.."HitAverage"] = (min + max) / 2
|
|
totalHitMin = totalHitMin + min
|
|
totalHitMax = totalHitMax + max
|
|
end
|
|
end
|
|
end
|
|
output.TotalMin = totalHitMin
|
|
output.TotalMax = totalHitMax
|
|
|
|
if 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
|
|
|
|
-- Calculate average damage and final DPS
|
|
output.AverageHit = (totalHitMin + totalHitMax) / 2 * (1 - output.CritChance / 100) + (totalCritMin + totalCritMax) / 2 * output.CritChance / 100
|
|
output.AverageDamage = output.AverageHit * output.HitChance / 100
|
|
output.TotalDPS = output.AverageDamage * (globalOutput.HitSpeed or globalOutput.Speed) * (skillData.dpsMultiplier or 1)
|
|
if breakdown then
|
|
if output.CritEffect ~= 1 then
|
|
breakdown.AverageHit = {
|
|
s_format("%.1f x (1 - %.4f) ^8(damage from non-crits)", (totalHitMin + totalHitMax) / 2, output.CritChance / 100),
|
|
s_format("+ %.1f x %.4f ^8(damage from crits)", (totalCritMin + totalCritMax) / 2, output.CritChance / 100),
|
|
s_format("= %.1f", output.AverageHit),
|
|
}
|
|
end
|
|
if isAttack then
|
|
breakdown.AverageDamage = { }
|
|
t_insert(breakdown.AverageDamage, s_format("%s:", pass.label))
|
|
t_insert(breakdown.AverageDamage, s_format("%.1f ^8(average hit)", output.AverageHit))
|
|
t_insert(breakdown.AverageDamage, s_format("x %.2f ^8(chance to hit)", output.HitChance / 100))
|
|
t_insert(breakdown.AverageDamage, s_format("= %.1f", output.AverageDamage))
|
|
end
|
|
end
|
|
end
|
|
|
|
if isAttack then
|
|
-- Combine crit stats, average damage and DPS
|
|
combineStat("CritChance", "AVERAGE")
|
|
combineStat("CritMultiplier", "AVERAGE")
|
|
combineStat("AverageDamage", "DPS")
|
|
combineStat("TotalDPS", "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 env.mode_average 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 skill DOT components
|
|
local dotCfg = {
|
|
skillName = skillCfg.skillName,
|
|
skillPart = skillCfg.skillPart,
|
|
slotName = skillCfg.slotName,
|
|
flags = bor(band(skillCfg.flags, ModFlag.SourceMask), ModFlag.Dot, skillData.dotIsSpell and ModFlag.Spell or 0),
|
|
keywordFlags = skillCfg.keywordFlags
|
|
}
|
|
env.mainSkill.dotCfg = dotCfg
|
|
output.TotalDot = 0
|
|
for _, damageType in ipairs(dmgTypeList) do
|
|
local baseVal
|
|
if canDeal[damageType] then
|
|
baseVal = skillData[damageType.."Dot"] or 0
|
|
else
|
|
baseVal = 0
|
|
end
|
|
if baseVal > 0 then
|
|
skillFlags.dot = true
|
|
local effMult = 1
|
|
if env.mode_effective then
|
|
local resist = 0
|
|
local taken = enemyDB:Sum("INC", nil, "DamageTaken", damageType.."DamageTaken", "DotTaken")
|
|
if damageType == "Physical" then
|
|
resist = enemyDB:Sum("INC", nil, "PhysicalDamageReduction")
|
|
else
|
|
resist = enemyDB:Sum("BASE", nil, damageType.."Resist")
|
|
if isElemental[damageType] then
|
|
resist = resist + enemyDB:Sum("BASE", nil, "ElementalResist")
|
|
taken = taken + enemyDB:Sum("INC", nil, "ElementalDamageTaken")
|
|
end
|
|
if damageType == "Fire" then
|
|
taken = taken + enemyDB:Sum("INC", nil, "BurningDamageTaken")
|
|
end
|
|
resist = m_min(resist, 75)
|
|
end
|
|
effMult = (1 - resist / 100) * (1 + taken / 100)
|
|
output[damageType.."DotEffMult"] = effMult
|
|
if breakdown and effMult ~= 1 then
|
|
breakdown[damageType.."DotEffMult"] = effMultBreakdown(damageType, resist, 0, taken, effMult)
|
|
end
|
|
end
|
|
local inc = modDB:Sum("INC", dotCfg, "Damage", damageType.."Damage", isElemental[damageType] and "ElementalDamage" or nil)
|
|
local more = round(modDB:Sum("MORE", dotCfg, "Damage", damageType.."Damage", isElemental[damageType] and "ElementalDamage" or nil), 2)
|
|
local total = baseVal * (1 + inc/100) * more * effMult
|
|
output[damageType.."Dot"] = total
|
|
output.TotalDot = output.TotalDot + total
|
|
if breakdown then
|
|
breakdown[damageType.."Dot"] = { }
|
|
dotBreakdown(breakdown[damageType.."Dot"], baseVal, inc, more, nil, effMult, total)
|
|
end
|
|
end
|
|
end
|
|
|
|
skillFlags.bleed = false
|
|
skillFlags.poison = false
|
|
skillFlags.ignite = false
|
|
skillFlags.igniteCanStack = modDB:Sum("FLAG", nil, "IgniteCanStack")
|
|
skillFlags.shock = false
|
|
skillFlags.freeze = false
|
|
for _, pass in ipairs(passList) do
|
|
local globalOutput, globalBreakdown = output, breakdown
|
|
local source, output, cfg, breakdown = pass.source, pass.output, pass.cfg, pass.breakdown
|
|
|
|
-- Calculate chance to inflict secondary dots/status effects
|
|
condList["CriticalStrike"] = true
|
|
if modDB:Sum("FLAG", cfg, "CannotBleed") then
|
|
output.BleedChanceOnCrit = 0
|
|
else
|
|
output.BleedChanceOnCrit = m_min(100, modDB:Sum("BASE", cfg, "BleedChance"))
|
|
end
|
|
output.PoisonChanceOnCrit = m_min(100, modDB:Sum("BASE", cfg, "PoisonChance"))
|
|
if modDB:Sum("FLAG", cfg, "CannotIgnite") then
|
|
output.IgniteChanceOnCrit = 0
|
|
else
|
|
output.IgniteChanceOnCrit = 100
|
|
end
|
|
if modDB:Sum("FLAG", cfg, "CannotShock") then
|
|
output.ShockChanceOnCrit = 0
|
|
else
|
|
output.ShockChanceOnCrit = 100
|
|
end
|
|
if modDB:Sum("FLAG", cfg, "CannotFreeze") then
|
|
output.FreezeChanceOnCrit = 0
|
|
else
|
|
output.FreezeChanceOnCrit = 100
|
|
end
|
|
condList["CriticalStrike"] = false
|
|
if modDB:Sum("FLAG", cfg, "CannotBleed") then
|
|
output.BleedChanceOnHit = 0
|
|
else
|
|
output.BleedChanceOnHit = m_min(100, modDB:Sum("BASE", cfg, "BleedChance"))
|
|
end
|
|
output.PoisonChanceOnHit = m_min(100, modDB:Sum("BASE", cfg, "PoisonChance"))
|
|
if modDB:Sum("FLAG", cfg, "CannotIgnite") then
|
|
output.IgniteChanceOnHit = 0
|
|
else
|
|
output.IgniteChanceOnHit = m_min(100, modDB:Sum("BASE", cfg, "EnemyIgniteChance") + enemyDB:Sum("BASE", nil, "SelfIgniteChance"))
|
|
end
|
|
if modDB:Sum("FLAG", cfg, "CannotShock") then
|
|
output.ShockChanceOnHit = 0
|
|
else
|
|
output.ShockChanceOnHit = m_min(100, modDB:Sum("BASE", cfg, "EnemyShockChance") + enemyDB:Sum("BASE", nil, "SelfShockChance"))
|
|
end
|
|
if modDB:Sum("FLAG", cfg, "CannotFreeze") then
|
|
output.FreezeChanceOnHit = 0
|
|
else
|
|
output.FreezeChanceOnHit = m_min(100, modDB:Sum("BASE", cfg, "EnemyFreezeChance") + enemyDB:Sum("BASE", nil, "SelfFreezeChance"))
|
|
if modDB:Sum("FLAG", cfg, "CritsDontAlwaysFreeze") then
|
|
output.FreezeChanceOnCrit = output.FreezeChanceOnHit
|
|
end
|
|
end
|
|
|
|
local function calcSecondaryEffectBase(type, sourceHitDmg, sourceCritDmg)
|
|
-- Calculate the inflict chance and base damage of a secondary effect (bleed/poison/ignite/shock/freeze)
|
|
local chanceOnHit, chanceOnCrit = output[type.."ChanceOnHit"], output[type.."ChanceOnCrit"]
|
|
local chanceFromHit = chanceOnHit * (1 - output.CritChance / 100)
|
|
local chanceFromCrit = chanceOnCrit * output.CritChance / 100
|
|
local chance = chanceFromHit + chanceFromCrit
|
|
output[type.."Chance"] = chance
|
|
local baseFromHit = sourceHitDmg * chanceFromHit / (chanceFromHit + chanceFromCrit)
|
|
local baseFromCrit = sourceCritDmg * chanceFromCrit / (chanceFromHit + chanceFromCrit)
|
|
local baseVal = baseFromHit + baseFromCrit
|
|
if breakdown and chance ~= 0 then
|
|
local breakdownChance = breakdown[type.."Chance"] or { }
|
|
breakdown[type.."Chance"] = breakdownChance
|
|
if breakdownChance[1] then
|
|
t_insert(breakdownChance, "")
|
|
end
|
|
if isAttack then
|
|
t_insert(breakdownChance, pass.label..":")
|
|
end
|
|
t_insert(breakdownChance, s_format("Chance on Non-crit: %d%%", chanceOnHit))
|
|
t_insert(breakdownChance, s_format("Chance on Crit: %d%%", chanceOnCrit))
|
|
if chanceOnHit ~= chanceOnCrit then
|
|
t_insert(breakdownChance, "Combined chance:")
|
|
t_insert(breakdownChance, s_format("%d x (1 - %.4f) ^8(chance from non-crits)", chanceOnHit, output.CritChance/100))
|
|
t_insert(breakdownChance, s_format("+ %d x %.4f ^8(chance from crits)", chanceOnCrit, output.CritChance/100))
|
|
t_insert(breakdownChance, s_format("= %.2f", chance))
|
|
end
|
|
end
|
|
if breakdown and baseVal > 0 then
|
|
local breakdownDPS = breakdown[type.."DPS"] or { }
|
|
breakdown[type.."DPS"] = breakdownDPS
|
|
if breakdownDPS[1] then
|
|
t_insert(breakdownDPS, "")
|
|
end
|
|
if isAttack then
|
|
t_insert(breakdownDPS, pass.label..":")
|
|
end
|
|
if sourceHitDmg == sourceCritDmg then
|
|
t_insert(breakdownDPS, "Base damage:")
|
|
t_insert(breakdownDPS, s_format("%.1f ^8(source damage)",sourceHitDmg))
|
|
else
|
|
if baseFromHit > 0 then
|
|
t_insert(breakdownDPS, "Base from Non-crits:")
|
|
t_insert(breakdownDPS, s_format("%.1f ^8(source damage from non-crits)", sourceHitDmg))
|
|
t_insert(breakdownDPS, s_format("x %.3f ^8(portion of instances created by non-crits)", chanceFromHit / (chanceFromHit + chanceFromCrit)))
|
|
t_insert(breakdownDPS, s_format("= %.1f", baseFromHit))
|
|
end
|
|
if baseFromCrit > 0 then
|
|
t_insert(breakdownDPS, "Base from Crits:")
|
|
t_insert(breakdownDPS, s_format("%.1f ^8(source damage from crits)", sourceCritDmg))
|
|
t_insert(breakdownDPS, s_format("x %.3f ^8(portion of instances created by crits)", chanceFromCrit / (chanceFromHit + chanceFromCrit)))
|
|
t_insert(breakdownDPS, s_format("= %.1f", baseFromCrit))
|
|
end
|
|
if baseFromHit > 0 and baseFromCrit > 0 then
|
|
t_insert(breakdownDPS, "Total base damage:")
|
|
t_insert(breakdownDPS, s_format("%.1f + %.1f", baseFromHit, baseFromCrit))
|
|
t_insert(breakdownDPS, s_format("= %.1f", baseVal))
|
|
end
|
|
end
|
|
end
|
|
return baseVal
|
|
end
|
|
|
|
-- Calculate bleeding chance and damage
|
|
if canDeal.Physical and (output.BleedChanceOnHit + output.BleedChanceOnCrit) > 0 then
|
|
local sourceHitDmg = output.PhysicalHitAverage
|
|
local sourceCritDmg = output.PhysicalCritAverage
|
|
local baseVal = calcSecondaryEffectBase("Bleed", sourceHitDmg, sourceCritDmg) * 0.1
|
|
if baseVal > 0 then
|
|
skillFlags.bleed = true
|
|
skillFlags.duration = true
|
|
if not env.mainSkill.bleedCfg then
|
|
env.mainSkill.bleedCfg = {
|
|
slotName = skillCfg.slotName,
|
|
flags = bor(band(skillCfg.flags, ModFlag.SourceMask), ModFlag.Dot, skillData.dotIsSpell and ModFlag.Spell or 0),
|
|
keywordFlags = bor(skillCfg.keywordFlags, KeywordFlag.Bleed)
|
|
}
|
|
end
|
|
local dotCfg = env.mainSkill.bleedCfg
|
|
local effMult = 1
|
|
if env.mode_effective then
|
|
local resist = enemyDB:Sum("INC", nil, "PhysicalDamageReduction")
|
|
local taken = enemyDB:Sum("INC", dotCfg, "DamageTaken", "PhysicalDamageTaken", "DotTaken")
|
|
effMult = (1 - resist / 100) * (1 + taken / 100)
|
|
globalOutput["BleedEffMult"] = effMult
|
|
if breakdown and effMult ~= 1 then
|
|
globalBreakdown.BleedEffMult = effMultBreakdown("Physical", resist, 0, taken, effMult)
|
|
end
|
|
end
|
|
local inc = modDB:Sum("INC", dotCfg, "Damage", "PhysicalDamage")
|
|
local more = round(modDB:Sum("MORE", dotCfg, "Damage", "PhysicalDamage"), 2)
|
|
output.BleedDPS = baseVal * (1 + inc/100) * more * effMult
|
|
local durationMod = calcMod(modDB, dotCfg, "Duration")
|
|
globalOutput.BleedDuration = 5 * durationMod * debuffDurationMult
|
|
if breakdown then
|
|
t_insert(breakdown.BleedDPS, "x 0.1 ^8(bleed deals 10% per second)")
|
|
t_insert(breakdown.BleedDPS, s_format("= %.1f", baseVal))
|
|
t_insert(breakdown.BleedDPS, "Bleed DPS:")
|
|
dotBreakdown(breakdown.BleedDPS, baseVal, inc, more, nil, effMult, output.BleedDPS)
|
|
if globalOutput.BleedDuration ~= 5 then
|
|
globalBreakdown.BleedDuration = {
|
|
"5.00s ^8(base duration)"
|
|
}
|
|
if durationMod ~= 1 then
|
|
t_insert(globalBreakdown.BleedDuration, s_format("x %.2f ^8(duration modifier)", durationMod))
|
|
end
|
|
if debuffDurationMult ~= 1 then
|
|
t_insert(globalBreakdown.BleedDuration, s_format("/ %.2f ^8(debuff expires slower/faster)", 1 / debuffDurationMult))
|
|
end
|
|
t_insert(globalBreakdown.BleedDuration, s_format("= %.2fs", globalOutput.BleedDuration))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Calculate poison chance and damage
|
|
if canDeal.Chaos and (output.PoisonChanceOnHit + output.PoisonChanceOnCrit) > 0 then
|
|
local sourceHitDmg = output.PhysicalHitAverage + output.ChaosHitAverage
|
|
local sourceCritDmg = output.PhysicalCritAverage + output.ChaosCritAverage
|
|
local baseVal = calcSecondaryEffectBase("Poison", sourceHitDmg, sourceCritDmg * modDB:Sum("MORE", cfg, "PoisonDamageOnCrit")) * 0.08
|
|
if baseVal > 0 then
|
|
skillFlags.poison = true
|
|
skillFlags.duration = true
|
|
if not env.mainSkill.poisonCfg then
|
|
env.mainSkill.poisonCfg = {
|
|
slotName = skillCfg.slotName,
|
|
flags = bor(band(skillCfg.flags, ModFlag.SourceMask), ModFlag.Dot, skillData.dotIsSpell and ModFlag.Spell or 0),
|
|
keywordFlags = bor(skillCfg.keywordFlags, KeywordFlag.Poison)
|
|
}
|
|
end
|
|
local dotCfg = env.mainSkill.poisonCfg
|
|
local effMult = 1
|
|
if env.mode_effective then
|
|
local resist = m_min(enemyDB:Sum("BASE", nil, "ChaosResist"), 75)
|
|
local taken = enemyDB:Sum("INC", nil, "DamageTaken", "ChaosDamageTaken", "DotTaken")
|
|
effMult = (1 - resist / 100) * (1 + taken / 100)
|
|
globalOutput["PoisonEffMult"] = effMult
|
|
if breakdown and effMult ~= 1 then
|
|
globalBreakdown.PoisonEffMult = effMultBreakdown("Chaos", resist, 0, taken, effMult)
|
|
end
|
|
end
|
|
local inc = modDB:Sum("INC", dotCfg, "Damage", "ChaosDamage")
|
|
local more = round(modDB:Sum("MORE", dotCfg, "Damage", "ChaosDamage"), 2)
|
|
output.PoisonDPS = baseVal * (1 + inc/100) * more * effMult
|
|
local durationBase
|
|
if skillData.poisonDurationIsSkillDuration then
|
|
durationBase = skillData.duration
|
|
else
|
|
durationBase = 2
|
|
end
|
|
local durationMod = calcMod(modDB, dotCfg, "Duration")
|
|
globalOutput.PoisonDuration = durationBase * durationMod * debuffDurationMult
|
|
output.PoisonDamage = output.PoisonDPS * globalOutput.PoisonDuration
|
|
if env.mode_average then
|
|
output.TotalPoisonAverageDamage = output.HitChance / 100 * output.PoisonChance / 100 * output.PoisonDamage
|
|
else
|
|
output.TotalPoisonDPS = output.HitChance / 100 * output.PoisonChance / 100 * output.PoisonDamage * (globalOutput.HitSpeed or globalOutput.Speed) * (skillData.dpsMultiplier or 1)
|
|
end
|
|
if breakdown then
|
|
t_insert(breakdown.PoisonDPS, "x 0.08 ^8(poison deals 8% per second)")
|
|
t_insert(breakdown.PoisonDPS, s_format("= %.1f", baseVal, 1))
|
|
t_insert(breakdown.PoisonDPS, "Poison DPS:")
|
|
dotBreakdown(breakdown.PoisonDPS, baseVal, inc, more, nil, effMult, output.PoisonDPS)
|
|
if globalOutput.PoisonDuration ~= 2 then
|
|
globalBreakdown.PoisonDuration = {
|
|
s_format("%.2fs ^8(base duration)", durationBase)
|
|
}
|
|
if durationMod ~= 1 then
|
|
t_insert(globalBreakdown.PoisonDuration, s_format("x %.2f ^8(duration modifier)", durationMod))
|
|
end
|
|
if debuffDurationMult ~= 1 then
|
|
t_insert(globalBreakdown.PoisonDuration, s_format("/ %.2f ^8(debuff expires slower/faster)", 1 / debuffDurationMult))
|
|
end
|
|
t_insert(globalBreakdown.PoisonDuration, s_format("= %.2fs", globalOutput.PoisonDuration))
|
|
end
|
|
breakdown.PoisonDamage = { }
|
|
if isAttack then
|
|
t_insert(breakdown.PoisonDamage, pass.label..":")
|
|
end
|
|
t_insert(breakdown.PoisonDamage, s_format("%.1f ^8(damage per second)", output.PoisonDPS))
|
|
t_insert(breakdown.PoisonDamage, s_format("x %.2fs ^8(poison duration)", globalOutput.PoisonDuration))
|
|
t_insert(breakdown.PoisonDamage, s_format("= %.1f ^8damage per poison stack", output.PoisonDamage))
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Calculate ignite chance and damage
|
|
if canDeal.Fire and (output.IgniteChanceOnHit + output.IgniteChanceOnCrit) > 0 then
|
|
local sourceHitDmg = 0
|
|
local sourceCritDmg = 0
|
|
if canDeal.Fire and not modDB:Sum("FLAG", cfg, "FireCannotIgnite") then
|
|
sourceHitDmg = sourceHitDmg + output.FireHitAverage
|
|
sourceCritDmg = sourceCritDmg + output.FireCritAverage
|
|
end
|
|
if canDeal.Cold and modDB:Sum("FLAG", cfg, "ColdCanIgnite") then
|
|
sourceHitDmg = sourceHitDmg + output.ColdHitAverage
|
|
sourceCritDmg = sourceCritDmg + output.ColdCritAverage
|
|
end
|
|
local igniteMode = env.configInput.igniteMode or "AVERAGE"
|
|
if igniteMode == "CRIT" then
|
|
output.IgniteChanceOnHit = 0
|
|
end
|
|
if globalBreakdown then
|
|
globalBreakdown.IgniteDPS = {
|
|
s_format("Ignite mode: %s ^8(can be changed in the Configuration tab)", igniteMode == "CRIT" and "Crit Damage" or "Average Damage")
|
|
}
|
|
end
|
|
local baseVal = calcSecondaryEffectBase("Ignite", sourceHitDmg, sourceCritDmg) * 0.2
|
|
if baseVal > 0 then
|
|
skillFlags.ignite = true
|
|
if not env.mainSkill.igniteCfg then
|
|
env.mainSkill.igniteCfg = {
|
|
slotName = skillCfg.slotName,
|
|
flags = bor(band(skillCfg.flags, ModFlag.SourceMask), ModFlag.Dot, skillData.dotIsSpell and ModFlag.Spell or 0),
|
|
keywordFlags = skillCfg.keywordFlags,
|
|
}
|
|
end
|
|
local dotCfg = env.mainSkill.igniteCfg
|
|
local effMult = 1
|
|
if env.mode_effective then
|
|
local resist = m_min(enemyDB:Sum("BASE", nil, "FireResist", "ElementalResist"), 75)
|
|
local taken = enemyDB:Sum("INC", dotCfg, "DamageTaken", "FireDamageTaken", "ElementalDamageTaken", "BurningDamageTaken", "DotTaken")
|
|
effMult = (1 - resist / 100) * (1 + taken / 100)
|
|
globalOutput["IgniteEffMult"] = effMult
|
|
if breakdown and effMult ~= 1 then
|
|
globalBreakdown.IgniteEffMult = effMultBreakdown("Fire", resist, 0, taken, effMult)
|
|
end
|
|
end
|
|
local inc = modDB:Sum("INC", dotCfg, "Damage", "FireDamage", "ElementalDamage")
|
|
local more = round(modDB:Sum("MORE", dotCfg, "Damage", "FireDamage", "ElementalDamage"), 2)
|
|
local burnRateMod = calcMod(modDB, cfg, "IgniteBurnRate")
|
|
output.IgniteDPS = baseVal * (1 + inc/100) * more * burnRateMod * effMult
|
|
local incDur = modDB:Sum("INC", dotCfg, "EnemyIgniteDuration") + enemyDB:Sum("INC", nil, "SelfIgniteDuration")
|
|
globalOutput.IgniteDuration = 4 * (1 + incDur / 100) / burnRateMod * debuffDurationMult
|
|
if skillFlags.igniteCanStack then
|
|
output.IgniteDamage = output.IgniteDPS * globalOutput.IgniteDuration
|
|
if env.mode_average then
|
|
output.TotalIgniteAverageDamage = output.HitChance / 100 * output.IgniteChance / 100 * output.IgniteDamage
|
|
else
|
|
output.TotalIgniteDPS = output.HitChance / 100 * output.IgniteChance / 100 * output.IgniteDamage * (globalOutput.HitSpeed or globalOutput.Speed) * (skillData.dpsMultiplier or 1)
|
|
end
|
|
end
|
|
if breakdown then
|
|
t_insert(breakdown.IgniteDPS, "x 0.2 ^8(ignite deals 20% per second)")
|
|
t_insert(breakdown.IgniteDPS, s_format("= %.1f", baseVal, 1))
|
|
t_insert(breakdown.IgniteDPS, "Ignite DPS:")
|
|
dotBreakdown(breakdown.IgniteDPS, baseVal, inc, more, burnRateMod, effMult, output.IgniteDPS)
|
|
if skillFlags.igniteCanStack then
|
|
breakdown.IgniteDamage = { }
|
|
if isAttack then
|
|
t_insert(breakdown.IgniteDamage, pass.label..":")
|
|
end
|
|
t_insert(breakdown.IgniteDamage, s_format("%.1f ^8(damage per second)", output.IgniteDPS))
|
|
t_insert(breakdown.IgniteDamage, s_format("x %.2fs ^8(ignite duration)", globalOutput.IgniteDuration))
|
|
t_insert(breakdown.IgniteDamage, s_format("= %.1f ^8damage per ignite stack", output.IgniteDamage))
|
|
end
|
|
if globalOutput.IgniteDuration ~= 4 then
|
|
globalBreakdown.IgniteDuration = {
|
|
s_format("4.00s ^8(base duration)", durationBase)
|
|
}
|
|
if incDur ~= 0 then
|
|
t_insert(globalBreakdown.IgniteDuration, s_format("x %.2f ^8(increased/reduced duration)", 1 + incDur/100))
|
|
end
|
|
if burnRateMod ~= 1 then
|
|
t_insert(globalBreakdown.IgniteDuration, s_format("/ %.2f ^8(rate modifier)", burnRateMod))
|
|
end
|
|
if debuffDurationMult ~= 1 then
|
|
t_insert(globalBreakdown.IgniteDuration, s_format("/ %.2f ^8(debuff expires slower/faster)", 1 / debuffDurationMult))
|
|
end
|
|
t_insert(globalBreakdown.IgniteDuration, s_format("= %.2fs", globalOutput.IgniteDuration))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Calculate shock and freeze chance + duration modifier
|
|
if (output.ShockChanceOnHit + output.ShockChanceOnCrit) > 0 then
|
|
local sourceHitDmg = 0
|
|
local sourceCritDmg = 0
|
|
if canDeal.Lightning and not modDB:Sum("FLAG", cfg, "LightningCannotShock") then
|
|
sourceHitDmg = sourceHitDmg + output.LightningHitAverage
|
|
sourceCritDmg = sourceCritDmg + output.LightningCritAverage
|
|
end
|
|
if canDeal.Physical and modDB:Sum("FLAG", cfg, "PhysicalCanShock") then
|
|
sourceHitDmg = sourceHitDmg + output.PhysicalHitAverage
|
|
sourceCritDmg = sourceCritDmg + output.PhysicalCritAverage
|
|
end
|
|
if canDeal.Fire and modDB:Sum("FLAG", cfg, "FireCanShock") then
|
|
sourceHitDmg = sourceHitDmg + output.FireHitAverage
|
|
sourceCritDmg = sourceCritDmg + output.FireCritAverage
|
|
end
|
|
if canDeal.Chaos and modDB:Sum("FLAG", cfg, "ChaosCanShock") then
|
|
sourceHitDmg = sourceHitDmg + output.ChaosHitAverage
|
|
sourceCritDmg = sourceCritDmg + output.ChaosCritAverage
|
|
end
|
|
local baseVal = calcSecondaryEffectBase("Shock", sourceHitDmg, sourceCritDmg)
|
|
if baseVal > 0 then
|
|
skillFlags.shock = true
|
|
output.ShockDurationMod = 1 + modDB:Sum("INC", cfg, "EnemyShockDuration") / 100 + enemyDB:Sum("INC", nil, "SelfShockDuration") / 100
|
|
if breakdown then
|
|
t_insert(breakdown.ShockDPS, s_format("For shock to apply, target must have no more than %d life.", baseVal * 20 * output.ShockDurationMod))
|
|
end
|
|
end
|
|
end
|
|
if (output.FreezeChanceOnHit + output.FreezeChanceOnCrit) > 0 then
|
|
local sourceHitDmg = 0
|
|
local sourceCritDmg = 0
|
|
if canDeal.Cold and not modDB:Sum("FLAG", cfg, "ColdCannotFreeze") then
|
|
sourceHitDmg = sourceHitDmg + output.ColdHitAverage
|
|
sourceCritDmg = sourceCritDmg + output.ColdCritAverage
|
|
end
|
|
if canDeal.Lightning and modDB:Sum("FLAG", cfg, "LightningCanFreeze") then
|
|
sourceHitDmg = sourceHitDmg + output.LightningHitAverage
|
|
sourceCritDmg = sourceCritDmg + output.LightningCritAverage
|
|
end
|
|
local baseVal = calcSecondaryEffectBase("Freeze", sourceHitDmg, sourceCritDmg)
|
|
if baseVal > 0 then
|
|
skillFlags.freeze = true
|
|
output.FreezeDurationMod = 1 + modDB:Sum("INC", cfg, "EnemyFreezeDuration") / 100 + enemyDB:Sum("INC", nil, "SelfFreezeDuration") / 100
|
|
if breakdown then
|
|
t_insert(breakdown.FreezeDPS, s_format("For freeze to apply, target must have no more than %d life.", baseVal * 20 * output.FreezeDurationMod))
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Calculate enemy stun modifiers
|
|
local enemyStunThresholdRed = -modDB:Sum("INC", cfg, "EnemyStunThreshold")
|
|
if enemyStunThresholdRed > 75 then
|
|
output.EnemyStunThresholdMod = 1 - (75 + (enemyStunThresholdRed - 75) * 25 / (enemyStunThresholdRed - 50)) / 100
|
|
else
|
|
output.EnemyStunThresholdMod = 1 - enemyStunThresholdRed / 100
|
|
end
|
|
local incDur = modDB:Sum("INC", cfg, "EnemyStunDuration")
|
|
local incRecov = enemyDB:Sum("INC", nil, "StunRecovery")
|
|
output.EnemyStunDuration = 0.35 * (1 + incDur / 100) / (1 + incRecov / 100)
|
|
if breakdown then
|
|
if output.EnemyStunDuration ~= 0.35 then
|
|
breakdown.EnemyStunDuration = {
|
|
"0.35s ^8(base duration)"
|
|
}
|
|
if incDur ~= 0 then
|
|
t_insert(breakdown.EnemyStunDuration, s_format("x %.2f ^8(increased/reduced stun duration)", 1 + incDur/100))
|
|
end
|
|
if incRecov ~= 0 then
|
|
t_insert(breakdown.EnemyStunDuration, s_format("/ %.2f ^8(increased/reduced enemy stun recovery)", 1 + incRecov/100))
|
|
end
|
|
t_insert(breakdown.EnemyStunDuration, s_format("= %.2fs", output.EnemyStunDuration))
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
-- Combine secondary effect stats
|
|
if isAttack then
|
|
combineStat("BleedChance", "AVERAGE")
|
|
combineStat("BleedDPS", "CHANCE", "BleedChance")
|
|
combineStat("PoisonChance", "AVERAGE")
|
|
combineStat("PoisonDPS", "CHANCE", "PoisonChance")
|
|
combineStat("PoisonDamage", "CHANCE", "PoisonChance")
|
|
if env.mode_average then
|
|
combineStat("TotalPoisonAverageDamage", "DPS")
|
|
else
|
|
combineStat("TotalPoisonDPS", "DPS")
|
|
end
|
|
combineStat("IgniteChance", "AVERAGE")
|
|
combineStat("IgniteDPS", "CHANCE", "IgniteChance")
|
|
if skillFlags.igniteCanStack then
|
|
combineStat("IgniteDamage", "CHANCE", "IgniteChance")
|
|
if env.mode_average then
|
|
combineStat("TotalIgniteAverageDamage", "DPS")
|
|
else
|
|
combineStat("TotalIgniteDPS", "DPS")
|
|
end
|
|
end
|
|
combineStat("ShockChance", "AVERAGE")
|
|
combineStat("ShockDurationMod", "AVERAGE")
|
|
combineStat("FreezeChance", "AVERAGE")
|
|
combineStat("FreezeDurationMod", "AVERAGE")
|
|
end
|
|
|
|
if skillFlags.hit and skillData.decay then
|
|
-- Calculate DPS for Essence of Delirium's Decay effect
|
|
skillFlags.decay = true
|
|
env.mainSkill.decayCfg = {
|
|
slotName = skillCfg.slotName,
|
|
flags = bor(band(skillCfg.flags, ModFlag.SourceMask), ModFlag.Dot, skillData.dotIsSpell and ModFlag.Spell or 0),
|
|
keywordFlags = skillCfg.keywordFlags,
|
|
}
|
|
local dotCfg = env.mainSkill.decayCfg
|
|
local effMult = 1
|
|
if env.mode_effective then
|
|
local resist = m_min(enemyDB:Sum("BASE", nil, "ChaosResist"), 75)
|
|
local taken = enemyDB:Sum("INC", nil, "DamageTaken", "ChaosDamageTaken", "DotTaken")
|
|
effMult = (1 - resist / 100) * (1 + taken / 100)
|
|
output["DecayEffMult"] = effMult
|
|
if breakdown and effMult ~= 1 then
|
|
breakdown.DecayEffMult = effMultBreakdown("Chaos", resist, 0, taken, effMult)
|
|
end
|
|
end
|
|
local inc = modDB:Sum("INC", dotCfg, "Damage", "ChaosDamage")
|
|
local more = round(modDB:Sum("MORE", dotCfg, "Damage", "ChaosDamage"), 2)
|
|
output.DecayDPS = skillData.decay * (1 + inc/100) * more * effMult
|
|
local durationMod = calcMod(modDB, dotCfg, "Duration")
|
|
output.DecayDuration = 10 * durationMod * debuffDurationMult
|
|
if breakdown then
|
|
breakdown.DecayDPS = { }
|
|
t_insert(breakdown.DecayDPS, "Decay DPS:")
|
|
dotBreakdown(breakdown.DecayDPS, skillData.decay, inc, more, nil, effMult, output.DecayDPS)
|
|
if output.DecayDuration ~= 2 then
|
|
breakdown.DecayDuration = {
|
|
s_format("%.2fs ^8(base duration)", 10)
|
|
}
|
|
if durationMod ~= 1 then
|
|
t_insert(breakdown.DecayDuration, s_format("x %.2f ^8(duration modifier)", durationMod))
|
|
end
|
|
if debuffDurationMult ~= 1 then
|
|
t_insert(breakdown.DecayDuration, s_format("/ %.2f ^8(debuff expires slower/faster)", 1 / debuffDurationMult))
|
|
end
|
|
t_insert(breakdown.DecayDuration, s_format("= %.2fs", output.DecayDuration))
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Calculate combined DPS estimate, including DoTs
|
|
local baseDPS = output[(env.mode_average and "AverageDamage") or "TotalDPS"] + output.TotalDot
|
|
output.CombinedDPS = baseDPS
|
|
if skillFlags.poison then
|
|
if env.mode_average then
|
|
output.CombinedDPS = output.CombinedDPS + output.TotalPoisonAverageDamage
|
|
output.WithPoisonAverageDamage = baseDPS + output.TotalPoisonAverageDamage
|
|
else
|
|
output.CombinedDPS = output.CombinedDPS + output.TotalPoisonDPS
|
|
output.WithPoisonDPS = baseDPS + output.TotalPoisonDPS
|
|
end
|
|
end
|
|
if skillFlags.ignite then
|
|
if skillFlags.igniteCanStack then
|
|
if env.mode_average then
|
|
output.CombinedDPS = output.CombinedDPS + output.TotalIgniteAverageDamage
|
|
output.WithIgniteAverageDamage = baseDPS + output.TotalIgniteAverageDamage
|
|
else
|
|
output.CombinedDPS = output.CombinedDPS + output.TotalIgniteDPS
|
|
output.WithIgniteDPS = baseDPS + output.TotalIgniteDPS
|
|
end
|
|
else
|
|
output.CombinedDPS = output.CombinedDPS + output.IgniteDPS
|
|
end
|
|
end
|
|
if skillFlags.bleed then
|
|
output.CombinedDPS = output.CombinedDPS + output.BleedDPS
|
|
end
|
|
if skillFlags.decay then
|
|
output.CombinedDPS = output.CombinedDPS + output.DecayDPS
|
|
end
|
|
end
|
|
|
|
-- Print various tables to the console
|
|
local function infoDump(env, output)
|
|
env.modDB:Print()
|
|
ConPrintf("=== Enemy Mod DB ===")
|
|
env.enemyDB:Print()
|
|
ConPrintf("=== Main Skill ===")
|
|
for _, gem in ipairs(env.mainSkill.gemList) do
|
|
ConPrintf("%s %d/%d", gem.name, gem.level, gem.quality)
|
|
end
|
|
ConPrintf("=== Main Skill Flags ===")
|
|
ConPrintf("Mod: %s", modLib.formatFlags(env.mainSkill.skillCfg.flags, ModFlag))
|
|
ConPrintf("Keyword: %s", modLib.formatFlags(env.mainSkill.skillCfg.keywordFlags, KeywordFlag))
|
|
ConPrintf("=== Main Skill Mods ===")
|
|
env.mainSkill.skillModList:Print()
|
|
ConPrintf("== Aux Skills ==")
|
|
for i, aux in ipairs(env.auxSkillList) do
|
|
ConPrintf("Skill #%d:", i)
|
|
for _, gem in ipairs(aux.gemList) do
|
|
ConPrintf(" %s %d/%d", gem.name, gem.level, gem.quality)
|
|
end
|
|
end
|
|
-- ConPrintf("== Conversion Table ==")
|
|
-- ConPrintTable(env.conversionTable)
|
|
ConPrintf("== Output Table ==")
|
|
local outNames = { }
|
|
for name in pairs(env.output) do
|
|
t_insert(outNames, name)
|
|
end
|
|
table.sort(outNames)
|
|
for _, name in ipairs(outNames) do
|
|
if type(env.output[name]) == "table" then
|
|
local subNames = { }
|
|
for subName in pairs(env.output[name]) do
|
|
t_insert(subNames, subName)
|
|
end
|
|
table.sort(subNames)
|
|
for _, subName in ipairs(subNames) do
|
|
ConPrintf("%s.%s = %s", name, subName, tostring(env.output[name][subName]))
|
|
end
|
|
else
|
|
ConPrintf("%s = %s", name, tostring(env.output[name]))
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Generate a function for calculating the effect of some modification to the environment
|
|
local function getCalculator(build, fullInit, modFunc)
|
|
-- Initialise environment
|
|
local env = initEnv(build, "CALCULATOR")
|
|
|
|
-- Save a copy of the initial mod database
|
|
local initModDB = common.New("ModDB")
|
|
initModDB:AddDB(env.modDB)
|
|
initModDB.conditions = copyTable(env.modDB.conditions)
|
|
initModDB.multipliers = copyTable(env.modDB.multipliers)
|
|
local initEnemyDB = common.New("ModDB")
|
|
initEnemyDB:AddDB(env.enemyDB)
|
|
initEnemyDB.conditions = copyTable(env.enemyDB.conditions)
|
|
initEnemyDB.multipliers = copyTable(env.enemyDB.multipliers)
|
|
|
|
-- Run base calculation pass
|
|
performCalcs(env)
|
|
local baseOutput = env.output
|
|
|
|
return function(...)
|
|
-- Restore initial mod database
|
|
env.modDB.mods = wipeTable(env.modDB.mods)
|
|
env.modDB:AddDB(initModDB)
|
|
env.modDB.conditions = copyTable(initModDB.conditions)
|
|
env.modDB.multipliers = copyTable(initModDB.multipliers)
|
|
env.enemyDB.mods = wipeTable(env.enemyDB.mods)
|
|
env.enemyDB:AddDB(initEnemyDB)
|
|
env.enemyDB.conditions = copyTable(initEnemyDB.conditions)
|
|
env.enemyDB.multipliers = copyTable(initEnemyDB.multipliers)
|
|
|
|
-- Call function to make modifications to the enviroment
|
|
modFunc(env, ...)
|
|
|
|
-- Run calculation pass
|
|
performCalcs(env)
|
|
|
|
return env.output
|
|
end, baseOutput
|
|
end
|
|
|
|
local calcs = { }
|
|
|
|
-- Get fast calculator for adding tree node modifiers
|
|
function calcs.getNodeCalculator(build)
|
|
return getCalculator(build, true, function(env, nodeList)
|
|
-- Build and merge modifiers for these nodes
|
|
env.modDB:AddList(buildNodeModList(env, nodeList))
|
|
--[[local nodeModList = buildNodeModList(env, nodeList)
|
|
if remove then
|
|
for _, mod in ipairs(nodeModList) do
|
|
if mod.type == "LIST" or mod.type == "FLAG" then
|
|
for i, dbMod in ipairs(env.modDB.mods[mod.name] or { }) do
|
|
if mod == dbMod then
|
|
t_remove(env.modDB.mods[mod.name], i)
|
|
break
|
|
end
|
|
end
|
|
elseif mod.type == "MORE" then
|
|
env.modDB:NewMod(mod.name, mod.type, (1 / (1 + mod.value / 100) - 1) * 100, mod.source, mod.flags, mod.keywordFlags, unpack(mod.tagList))
|
|
else
|
|
env.modDB:NewMod(mod.name, mod.type, -mod.value, mod.source, mod.flags, mod.keywordFlags, unpack(mod.tagList))
|
|
end
|
|
end
|
|
else
|
|
env.modDB:AddList(nodeModList)
|
|
end]]
|
|
end)
|
|
end
|
|
|
|
-- Get calculator for other changes (adding/removing nodes, items, gems, etc)
|
|
function calcs.getMiscCalculator(build)
|
|
-- Run base calculation pass
|
|
local env = initEnv(build, "CALCULATOR")
|
|
performCalcs(env)
|
|
local baseOutput = env.output
|
|
|
|
return function(override)
|
|
env = initEnv(build, "CALCULATOR", override)
|
|
performCalcs(env)
|
|
return env.output
|
|
end, baseOutput
|
|
end
|
|
|
|
-- Build output for display in the side bar or calcs tab
|
|
function calcs.buildOutput(build, mode)
|
|
-- Build output
|
|
local env = initEnv(build, mode)
|
|
performCalcs(env)
|
|
|
|
local output = env.output
|
|
|
|
if mode == "MAIN" then
|
|
output.ExtraPoints = env.modDB:Sum("BASE", nil, "ExtraPoints")
|
|
|
|
local specCfg = {
|
|
source = "Tree"
|
|
}
|
|
for _, stat in pairs({"Life", "Mana", "Armour", "Evasion", "EnergyShield"}) do
|
|
output["Spec:"..stat.."Inc"] = env.modDB:Sum("INC", specCfg, stat)
|
|
end
|
|
|
|
env.conditionsUsed = { }
|
|
local function addCond(var, mod)
|
|
if not env.conditionsUsed[var] then
|
|
env.conditionsUsed[var] = { }
|
|
end
|
|
t_insert(env.conditionsUsed[var], mod)
|
|
end
|
|
for _, db in ipairs{env.modDB, env.enemyDB} do
|
|
for modName, modList in pairs(db.mods) do
|
|
for _, mod in ipairs(modList) do
|
|
for _, tag in ipairs(mod.tagList) do
|
|
if tag.type == "Condition" then
|
|
if tag.varList then
|
|
for _, var in ipairs(tag.varList) do
|
|
addCond(var, mod)
|
|
end
|
|
else
|
|
addCond(tag.var, mod)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
elseif mode == "CALCS" then
|
|
local buffList = { }
|
|
local combatList = { }
|
|
local curseList = { }
|
|
if output.PowerCharges > 0 then
|
|
t_insert(combatList, s_format("%d Power Charges", output.PowerCharges))
|
|
end
|
|
if output.FrenzyCharges > 0 then
|
|
t_insert(combatList, s_format("%d Frenzy Charges", output.FrenzyCharges))
|
|
end
|
|
if output.EnduranceCharges > 0 then
|
|
t_insert(combatList, s_format("%d Endurance Charges", output.EnduranceCharges))
|
|
end
|
|
if env.modDB.conditions.Onslaught then
|
|
t_insert(combatList, "Onslaught")
|
|
end
|
|
if env.modDB.conditions.UnholyMight then
|
|
t_insert(combatList, "Unholy Might")
|
|
end
|
|
for _, activeSkill in ipairs(env.activeSkillList) do
|
|
if activeSkill.buffSkill then
|
|
if activeSkill.skillFlags.multiPart then
|
|
t_insert(buffList, activeSkill.activeGem.name .. " (" .. activeSkill.skillPartName .. ")")
|
|
else
|
|
t_insert(buffList, activeSkill.activeGem.name)
|
|
end
|
|
end
|
|
if activeSkill.debuffSkill then
|
|
if activeSkill.skillFlags.multiPart then
|
|
t_insert(curseList, activeSkill.activeGem.name .. " (" .. activeSkill.skillPartName .. ")")
|
|
else
|
|
t_insert(curseList, activeSkill.activeGem.name)
|
|
end
|
|
end
|
|
end
|
|
for _, value in ipairs(env.modDB:Sum("LIST", nil, "ExtraCurse")) do
|
|
t_insert(curseList, value.name)
|
|
end
|
|
output.BuffList = table.concat(buffList, ", ")
|
|
output.CombatList = table.concat(combatList, ", ")
|
|
output.CurseList = table.concat(curseList, ", ")
|
|
|
|
infoDump(env)
|
|
end
|
|
|
|
return env
|
|
end
|
|
|
|
return calcs |