Files
PathOfBuilding/Modules/Calcs.lua
Openarl e246d67519 Release 1.3.23
- Added item enchanting system
- Added support for more helmet enchantments
- Added support for The Wise Oak's penetration
- Corrected Lycosidae's base
- Fixed issue with Blood Rage's quality bonus and the new buff handling code
2017-03-31 18:44:03 +10:00

3424 lines
129 KiB
Lua

-- Path of Building
--
-- Module: Calcs
-- Performs all the offense and defense calculations.
-- Here be dragons!
-- This file is 3400 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)
activeSkill.skillData = { }
-- 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] or skillFlags.forceMainHand 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] and not skillFlags.forceMainHand 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 = { },
skillDist = env.buffMode == "EFFECTIVE" and env.configInput.projectileDistance,
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)
-- Add extra modifiers
for _, value in ipairs(env.modDB:Sum("LIST", activeSkill.skillCfg, "ExtraSkillMod")) do
skillModList:AddMod(value.mod)
end
-- Extract skill data
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
local sig = modLib.formatModParams(skillModList[i])
for d = 1, #activeSkill[destList] do
local destMod = activeSkill[destList][d]
if sig == modLib.formatModParams(destMod) and (destMod.type == "BASE" or destMod.type == "INC") then
destMod.value = destMod.value + skillModList[i].value
sig = nil
break
end
end
if sig then
t_insert(activeSkill[destList], skillModList[i])
end
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, rad.attributes)
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
-- Merge an instance of a buff, taking the highest value of each modifier
local function mergeBuff(src, destTable, destKey)
if not destTable[destKey] then
destTable[destKey] = { }
end
local dest = destTable[destKey]
for _, mod in ipairs(src) do
local param = modLib.formatModParams(mod)
for index, destMod in ipairs(dest) do
if param == modLib.formatModParams(destMod) then
if type(destMod.value) == "number" and mod.value > destMod.value then
dest[index] = mod
end
param = nil
break
end
end
if param then
t_insert(dest, mod)
end
end
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("MaxLifeLeechRate", "BASE", 20, "Base")
modDB:NewMod("MaxManaLeechRate", "BASE", 20, "Base")
modDB:NewMod("ActiveTrapLimit", "BASE", 3, "Base")
modDB:NewMod("ActiveMineLimit", "BASE", 5, "Base")
modDB:NewMod("ActiveTotemLimit", "BASE", 1, "Base")
modDB:NewMod("EnemyCurseLimit", "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,
attributes = node.attributesInRadius[item.jewelRadiusIndex],
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" or item.rarity == "RELIC" 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 2100 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")
local flaskBuffs = { }
for item in pairs(env.flasks) do
modDB.conditions["UsingFlask"] = true
-- Avert thine eyes, lest they be forever scarred
-- I have no idea how to determine which buff is applied by a given flask,
-- so utility flasks are grouped by base, unique flasks are grouped by name, and magic flasks by their modifiers
local effectMod = 1 + (effectInc + item.flaskData.effectInc) / 100
if item.buffModList[1] then
local srcList = common.New("ModList")
srcList:ScaleAddList(item.buffModList, effectMod)
mergeBuff(srcList, flaskBuffs, item.baseName)
end
if item.modList[1] then
local srcList = common.New("ModList")
srcList:ScaleAddList(item.modList, effectMod)
local key
if item.rarity == "UNIQUE" then
key = item.title
else
key = ""
for _, mod in ipairs(item.modList) do
key = key .. modLib.formatModParams(mod) .. "&"
end
end
mergeBuff(srcList, flaskBuffs, key)
end
end
for _, buffModList in pairs(flaskBuffs) do
modDB:AddList(buffModList)
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
if env.weaponData1.type == "Claw" and env.weaponData2.type == "Claw" then
condList["DualWieldingClaws"] = true
end
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 not env.mainSkill.skillData.triggered then
if env.mainSkill.skillFlags.attack then
condList["AttackedRecently"] = true
elseif env.mainSkill.skillFlags.spell then
condList["CastSpellRecently"] = true
end
end
if env.mainSkill.skillFlags.hit and 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 modifiers to apply to aura skills
local extraAuraModList = { }
for _, value in ipairs(modDB:Sum("LIST", nil, "ExtraAuraEffect")) do
t_insert(extraAuraModList, value.mod)
end
-- Combine buffs/debuffs and calculate skill life and mana reservations
local buffs = { }
local debuffs = { }
local curses = { }
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
-- Combine buffs/debuffs
if env.mode_buffs then
if activeSkill.buffModList and
not activeSkill.skillFlags.curse 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 srcList = common.New("ModList")
local inc = modDB:Sum("INC", skillCfg, "BuffEffect")
local more = modDB:Sum("MORE", skillCfg, "BuffEffect")
srcList:ScaleAddList(activeSkill.buffModList, (1 + inc / 100) * more)
mergeBuff(srcList, buffs, activeSkill.activeGem.name)
end
if activeSkill.auraModList and not activeSkill.skillData.auraCannotAffectSelf then
activeSkill.buffSkill = true
local srcList = common.New("ModList")
local inc = modDB:Sum("INC", skillCfg, "AuraEffect", "BuffEffect") + skillModList:Sum("INC", skillCfg, "AuraEffect", "BuffEffect")
local more = modDB:Sum("MORE", skillCfg, "AuraEffect", "BuffEffect") * skillModList:Sum("MORE", skillCfg, "AuraEffect", "BuffEffect")
srcList:ScaleAddList(activeSkill.auraModList, (1 + inc / 100) * more)
srcList:ScaleAddList(extraAuraModList, (1 + inc / 100) * more)
mergeBuff(srcList, buffs, activeSkill.activeGem.name)
condList["HaveAuraActive"] = true
end
end
if env.mode_effective then
if activeSkill.debuffModList then
activeSkill.debuffSkill = true
local srcList = common.New("ModList")
srcList:ScaleAddList(activeSkill.debuffModList, activeSkill.skillData.stackCount or 1)
mergeBuff(srcList, debuffs, activeSkill.activeGem.name)
end
if activeSkill.curseModList or (activeSkill.skillFlags.curse and activeSkill.buffModList) then
local curse = {
name = activeSkill.activeGem.name,
priority = activeSkill.skillTypes[SkillType.Aura] and 3 or 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")
if activeSkill.curseModList then
curse.modList = common.New("ModList")
curse.modList:ScaleAddList(activeSkill.curseModList, (1 + inc / 100) * more)
end
if activeSkill.buffModList then
-- Curse applies a buff; scale by curse effect, then buff effect
local temp = common.New("ModList")
temp:ScaleAddList(activeSkill.buffModList, (1 + inc / 100) * more)
curse.buffModList = common.New("ModList")
local buffInc = modDB:Sum("INC", skillCfg, "BuffEffect")
local buffMore = modDB:Sum("MORE", skillCfg, "BuffEffect")
curse.buffModList:ScaleAddList(temp, (1 + buffInc / 100) * buffMore)
end
t_insert(curses, curse)
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
-- Check for extra curses
for _, value in ipairs(modDB:Sum("LIST", nil, "ExtraCurse")) do
local curse = {
name = value.name,
priority = 2,
modList = common.New("ModList")
}
local gemModList = common.New("ModList")
mergeGemMods(gemModList, {
level = value.level,
quality = 0,
data = data.gems[value.name],
})
local curseModList = { }
for _, mod in ipairs(gemModList) do
for _, tag in ipairs(mod.tagList) do
if tag.type == "GlobalEffect" and tag.effectType == "Curse" then
t_insert(curseModList, mod)
break
end
end
end
curse.modList:ScaleAddList(curseModList, (1 + enemyDB:Sum("INC", nil, "CurseEffect") / 100) * enemyDB:Sum("MORE", nil, "CurseEffect"))
t_insert(curses, curse)
end
-- Assign curses to slots
local curseSlots = { }
env.curseSlots = curseSlots
output.EnemyCurseLimit = modDB:Sum("BASE", nil, "EnemyCurseLimit")
for _, curse in ipairs(curses) do
local slot
for i = 1, output.EnemyCurseLimit do
if not curseSlots[i] then
slot = i
break
elseif curseSlots[i].name == curse.name then
if curseSlots[i].priority < curse.priority then
slot = i
else
slot = nil
end
break
elseif curseSlots[i].priority < curse.priority then
slot = i
end
end
if slot then
curseSlots[slot] = curse
end
end
-- Merge buff/debuff modifiers
for _, modList in pairs(buffs) do
modDB:AddList(modList)
end
for _, modList in pairs(debuffs) do
enemyDB:AddList(modList)
end
modDB.multipliers["CurseOnEnemy"] = #curseSlots
for _, slot in ipairs(curseSlots) do
condList["EnemyCursed"] = true
if slot.modList then
enemyDB:AddList(slot.modList)
end
if slot.buffModList then
modDB:AddList(slot.buffModList)
end
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 == 0 then
condList["HaveNoPowerCharges"] = true
end
if output.PowerCharges == output.PowerChargesMax then
condList["AtMaxPowerCharges"] = true
end
if output.FrenzyCharges == 0 then
condList["HaveNoFrenzyCharges"] = true
end
if output.FrenzyCharges == output.FrenzyChargesMax then
condList["AtMaxFrenzyCharges"] = true
end
if output.EnduranceCharges == 0 then
condList["HaveNoEnduranceCharges"] = true
end
if output.EnduranceCharges == output.EnduranceChargesMax then
condList["AtMaxEnduranceCharges"] = true
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
-- 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
condList.UncappedLightningResistIsLowest = (output.LightningResistTotal <= output.ColdResistTotal and output.LightningResistTotal <= output.FireResistTotal)
condList.UncappedColdResistIsLowest = (output.ColdResistTotal <= output.LightningResistTotal and output.ColdResistTotal <= output.FireResistTotal)
condList.UncappedFireResistIsLowest = (output.FireResistTotal <= output.LightningResistTotal and output.FireResistTotal <= output.ColdResistTotal)
condList.UncappedLightningResistIsHighest = (output.LightningResistTotal >= output.ColdResistTotal and output.LightningResistTotal >= output.FireResistTotal)
condList.UncappedColdResistIsHighest = (output.ColdResistTotal >= output.LightningResistTotal and output.ColdResistTotal >= output.FireResistTotal)
condList.UncappedFireResistIsHighest = (output.FireResistTotal >= output.LightningResistTotal and output.FireResistTotal >= output.ColdResistTotal)
-- 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(output.LifeRegen / 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(output.EnergyShieldRegen / output.EnergyShield * 100, 1)
else
output.EnergyShieldRegen = 0
end
end
-- Leech caps
if modDB:Sum("FLAG", nil, "GhostReaver") then
output.MaxEnergyShieldLeechRate = output.EnergyShield * modDB:Sum("BASE", nil, "MaxLifeLeechRate") / 100
else
output.MaxLifeLeechRate = output.Life * modDB:Sum("BASE", nil, "MaxLifeLeechRate") / 100
end
output.MaxManaLeechRate = output.Mana * modDB:Sum("BASE", nil, "MaxManaLeechRate") / 100
-- 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
if modDB:Sum("FLAG", nil, "PointBlank") then
modDB:NewMod("Damage", "MORE", 50, "Point Blank", bor(ModFlag.Attack, ModFlag.Projectile), { type = "DistanceRamp", ramp = {{10,1},{35,0},{150,-1}} })
end
output.ProjectileCount = modDB:Sum("BASE", skillCfg, "ProjectileCount")
output.PierceChance = m_min(100, modDB:Sum("BASE", skillCfg, "PierceChance"))
output.ProjectileSpeedMod = calcMod(modDB, skillCfg, "ProjectileSpeed")
if breakdown then
breakdown.ProjectileSpeedMod = modBreakdown(skillCfg, "ProjectileSpeed")
end
end
if skillFlags.area then
output.AreaOfEffectMod = calcMod(modDB, skillCfg, "AreaOfEffect")
if breakdown then
breakdown.AreaOfEffectMod = modBreakdown(skillCfg, "AreaOfEffect")
end
end
if skillFlags.trap then
output.ActiveTrapLimit = modDB:Sum("BASE", skillCfg, "ActiveTrapLimit")
output.TrapCooldown = (skillData.trapCooldown or 4) / calcMod(modDB, skillCfg, "CooldownRecovery")
if breakdown then
breakdown.TrapCooldown = {
s_format("%.2fs ^8(base)", skillData.trapCooldown or 4),
s_format("/ %.2f ^8(increased/reduced cooldown recovery)", 1 + modDB:Sum("INC", skillCfg, "CooldownRecovery") / 100),
s_format("= %.2fs", output.TrapCooldown)
}
end
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.PreEffectiveCritChance = 0
output.CritChance = 0
output.CritMultiplier = 0
output.CritEffect = 1
else
local baseCrit = source.critChance or 0
if baseCrit == 100 then
output.PreEffectiveCritChance = 100
output.CritChance = 100
else
local base = modDB:Sum("BASE", cfg, "CritChance")
local inc = modDB:Sum("INC", cfg, "CritChance")
local more = modDB:Sum("MORE", cfg, "CritChance")
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
output.PreEffectiveCritChance = output.CritChance
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)
local lifeLeechTotal = 0
local manaLeechTotal = 0
for _, damageType in ipairs(dmgTypeList) do
local min, max
if skillFlags.hit and canDeal[damageType] then
if breakdown then
breakdown[damageType] = {
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
local lifeLeech = modDB:Sum("BASE", cfg, "DamageLifeLeech", damageType.."LifeLeech", isElemental[damageType] and "ElementalLifeLeech" or nil) + enemyDB:Sum("BASE", nil, "SelfDamageLifeLeech") / 100
if lifeLeech > 0 then
lifeLeechTotal = lifeLeechTotal + (min + max) / 2 * lifeLeech / 100
end
local manaLeech = modDB:Sum("BASE", cfg, "DamageManaLeech", damageType.."ManaLeech", isElemental[damageType] and "ElementalManaLeech" or nil) + enemyDB:Sum("BASE", nil, "SelfDamageManaLeech") / 100
if manaLeech > 0 then
manaLeechTotal = manaLeechTotal + (min + max) / 2 * manaLeech / 100
end
else
min, max = 0, 0
if breakdown then
breakdown[damageType] = {
"You can't deal "..damageType.." damage"
}
end
end
if pass == 1 then
output[damageType.."CritAverage"] = (min + max) / 2
totalCritMin = totalCritMin + min
totalCritMax = totalCritMax + max
else
if env.mode == "CALCS" then
output[damageType.."Min"] = min
output[damageType.."Max"] = max
end
output[damageType.."HitAverage"] = (min + max) / 2
totalHitMin = totalHitMin + min
totalHitMax = totalHitMax + max
end
end
if pass == 1 then
if modDB:Sum("FLAG", cfg, "InstantLifeLeech") then
output.LifeLeechInstant = (output.LifeLeechInstant or 0) + lifeLeechTotal * output.CritChance / 100
else
output.LifeLeech = (output.LifeLeech or 0) + lifeLeechTotal * output.CritChance / 100
end
if modDB:Sum("FLAG", cfg, "InstantManaLeech") then
output.ManaLeechInstant = (output.ManaLeechInstant or 0) + manaLeechTotal * output.CritChance / 100
else
output.ManaLeech = (output.ManaLeech or 0) + manaLeechTotal * output.CritChance / 100
end
else
if modDB:Sum("FLAG", cfg, "InstantLifeLeech") then
output.LifeLeechInstant = (output.LifeLeechInstant or 0) + lifeLeechTotal * (1 - output.CritChance / 100)
else
output.LifeLeech = (output.LifeLeech or 0) + lifeLeechTotal * (1 - output.CritChance / 100)
end
if modDB:Sum("FLAG", cfg, "InstantManaLeech") then
output.ManaLeechInstant = (output.ManaLeechInstant or 0) + manaLeechTotal * (1 - output.CritChance / 100)
else
output.ManaLeech = (output.ManaLeech or 0) + manaLeechTotal * (1 - output.CritChance / 100)
end
end
end
output.TotalMin = totalHitMin
output.TotalMax = totalHitMax
if not env.configInput.EEIgnoreHitDamage and (output.FireHitAverage + output.ColdHitAverage + output.LightningHitAverage > 0) then
-- Update enemy hit-by-damage-type conditions
enemyDB.conditions.HitByFireDamage = output.FireHitAverage > 0
enemyDB.conditions.HitByColdDamage = output.ColdHitAverage > 0
enemyDB.conditions.HitByLightningDamage = output.LightningHitAverage > 0
end
-- Calculate leech
local hitRate = output.HitChance / 100 * (globalOutput.HitSpeed or globalOutput.Speed) * (skillData.dpsMultiplier or 1)
output.LifeLeechDuration = (output.LifeLeech or 0) / (modDB:Sum("FLAG", nil, "GhostReaver") and globalOutput.EnergyShield or globalOutput.Life) / 0.02
output.LifeLeechInstances = output.LifeLeechDuration * hitRate
output.LifeLeechInstantRate = (output.LifeLeechInstant or 0) * hitRate
output.ManaLeechDuration = (output.ManaLeech or 0) / globalOutput.Mana / 0.02
output.ManaLeechInstances = output.ManaLeechDuration * hitRate
output.ManaLeechInstantRate = (output.ManaLeechInstant or 0) * hitRate
-- Calculate average damage and final DPS
output.AverageHit = (totalHitMin + totalHitMax) / 2 * (1 - output.CritChance / 100) + (totalCritMin + totalCritMax) / 2 * output.CritChance / 100
output.AverageDamage = output.AverageHit * output.HitChance / 100
output.TotalDPS = output.AverageDamage * (globalOutput.HitSpeed or globalOutput.Speed) * (skillData.dpsMultiplier or 1)
if breakdown then
if output.CritEffect ~= 1 then
breakdown.AverageHit = {
s_format("%.1f x (1 - %.4f) ^8(damage from non-crits)", (totalHitMin + totalHitMax) / 2, output.CritChance / 100),
s_format("+ %.1f x %.4f ^8(damage from crits)", (totalCritMin + totalCritMax) / 2, output.CritChance / 100),
s_format("= %.1f", output.AverageHit),
}
end
if isAttack then
breakdown.AverageDamage = { }
t_insert(breakdown.AverageDamage, s_format("%s:", pass.label))
t_insert(breakdown.AverageDamage, s_format("%.1f ^8(average hit)", output.AverageHit))
t_insert(breakdown.AverageDamage, s_format("x %.2f ^8(chance to hit)", output.HitChance / 100))
t_insert(breakdown.AverageDamage, s_format("= %.1f", output.AverageDamage))
end
end
end
if isAttack then
-- Combine crit stats, average damage and DPS
combineStat("PreEffectiveCritChance", "AVERAGE")
combineStat("CritChance", "AVERAGE")
combineStat("CritMultiplier", "AVERAGE")
combineStat("AverageDamage", "DPS")
combineStat("TotalDPS", "DPS")
combineStat("LifeLeechDuration", "DPS")
combineStat("LifeLeechInstances", "DPS")
combineStat("LifeLeechInstantRate", "DPS")
combineStat("ManaLeechDuration", "DPS")
combineStat("ManaLeechInstances", "DPS")
combineStat("ManaLeechInstantRate", "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 leech rates
if modDB:Sum("FLAG", nil, "GhostReaver") then
output.EnergyShieldLeechRate = output.LifeLeechInstantRate + m_min(output.LifeLeechInstances * output.EnergyShield * 0.02 * calcMod(modDB, skillCfg, "LifeLeechRate"), output.MaxEnergyShieldLeechRate)
else
output.LifeLeechRate = output.LifeLeechInstantRate + m_min(output.LifeLeechInstances * output.Life * 0.02 * calcMod(modDB, skillCfg, "LifeLeechRate"), output.MaxLifeLeechRate)
end
output.ManaLeechRate = output.ManaLeechInstantRate + m_min(output.ManaLeechInstances * output.Mana * 0.02 * calcMod(modDB, skillCfg, "ManaLeechRate"), output.MaxManaLeechRate)
skillFlags.leechES = (output.EnergyShieldLeechRate or 0) > 0
skillFlags.leechLife = (output.LifeLeechRate or 0) > 0
skillFlags.leechMana = output.ManaLeechRate > 0
if breakdown then
local function leechBreakdown(instant, instances, pool, rate, max, dur)
local out = { }
if instant > 0 then
t_insert(out, s_format("Instant Leech per second: %.1f", instant))
end
if instances > 0 then
t_insert(out, "Rate per instance:")
t_insert(out, s_format("%d ^8(size of leech destination pool)", pool))
t_insert(out, "x 0.02 ^8(base leech rate is 2% per second)")
local rateMod = calcMod(modDB, skillCfg, rate)
if rateMod ~= 1 then
t_insert(out, s_format("x %.2f ^8(leech rate modifier)", rateMod))
end
t_insert(out, s_format("= %.1f ^8per second", pool * 0.02 * rateMod))
t_insert(out, "Maximum leech rate against one target:")
t_insert(out, s_format("%.1f", pool * 0.02 * rateMod))
t_insert(out, s_format("x %.1f ^8(average instances)", instances))
local total = pool * 0.02 * rateMod * instances
t_insert(out, s_format("= %.1f ^8per second", total))
if total <= max then
t_insert(out, s_format("Time to reach max: %.1fs", dur))
end
t_insert(out, s_format("Leech rate cap: %.1f", max))
if total > max then
t_insert(out, s_format("Time to reach cap: %.1fs", dur / total * max))
end
end
return out
end
if skillFlags.leechES then
breakdown.EnergyShieldLeechRate = leechBreakdown(output.LifeLeechInstantRate, output.LifeLeechInstances, output.EnergyShield, "LifeLeechRate", output.MaxEnergyShieldLeechRate, output.LifeLeechDuration)
end
if skillFlags.leechLife then
breakdown.LifeLeechRate = leechBreakdown(output.LifeLeechInstantRate, output.LifeLeechInstances, output.Life, "LifeLeechRate", output.MaxLifeLeechRate, output.LifeLeechDuration)
end
if skillFlags.leechMana then
breakdown.ManaLeechRate = leechBreakdown(output.ManaLeechInstantRate, output.ManaLeechInstances, output.Mana, "ManaLeechRate", output.MaxManaLeechRate, output.ManaLeechDuration)
end
end
-- Calculate skill DOT components
local dotCfg = {
skillName = skillCfg.skillName,
skillPart = skillCfg.skillPart,
slotName = skillCfg.slotName,
flags = bor(band(skillCfg.flags, ModFlag.SourceMask), ModFlag.Dot, skillData.dotIsSpell and ModFlag.Spell or 0, skillData.dotIsArea and ModFlag.Area or 0),
keywordFlags = skillCfg.keywordFlags
}
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", skillCfg, "IgniteCanStack")
skillFlags.shock = false
skillFlags.freeze = false
for _, pass in ipairs(passList) do
local globalOutput, globalBreakdown = output, breakdown
local source, output, cfg, breakdown = pass.source, pass.output, pass.cfg, pass.breakdown
-- Calculate chance to inflict secondary dots/status effects
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
if skillFlags.attack and skillFlags.projectile and modDB:Sum("FLAG", cfg, "ArrowsThatPierceCauseBleeding") then
output.BleedChanceOnHit = 100 - (1 - output.BleedChanceOnHit / 100) * (1 - globalOutput.PierceChance / 100) * 100
output.BleedChanceOnCrit = 100 - (1 - output.BleedChanceOnCrit / 100) * (1 - globalOutput.PierceChance / 100) * 100
end
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") * calcMod(enemyDB, nil, "SelfBleedDuration")
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") * calcMod(enemyDB, nil, "SelfPoisonDuration")
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")
local moreDur = enemyDB:Sum("MORE", nil, "SelfIgniteDuration")
globalOutput.IgniteDuration = 4 * (1 + incDur / 100) * moreDur / burnRateMod * debuffDurationMult
if skillFlags.igniteCanStack then
output.IgniteDamage = output.IgniteDPS * globalOutput.IgniteDuration
if 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 moreDur ~= 1 then
t_insert(globalBreakdown.IgniteDuration, s_format("x %.2f ^8(more/less duration)", moreDur))
end
if burnRateMod ~= 1 then
t_insert(globalBreakdown.IgniteDuration, s_format("/ %.2f ^8(rate modifier)", burnRateMod))
end
if debuffDurationMult ~= 1 then
t_insert(globalBreakdown.IgniteDuration, s_format("/ %.2f ^8(debuff expires slower/faster)", 1 / debuffDurationMult))
end
t_insert(globalBreakdown.IgniteDuration, s_format("= %.2fs", globalOutput.IgniteDuration))
end
end
end
end
-- Calculate shock and freeze chance + duration modifier
if (output.ShockChanceOnHit + output.ShockChanceOnCrit) > 0 then
local sourceHitDmg = 0
local sourceCritDmg = 0
if canDeal.Lightning and not modDB:Sum("FLAG", cfg, "LightningCannotShock") then
sourceHitDmg = sourceHitDmg + output.LightningHitAverage
sourceCritDmg = sourceCritDmg + output.LightningCritAverage
end
if canDeal.Physical and modDB:Sum("FLAG", cfg, "PhysicalCanShock") then
sourceHitDmg = sourceHitDmg + output.PhysicalHitAverage
sourceCritDmg = sourceCritDmg + output.PhysicalCritAverage
end
if canDeal.Fire and modDB:Sum("FLAG", cfg, "FireCanShock") then
sourceHitDmg = sourceHitDmg + output.FireHitAverage
sourceCritDmg = sourceCritDmg + output.FireCritAverage
end
if canDeal.Chaos and modDB:Sum("FLAG", cfg, "ChaosCanShock") then
sourceHitDmg = sourceHitDmg + output.ChaosHitAverage
sourceCritDmg = sourceCritDmg + output.ChaosCritAverage
end
local baseVal = calcSecondaryEffectBase("Shock", sourceHitDmg, sourceCritDmg)
if baseVal > 0 then
skillFlags.shock = true
output.ShockDurationMod = 1 + modDB:Sum("INC", cfg, "EnemyShockDuration") / 100 + enemyDB:Sum("INC", nil, "SelfShockDuration") / 100
if breakdown then
t_insert(breakdown.ShockDPS, s_format("For shock to apply, target must have no more than %d life.", baseVal * 20 * output.ShockDurationMod))
end
end
end
if (output.FreezeChanceOnHit + output.FreezeChanceOnCrit) > 0 then
local sourceHitDmg = 0
local sourceCritDmg = 0
if canDeal.Cold and not modDB:Sum("FLAG", cfg, "ColdCannotFreeze") then
sourceHitDmg = sourceHitDmg + output.ColdHitAverage
sourceCritDmg = sourceCritDmg + output.ColdCritAverage
end
if canDeal.Lightning and modDB:Sum("FLAG", cfg, "LightningCanFreeze") then
sourceHitDmg = sourceHitDmg + output.LightningHitAverage
sourceCritDmg = sourceCritDmg + output.LightningCritAverage
end
local baseVal = calcSecondaryEffectBase("Freeze", sourceHitDmg, sourceCritDmg)
if baseVal > 0 then
skillFlags.freeze = true
output.FreezeDurationMod = 1 + modDB:Sum("INC", cfg, "EnemyFreezeDuration") / 100 + enemyDB:Sum("INC", nil, "SelfFreezeDuration") / 100
if breakdown then
t_insert(breakdown.FreezeDPS, s_format("For freeze to apply, target must have no more than %d life.", baseVal * 20 * output.FreezeDurationMod))
end
end
end
-- Calculate enemy stun modifiers
local enemyStunThresholdRed = -modDB:Sum("INC", cfg, "EnemyStunThreshold")
if enemyStunThresholdRed > 75 then
output.EnemyStunThresholdMod = 1 - (75 + (enemyStunThresholdRed - 75) * 25 / (enemyStunThresholdRed - 50)) / 100
else
output.EnemyStunThresholdMod = 1 - enemyStunThresholdRed / 100
end
local incDur = modDB:Sum("INC", cfg, "EnemyStunDuration")
local incRecov = enemyDB:Sum("INC", nil, "StunRecovery")
output.EnemyStunDuration = 0.35 * (1 + incDur / 100) / (1 + incRecov / 100)
if breakdown then
if output.EnemyStunDuration ~= 0.35 then
breakdown.EnemyStunDuration = {
"0.35s ^8(base duration)"
}
if incDur ~= 0 then
t_insert(breakdown.EnemyStunDuration, s_format("x %.2f ^8(increased/reduced stun duration)", 1 + incDur/100))
end
if incRecov ~= 0 then
t_insert(breakdown.EnemyStunDuration, s_format("/ %.2f ^8(increased/reduced enemy stun recovery)", 1 + incRecov/100))
end
t_insert(breakdown.EnemyStunDuration, s_format("= %.2fs", output.EnemyStunDuration))
end
end
end
-- Combine secondary effect stats
if isAttack then
combineStat("BleedChance", "AVERAGE")
combineStat("BleedDPS", "CHANCE", "BleedChance")
combineStat("PoisonChance", "AVERAGE")
combineStat("PoisonDPS", "CHANCE", "PoisonChance")
combineStat("PoisonDamage", "CHANCE", "PoisonChance")
if 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 _, slot in ipairs(env.curseSlots) do
t_insert(curseList, slot.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