Files
PathOfBuilding/Modules/Calcs.lua
Openarl 1ccc320357 Release 1.2.29
- Add Enemy is Hindered option
- Add Crit Chance is Lucky option
2016-12-29 03:17:34 +10:00

2607 lines
95 KiB
Lua

-- Path of Building
--
-- Module: Calcs
-- Performs all the offense and defense calculations.
-- Here be dragons!
-- This file is 2400 lines long, over half of which is in one function...
--
local pairs = pairs
local ipairs = ipairs
local t_insert = table.insert
local t_remove = table.remove
local m_abs = math.abs
local m_ceil = math.ceil
local m_floor = math.floor
local m_min = math.min
local m_max = math.max
local s_format = string.format
local band = bit.band
local bor = bit.bor
local bnot = bit.bnot
-- List of all damage types, ordered according to the conversion sequence
local dmgTypeList = {"Physical", "Lightning", "Cold", "Fire", "Chaos"}
local resistTypeList = { "Fire", "Cold", "Lightning", "Chaos" }
local isElemental = { Fire = true, Cold = true, Lightning = true }
-- Calculate and combine INC/MORE modifiers for the given modifier names
local function calcMod(modDB, cfg, ...)
return (1 + (modDB:Sum("INC", cfg, ...)) / 100) * modDB:Sum("MORE", cfg, ...)
end
-- Calculate value, optionally adding additional base
local function calcVal(modDB, name, cfg, base)
local baseVal = modDB:Sum("BASE", cfg, name) + (base or 0)
if baseVal ~= 0 then
return baseVal * calcMod(modDB, cfg, name)
else
return 0
end
end
-- Calculate hit chance
local function calcHitChance(evasion, accuracy)
local rawChance = accuracy / (accuracy + (evasion / 4) ^ 0.8) * 100
return m_max(m_min(m_floor(rawChance + 0.5), 95), 5)
end
-- Merge gem modifiers with given mod list
local function mergeGemMods(modList, gem)
modList:AddList(gem.data.baseMods)
if gem.quality > 0 then
for i = 1, #gem.data.qualityMods do
local scaledMod = copyTable(gem.data.qualityMods[i])
scaledMod.value = m_floor(scaledMod.value * gem.quality)
modList:AddMod(scaledMod)
end
end
gem.level = m_max(gem.level, 1)
if not gem.data.levels[gem.level] then
gem.level = m_min(gem.level, #gem.data.levels)
end
local levelData = gem.data.levels[gem.level]
for col, mod in pairs(gem.data.levelMods) do
if levelData[col] then
local newMod = copyTable(mod)
if type(newMod.value) == "table" then
newMod.value.value = levelData[col]
else
newMod.value = levelData[col]
end
modList:AddMod(newMod)
end
end
end
-- Check if given support gem can support the given active skill
-- Global function, as GemSelectControl needs to use it too
function gemCanSupport(gem, activeSkill)
if gem.data.unsupported then
return false
end
for _, skillType in pairs(gem.data.excludeSkillTypes) do
if activeSkill.skillTypes[skillType] then
return false
end
end
if not gem.data.requireSkillTypes[1] then
return true
end
for _, skillType in pairs(gem.data.requireSkillTypes) do
if activeSkill.skillTypes[skillType] then
return true
end
end
return false
end
-- Check if given gem is of the given type ("all", "strength", "melee", etc)
-- Global function, as ModDBClass and ModListClass need to use it too
function gemIsType(gem, type)
return type == "all" or (type == "elemental" and (gem.data.fire or gem.data.cold or gem.data.lightning)) or gem.data[type]
end
-- Create an active skill using the given active gem and list of support gems
-- It will determine the base flag set, and check which of the support gems can support this skill
local function createActiveSkill(activeGem, supportList)
local activeSkill = { }
activeSkill.activeGem = {
name = activeGem.name,
data = activeGem.data,
level = activeGem.level,
quality = activeGem.quality,
fromItem = activeGem.fromItem,
srcGem = activeGem,
}
activeSkill.gemList = { activeSkill.activeGem }
activeSkill.skillTypes = copyTable(activeGem.data.skillTypes)
-- Initialise skill flag set ('attack', 'projectile', etc)
local skillFlags = copyTable(activeGem.data.baseFlags)
activeSkill.skillFlags = skillFlags
skillFlags.hit = activeSkill.skillTypes[SkillType.Attack] or activeSkill.skillTypes[SkillType.Hit]
for _, gem in ipairs(supportList) do
if gemCanSupport(gem, activeSkill) then
if gem.data.addFlags then
-- Support gem adds flags to supported skills (eg. Remote Mine adds 'mine')
for k in pairs(gem.data.addFlags) do
skillFlags[k] = true
end
end
for _, skillType in pairs(gem.data.addSkillTypes) do
activeSkill.skillTypes[skillType] = true
end
end
end
-- Process support gems
for _, gem in ipairs(supportList) do
if gemCanSupport(gem, activeSkill) then
t_insert(activeSkill.gemList, {
name = gem.name,
data = gem.data,
level = gem.level,
quality = gem.quality,
fromItem = gem.fromItem,
srcGem = gem,
})
if gem.isSupporting then
gem.isSupporting[activeGem.name] = true
end
end
end
return activeSkill
end
-- Build list of modifiers for given active skill
local function buildActiveSkillModList(env, activeSkill)
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
-- Set weapon flags
local weapon1Type = env.itemList["Weapon 1"] and env.itemList["Weapon 1"].type or "None"
local weapon2Type = env.itemList["Weapon 2"] and env.itemList["Weapon 2"].type or ""
skillFlags.mainIs1H = true
local weapon1Info = data.weaponTypeInfo[weapon1Type]
if weapon1Info then
if not weapon1Info.oneHand then
skillFlags.mainIs1H = nil
end
if skillFlags.attack then
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
end
end
local weapon2Info = data.weaponTypeInfo[weapon2Type]
if weapon2Info and skillFlags.mainIs1H then
if skillFlags.attack then
skillFlags.weapon2Attack = true
end
end
-- Build skill mod flag set
local skillModFlags = 0
if skillFlags.hit then
skillModFlags = bor(skillModFlags, ModFlag.Hit)
end
if skillFlags.spell then
skillModFlags = bor(skillModFlags, ModFlag.Spell)
elseif skillFlags.attack then
skillModFlags = bor(skillModFlags, ModFlag.Attack)
end
if skillFlags.weapon1Attack then
skillModFlags = bor(skillModFlags, env.weaponData1.flag or weapon1Info.flag)
if weapon1Type ~= "None" then
skillModFlags = bor(skillModFlags, ModFlag.Weapon)
if skillFlags.mainIs1H then
skillModFlags = bor(skillModFlags, ModFlag.Weapon1H)
else
skillModFlags = bor(skillModFlags, ModFlag.Weapon2H)
end
if weapon1Info.melee then
skillModFlags = bor(skillModFlags, ModFlag.WeaponMelee)
else
skillModFlags = bor(skillModFlags, ModFlag.WeaponRanged)
end
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
activeSkill.skillFlags = skillFlags
-- 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.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
activeSkill.skillKeywordFlags = skillKeywordFlags
-- 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 = skillModFlags,
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,
slotName = activeSkill.slotName,
}
-- 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
-- Add support gem modifiers to skill mod list
for _, gem in pairs(activeSkill.gemList) do
if gem.data.support then
mergeGemMods(skillModList, gem)
end
end
-- Apply gem/quality modifiers from support gems
if not activeSkill.activeGem.fromItem then
for _, value in ipairs(skillModList:Sum("LIST", activeSkill.skillCfg, "GemProperty")) do
if value.keyword == "active_skill" then
activeSkill.activeGem[value.key] = activeSkill.activeGem[value.key] + value.value
end
end
end
-- Add active gem modifiers
mergeGemMods(skillModList, activeSkill.activeGem)
-- Extract skill data
activeSkill.skillData = { }
for _, value in ipairs(skillModList:Sum("LIST", activeSkill.skillCfg, "Misc")) do
if value.type == "SkillData" then
activeSkill.skillData[value.key] = value.value
end
end
-- Separate global effect modifiers (mods that can affect defensive stats or other skills)
local i = 1
while skillModList[i] do
local destList
for _, tag in ipairs(skillModList[i].tagList) do
if tag.type == "GlobalEffect" then
if tag.effectType == "Buff" then
destList = "buffModList"
elseif tag.effectType == "Aura" then
destList = "auraModList"
elseif tag.effectType == "Debuff" then
destList = "debuffModList"
elseif tag.effectType == "Curse" then
destList = "curseModList"
end
break
end
end
if destList then
if not activeSkill[destList] then
activeSkill[destList] = { }
end
t_insert(activeSkill[destList], skillModList[i])
t_remove(skillModList, i)
else
i = i + 1
end
end
if activeSkill.buffModList or activeSkill.auraModList or activeSkill.debuffModList or activeSkill.curseModList then
-- Add to auxillary skill list
t_insert(env.auxSkillList, activeSkill)
end
end
-- Build list of modifiers from the listed tree nodes
local function buildNodeModList(env, nodeList, finishJewels)
-- Initialise radius jewels
for _, rad in pairs(env.radiusJewelList) do
wipeTable(rad.data)
end
-- Add node modifers
local modList = common.New("ModList")
for _, node in pairs(nodeList) do
-- Merge with output list
if node.type == "keystone" then
modList:AddMod(node.keystoneMod)
else
modList:AddList(node.modList)
end
-- Run radius jewels
for _, rad in pairs(env.radiusJewelList) do
local vX, vY = node.x - rad.x, node.y - rad.y
if vX * vX + vY * vY <= rad.rSq then
rad.func(node.modList, modList, rad.data)
end
end
end
if finishJewels then
-- Finalise radius jewels
for _, rad in pairs(env.radiusJewelList) do
rad.func(nil, modList, rad.data)
if env.mode == "MAIN" then
if not rad.item.jewelRadiusData then
rad.item.jewelRadiusData = { }
end
rad.item.jewelRadiusData[rad.nodeId] = rad.data
end
end
end
return modList
end
-- Calculate min/max damage of a hit for the given damage type
local function calcHitDamage(env, source, damageType, ...)
local modDB = env.modDB
local skillCfg = env.mainSkill.skillCfg
local damageTypeMin = damageType.."Min"
local damageTypeMax = damageType.."Max"
-- Calculate base values
local damageEffectiveness = source.damageEffectiveness or 1
local addedMin = modDB:Sum("BASE", skillCfg, damageTypeMin)
local addedMax = modDB:Sum("BASE", skillCfg, damageTypeMax)
local baseMin = (source[damageTypeMin] or 0) + addedMin * damageEffectiveness
local baseMax = (source[damageTypeMax] or 0) + addedMax * damageEffectiveness
if env.breakdown and not (...) and baseMin ~= 0 and baseMax ~= 0 then
t_insert(env.breakdown[damageType], "Base damage:")
local plus = ""
if (source[damageTypeMin] or 0) ~= 0 or (source[damageTypeMax] or 0) ~= 0 then
t_insert(env.breakdown[damageType], 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(env.breakdown[damageType], s_format("%s(%d to %d) x %.2f ^8(added damage multiplied by damage effectiveness)", plus, addedMin, addedMax, damageEffectiveness))
else
t_insert(env.breakdown[damageType], s_format("%s%d to %d ^8(added damage)", plus, addedMin, addedMax))
end
end
t_insert(env.breakdown[damageType], 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, 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 env.breakdown and (addMin ~= 0 or addMax ~= 0) then
local endType, convDst
if (...) then
endType = select(select('#', ...), ...)
convDst = s_format("%d%% to %s", conversionTable[damageType][...] * 100, ...)
else
endType = damageType
end
t_insert(env.breakdown[endType].damageComponents, {
source = damageType,
convSrc = (addMin ~= 0 or addMax ~= 0) and (addMin .. " to " .. addMax),
total = addMin .. " to " .. addMax,
convDst = convDst,
})
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", skillCfg, unpack(modNames)) / 100
local more = m_floor(modDB:Sum("MORE", skillCfg, unpack(modNames)) * 100 + 0.50000001) / 100
if env.breakdown then
local endType, convDst
if (...) then
endType = select(select('#', ...), ...)
convDst = s_format("%d%% to %s", conversionTable[damageType][...] * 100, ...)
else
endType = damageType
end
t_insert(env.breakdown[endType].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 = convDst,
})
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. Merge main modifiers (mergeMainMods)
-- 3. Run calculations (performCalcs)
--
-- Thus a basic calculation pass would look like this:
--
-- local env = initEnv(build, mode)
-- mergeMainMods(env)
-- performCalcs(env)
--
local tempTable1 = { }
local tempTable2 = { }
local tempTable3 = { }
-- Initialise environment
-- This will initialise the modifier databases
local function initEnv(build, mode)
local env = { }
env.build = build
env.configInput = build.configTab.input
env.calcsInput = build.calcsTab.input
env.mode = mode
env.classId = build.spec.curClassId
-- Initialise modifier database with base values
local modDB = common.New("ModDB")
env.modDB = modDB
local classStats = build.tree.characterData[env.classId]
for _, stat in pairs({"Str","Dex","Int"}) do
modDB:NewMod(stat, "BASE", classStats["base_"..stat:lower()], "Base")
end
modDB.multipliers["Level"] = m_max(1, m_min(100, build.characterLevel))
modDB:NewMod("Life", "BASE", 12, "Base", { type = "Multiplier", var = "Level", base = 38 })
modDB:NewMod("Mana", "BASE", 6, "Base", { type = "Multiplier", var = "Level", base = 34 })
modDB:NewMod("ManaRegen", "BASE", 0.0175, "Base", { type = "PerStat", stat = "Mana", div = 1 })
modDB:NewMod("Evasion", "BASE", 3, "Base", { type = "Multiplier", var = "Level", base = 53 })
modDB:NewMod("Accuracy", "BASE", 2, "Base", { type = "Multiplier", var = "Level", base = -2 })
modDB:NewMod("FireResistMax", "BASE", 75, "Base")
modDB:NewMod("FireResist", "BASE", -60, "Base")
modDB:NewMod("ColdResistMax", "BASE", 75, "Base")
modDB:NewMod("ColdResist", "BASE", -60, "Base")
modDB:NewMod("LightningResistMax", "BASE", 75, "Base")
modDB:NewMod("LightningResist", "BASE", -60, "Base")
modDB:NewMod("ChaosResistMax", "BASE", 75, "Base")
modDB:NewMod("ChaosResist", "BASE", -60, "Base")
modDB:NewMod("BlockChanceMax", "BASE", 75, "Base")
modDB:NewMod("PowerChargesMax", "BASE", 3, "Base")
modDB:NewMod("CritChance", "INC", 50, "Base", { type = "Multiplier", var = "PowerCharge" })
modDB:NewMod("FrenzyChargesMax", "BASE", 3, "Base")
modDB:NewMod("Speed", "INC", 4, "Base", { type = "Multiplier", var = "FrenzyCharge" })
modDB:NewMod("Damage", "MORE", 4, "Base", { type = "Multiplier", var = "FrenzyCharge" })
modDB:NewMod("EnduranceChargesMax", "BASE", 3, "Base")
modDB:NewMod("ElementalResist", "BASE", 4, "Base", { type = "Multiplier", var = "EnduranceCharge" })
modDB:NewMod("ActiveTrapLimit", "BASE", 3, "Base")
modDB:NewMod("ActiveMineLimit", "BASE", 5, "Base")
modDB:NewMod("ActiveTotemLimit", "BASE", 1, "Base")
modDB:NewMod("ProjectileCount", "BASE", 1, "Base")
modDB:NewMod("Speed", "MORE", 10, "Base", ModFlag.Attack, { type = "Condition", var = "DualWielding" })
modDB:NewMod("PhysicalDamage", "MORE", 20, "Base", ModFlag.Attack, { type = "Condition", var = "DualWielding" })
modDB:NewMod("BlockChance", "BASE", 15, "Base", { type = "Condition", var = "DualWielding" })
modDB:NewMod("Misc", "LIST", { type = "EnemyModifier", mod = modLib.createMod("DamageTaken", "INC", 50, "Shock") }, "Base", { type = "Condition", var = "EnemyShocked" })
-- 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)
return env
end
-- This function:
-- 1. Merges modifiers for all items, optionally replacing one item
-- 2. Builds a list of jewels with radius functions
-- 3. Merges modifiers for all allocated passive nodes
-- 4. Builds a list of active skills and their supports
-- 5. Builds modifier lists for all active skills
local function mergeMainMods(env, repSlotName, repItem)
local build = env.build
-- Build and merge item modifiers, and create list of radius jewels
env.radiusJewelList = wipeTable(env.radiusJewelList)
env.itemList = { }
env.modDB.conditions["UsingAllCorruptedItems"] = true
for slotName, slot in pairs(build.itemsTab.slots) do
local item
if slotName == repSlotName then
item = repItem
else
item = build.itemsTab.list[slot.selItemId]
end
if slot.nodeId then
-- Slot is a jewel socket, check if socket is allocated
if not build.spec.allocNodes[slot.nodeId] then
item = nil
elseif item and item.jewelRadiusIndex then
-- Jewel has a radius, add it to the list
local funcList = item.jewelFunc 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 radiusInfo = data.jewelRadius[item.jewelRadiusIndex]
local node = build.spec.nodes[slot.nodeId]
t_insert(env.radiusJewelList, {
rSq = radiusInfo.rad * radiusInfo.rad,
x = node.x,
y = node.y,
func = func,
item = item,
nodeId = slot.nodeId,
data = { }
})
end
end
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" then
-- Update item counts
local key
if item.rarity == "UNIQUE" then
key = "UniqueItem"
elseif item.rarity == "RARE" then
key = "RareItem"
elseif item.rarity == "MAGIC" then
key = "MagicItem"
else
key = "NormalItem"
end
env.modDB.multipliers[key] = (env.modDB.multipliers[key] or 0) + 1
if item.corrupted then
env.modDB.multipliers.CorruptedItem = (env.modDB.multipliers.CorruptedItem or 0) + 1
else
env.modDB.conditions["UsingAllCorruptedItems"] = false
end
end
end
end
if 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])
env.weaponData2 = env.itemList["Weapon 2"] and env.itemList["Weapon 2"].weaponData and env.itemList["Weapon 2"].weaponData[2] or { }
-- Build and merge modifiers for allocated passives
env.modDB:AddList(buildNodeModList(env, build.spec.allocNodes, 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
end
-- Finalise environment and perform the calculations
-- This function is 1300 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.mainSkill.skillFlags.attack then
env.mode_skillType = "ATTACK"
else
env.mode_skillType = "SPELL"
end
if env.mainSkill.skillData.showAverage then
env.mode_average = true
end
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
-- Set conditions
local condList = modDB.conditions
if env.itemList["Weapon 1"] and env.itemList["Weapon 1"].type == "Staff" then
condList["UsingStaff"] = true
end
if env.itemList["Weapon 1"] and env.itemList["Weapon 1"].type == "Bow" then
condList["UsingBow"] = true
end
if env.itemList["Weapon 2"] and env.itemList["Weapon 2"].type == "Shield" then
condList["UsingShield"] = true
end
if env.weaponData1.type and env.weaponData2.type then
condList["DualWielding"] = true
end
if env.mode_skillType == "ATTACK" then
condList["MainHandAttack"] = true
end
if not env.weaponData1.type then
condList["Unarmed"] = true
end
if (modDB.multipliers["NormalItem"] or 0) > 0 then
condList["UsingNormalItem"] = true
end
if (modDB.multipliers["MagicItem"] or 0) > 0 then
condList["UsingMagicItem"] = true
end
if (modDB.multipliers["RareItem"] or 0) > 0 then
condList["UsingRareItem"] = true
end
if (modDB.multipliers["UniqueItem"] or 0) > 0 then
condList["UsingUniqueItem"] = true
end
if (modDB.multipliers["CorruptedItem"] or 0) > 0 then
condList["UsingCorruptedItem"] = true
else
condList["NotUsingCorruptedItem"] = true
end
if env.mode_buffs then
condList["Buffed"] = true
end
if env.mode_combat then
condList["Combat"] = true
if not modDB:Sum("FLAG", nil, "NeverCrit") then
condList["CritInPast8Sec"] = true
end
if env.mainSkill.skillFlags.attack then
condList["AttackedRecently"] = true
elseif env.mainSkill.skillFlags.spell then
condList["CastSpellRecently"] = true
end
if not env.mainSkill.skillFlags.trap and not env.mainSkill.skillFlags.mine and not env.mainSkill.skillFlags.totem then
condList["HitRecently"] = true
end
if env.mainSkill.skillFlags.movement then
condList["UsedMovementSkillRecently"] = true
end
if env.mainSkill.skillFlags.totem then
condList["SummonedTotemRecently"] = true
end
if env.mainSkill.skillFlags.mine then
condList["DetonatedMinesRecently"] = true
end
end
if env.mode_effective then
condList["Effective"] = true
end
-- Merge auxillary skill modifiers and calculate skill life and mana reservations
env.reserved_LifeBase = 0
env.reserved_LifePercent = 0
env.reserved_ManaBase = 0
env.reserved_ManaPercent = 0
if breakdown then
breakdown.LifeReserved = { reservations = { } }
breakdown.ManaReserved = { reservations = { } }
end
for _, activeSkill in pairs(env.activeSkillList) do
local skillModList = activeSkill.skillModList
local skillCfg = activeSkill.skillCfg
-- Merge auxillary modifiers
if env.mode_buffs then
if activeSkill.buffModList and (not activeSkill.skillFlags.totem or activeSkill.skillData.allowTotemBuff) then
if activeSkill.activeGem.data.golem and modDB:Sum("FLAG", skillCfg, "LiegeOfThePrimordial") and (activeSkill.activeGem.data.fire or activeSkill.activeGem.data.cold or activeSkill.activeGem.data.lightning) then
modDB:ScaleAddList(activeSkill.buffModList, 2)
else
modDB:AddList(activeSkill.buffModList)
end
end
if activeSkill.auraModList then
local inc = modDB:Sum("INC", skillCfg, "AuraEffect") + skillModList:Sum("INC", skillCfg, "AuraEffect")
local more = modDB:Sum("MORE", skillCfg, "AuraEffect") * skillModList:Sum("MORE", skillCfg, "AuraEffect")
modDB:ScaleAddList(activeSkill.auraModList, (1 + inc / 100) * more)
condList["HaveAuraActive"] = true
modDB.multipliers["ActiveAura"] = (modDB.multipliers["ActiveAura"] or 0) + 1
end
end
if env.mode_effective then
if activeSkill.debuffModList then
enemyDB:ScaleAddList(activeSkill.debuffModList, activeSkill.skillData.stackCount or 1)
end
if activeSkill.curseModList then
condList["EnemyCursed"] = true
local inc = modDB:Sum("INC", skillCfg, "CurseEffect") + enemyDB:Sum("INC", nil, "CurseEffect") + skillModList:Sum("INC", skillCfg, "CurseEffect")
local more = modDB:Sum("MORE", skillCfg, "CurseEffect") * enemyDB:Sum("MORE", nil, "CurseEffect") * skillModList:Sum("MORE", skillCfg, "CurseEffect")
enemyDB:ScaleAddList(activeSkill.curseModList, (1 + inc / 100) * more)
end
end
-- Calculate reservations
if activeSkill.skillTypes[SkillType.ManaCostReserved] and not activeSkill.skillFlags.totem then
local baseVal = activeSkill.skillData.manaCostOverride or activeSkill.skillData.manaCost
local suffix = activeSkill.skillTypes[SkillType.ManaCostPercent] and "Percent" or "Base"
local mult = skillModList:Sum("MORE", skillCfg, "ManaCost")
local more = modDB:Sum("MORE", skillCfg, "ManaReserved") * skillModList:Sum("MORE", skillCfg, "ManaReserved")
local inc = modDB:Sum("INC", skillCfg, "ManaReserved") + skillModList:Sum("INC", skillCfg, "ManaReserved")
local cost = m_ceil(m_ceil(m_floor(baseVal * mult) * more) * (1 + inc / 100))
local pool
if modDB:Sum("FLAG", skillCfg, "BloodMagic", "SkillBloodMagic") or skillModList:Sum("FLAG", skillCfg, "SkillBloodMagic") then
pool = "Life"
else
pool = "Mana"
end
env["reserved_"..pool..suffix] = env["reserved_"..pool..suffix] + cost
if breakdown then
t_insert(breakdown[pool.."Reserved"].reservations, {
skillName = activeSkill.activeGem.name,
base = baseVal .. (activeSkill.skillTypes[SkillType.ManaCostPercent] and "%" or ""),
mult = mult ~= 1 and ("x "..mult),
more = more ~= 1 and ("x "..more),
inc = inc ~= 0 and ("x "..(1 + inc/100)),
total = cost .. (activeSkill.skillTypes[SkillType.ManaCostPercent] and "%" or ""),
})
end
end
end
-- Process misc modifiers
for _, value in ipairs(modDB:Sum("LIST", nil, "Misc")) do
if value.type == "Condition" then
condList[value.var] = true
elseif value.type == "EnemyCondition" then
enemyDB.conditions[value.var] = true
elseif value.type == "Multiplier" then
modDB.multipliers[value.var] = (modDB.multipliers[value.var] or 0) + value.value
end
end
-- Process enemy modifiers last in case they depend on conditions that were set by misc modifiers
for _, value in ipairs(modDB:Sum("LIST", nil, "Misc")) do
if value.type == "EnemyModifier" then
enemyDB:AddMod(value.mod)
end
end
-- Process conditions that can depend on other conditions
condList["NotCritRecently"] = not condList["CritRecently"]
condList["NotKilledRecently"] = not condList["KilledRecently"]
condList["NotBeenHitRecently"] = not condList["BeenHitRecently"]
if env.mode_effective then
if condList["EnemyIgnited"] then
condList["EnemyBurning"] = true
end
condList["EnemyFrozenShockedIgnited"] = condList["EnemyFrozen"] or condList["EnemyShocked"] or condList["EnemyIgnited"]
condList["EnemyElementalStatus"] = condList["EnemyChilled"] or condList["EnemyFrozen"] or condList["EnemyShocked"] or condList["EnemyIgnited"]
condList["NotEnemyElementalStatus"] = not condList["EnemyElementalStatus"]
end
-- Calculate current and maximum charges
output.PowerChargesMax = modDB:Sum("BASE", nil, "PowerChargesMax")
output.FrenzyChargesMax = modDB:Sum("BASE", nil, "FrenzyChargesMax")
output.EnduranceChargesMax = modDB:Sum("BASE", nil, "EnduranceChargesMax")
if env.configInput.usePowerCharges and env.mode_combat then
output.PowerCharges = output.PowerChargesMax
else
output.PowerCharges = 0
end
if env.configInput.useFrenzyCharges and env.mode_combat then
output.FrenzyCharges = output.FrenzyChargesMax
else
output.FrenzyCharges = 0
end
if env.configInput.useEnduranceCharges and env.mode_combat then
output.EnduranceCharges = output.EnduranceChargesMax
else
output.EnduranceCharges = 0
end
modDB.multipliers["PowerCharge"] = output.PowerCharges
modDB.multipliers["FrenzyCharge"] = output.FrenzyCharges
modDB.multipliers["EnduranceCharge"] = output.EnduranceCharges
if output.PowerCharges == output.PowerChargesMax then
condList["AtMaxPowerCharges"] = true
end
if output.FrenzyCharges == output.FrenzyChargesMax then
condList["AtMaxFrenzyCharges"] = true
end
if output.EnduranceCharges == output.EnduranceChargesMax then
condList["AtMaxEnduranceCharges"] = true
end
-- Add misc buffs
if env.mode_combat then
if condList["Onslaught"] then
local effect = m_floor(20 * (1 + modDB:Sum("INC", nil, "OnslaughtEffect") / 100))
modDB:NewMod("Speed", "INC", effect, "Onslaught")
modDB:NewMod("MovementSpeed", "INC", effect, "Onslaught")
end
if condList["UnholyMight"] then
modDB:NewMod("PhysicalDamageGainAsChaos", "BASE", 30, "Unholy Might")
end
end
-- Helper functions for stat breakdowns
local simpleBreakdown, modBreakdown, slotBreakdown, effMultBreakdown, dotBreakdown
if breakdown then
simpleBreakdown = function(extraBase, cfg, ...)
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", output[...]))
breakdown[...] = 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, 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 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
simpleBreakdown(nil, nil, 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
simpleBreakdown(nil, nil, "Mana")
simpleBreakdown(nil, nil, "ManaRegen", "ManaRecovery")
end
-- Life/mana reservation
for _, pool in pairs({"Life", "Mana"}) do
local max = output[pool]
local reserved = env["reserved_"..pool.."Base"] + m_ceil(max * env["reserved_"..pool.."Percent"] / 100)
output[pool.."Reserved"] = reserved
output[pool.."ReservedPercent"] = reserved / max * 100
output[pool.."Unreserved"] = max - reserved
output[pool.."UnreservedPercent"] = (max - reserved) / max * 100
if (max - reserved) / max <= 0.35 then
condList["Low"..pool] = true
end
if reserved == 0 then
condList["No"..pool.."Reserved"] = true
end
end
-- Resistances
for _, elem in ipairs(resistTypeList) do
local max, total
if elem == "Chaos" and modDB:Sum("FLAG", nil, "ChaosInoculation") then
max = 100
total = 100
else
max = modDB:Sum("BASE", nil, elem.."ResistMax")
total = modDB:Sum("BASE", nil, elem.."Resist", isElemental[elem] and "ElementalResist")
end
output[elem.."Resist"] = m_min(total, max)
output[elem.."ResistTotal"] = total
output[elem.."ResistOverCap"] = m_max(0, total - max)
if breakdown then
breakdown[elem.."Resist"] = {
"Max: "..max.."%",
"Total: "..total.."%",
"In hideout: "..(total + 60).."%",
}
end
end
-- Primary defences: Energy shield, evasion and armour
do
local ironReflexes = modDB:Sum("FLAG", nil, "IronReflexes")
local energyShield = 0
local armour = 0
local evasion = 0
if breakdown then
breakdown.EnergyShield = { slots = { } }
breakdown.Armour = { slots = { } }
breakdown.Evasion = { slots = { } }
end
local energyShieldBase = modDB:Sum("BASE", nil, "EnergyShield")
if energyShieldBase > 0 then
energyShield = energyShield + energyShieldBase * calcMod(modDB, nil, "EnergyShield", "Defences")
if breakdown then
slotBreakdown("Global", nil, nil, energyShieldBase, nil, "EnergyShield", "Defences")
end
end
local armourBase = modDB:Sum("BASE", nil, "Armour", "ArmourAndEvasion")
if armourBase > 0 then
armour = armour + armourBase * calcMod(modDB, nil, "Armour", "ArmourAndEvasion", "Defences")
if breakdown then
slotBreakdown("Global", nil, nil, armourBase, nil, "Armour", "ArmourAndEvasion", "Defences")
end
end
local evasionBase = modDB:Sum("BASE", nil, "Evasion", "ArmourAndEvasion")
if evasionBase > 0 then
if ironReflexes then
armour = armour + evasionBase * calcMod(modDB, nil, "Armour", "Evasion", "ArmourAndEvasion", "Defences")
if breakdown then
slotBreakdown("Conversion", "Evasion to Armour", nil, evasionBase, nil, "Armour", "Evasion", "ArmourAndEvasion", "Defences")
end
else
evasion = evasion + evasionBase * calcMod(modDB, nil, "Evasion", "ArmourAndEvasion", "Defences")
if breakdown then
slotBreakdown("Global", nil, nil, evasionBase, nil, "Evasion", "ArmourAndEvasion", "Defences")
end
end
end
local gearEnergyShield = 0
local gearArmour = 0
local gearEvasion = 0
local slotCfg = wipeTable(tempTable1)
for _, slot in pairs({"Helmet","Body Armour","Gloves","Boots","Weapon 2"}) do
local armourData = env.itemList[slot] and env.itemList[slot].armourData
if armourData then
slotCfg.slotName = slot
energyShieldBase = armourData.EnergyShield or 0
if energyShieldBase > 0 then
energyShield = energyShield + energyShieldBase * calcMod(modDB, slotCfg, "EnergyShield", "Defences")
gearEnergyShield = gearEnergyShield + energyShieldBase
if breakdown then
slotBreakdown(slot, nil, slotCfg, energyShieldBase, nil, "EnergyShield", "Defences")
end
end
armourBase = armourData.Armour or 0
if armourBase > 0 then
if slot == "Body Armour" and modDB:Sum("FLAG", nil, "Unbreakable") then
armourBase = armourBase * 2
end
armour = armour + armourBase * calcMod(modDB, slotCfg, "Armour", "ArmourAndEvasion", "Defences")
gearArmour = gearArmour + armourBase
if breakdown then
slotBreakdown(slot, nil, slotCfg, armourBase, nil, "Armour", "ArmourAndEvasion", "Defences")
end
end
evasionBase = armourData.Evasion or 0
if evasionBase > 0 then
if ironReflexes then
armour = armour + evasionBase * calcMod(modDB, slotCfg, "Armour", "Evasion", "ArmourAndEvasion", "Defences")
gearArmour = gearArmour + evasionBase
if breakdown then
slotBreakdown(slot, nil, slotCfg, evasionBase, nil, "Armour", "Evasion", "ArmourAndEvasion", "Defences")
end
else
evasion = evasion + evasionBase * calcMod(modDB, slotCfg, "Evasion", "ArmourAndEvasion", "Defences")
gearEvasion = gearEvasion + evasionBase
if breakdown then
slotBreakdown(slot, nil, slotCfg, evasionBase, nil, "Evasion", "ArmourAndEvasion", "Defences")
end
end
end
end
end
local convManaToES = modDB:Sum("BASE", nil, "ManaGainAsEnergyShield")
if convManaToES > 0 then
energyShieldBase = modDB:Sum("BASE", nil, "Mana") * convManaToES / 100
energyShield = energyShield + energyShieldBase * calcMod(modDB, nil, "Mana", "EnergyShield", "Defences")
if breakdown then
slotBreakdown("Conversion", "Mana to Energy Shield", nil, energyShieldBase, nil, "EnergyShield", "Defences", "Mana")
end
end
local convLifeToES = modDB:Sum("BASE", nil, "LifeConvertToEnergyShield")
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
simpleBreakdown(output.EnergyShield * 0.2, nil, "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)
if breakdown then
breakdown.EvadeChance = {
s_format("Enemy level: %d ^8(%s the Configuration tab)", env.enemyLevel, env.configInput.enemyLevel and "overridden from" or "can be overridden in"),
s_format("Average enemy accuracy: %d", enemyAccuracy),
s_format("Approximate evade chance: %d%%", output.EvadeChance),
}
end
end
end
-- Life and energy shield regen
do
if modDB:Sum("FLAG", nil, "NoLifeRegen") then
output.LifeRegen = 0
elseif modDB:Sum("FLAG", nil, "ZealotsOath") then
output.LifeRegen = 0
local lifeBase = modDB:Sum("BASE", nil, "LifeRegen")
if lifeBase > 0 then
modDB:NewMod("EnergyShieldRegen", "BASE", lifeBase, "Zealot's Oath")
end
local lifePercent = modDB:Sum("BASE", nil, "LifeRegenPercent")
if lifePercent > 0 then
modDB:NewMod("EnergyShieldRegenPercent", "BASE", lifePercent, "Zealot's Oath")
end
else
local lifeBase = modDB:Sum("BASE", nil, "LifeRegen")
local lifePercent = modDB:Sum("BASE", nil, "LifeRegenPercent")
if lifePercent > 0 then
lifeBase = lifeBase + output.Life * lifePercent / 100
end
if lifeBase > 0 then
output.LifeRegen = lifeBase * calcMod(modDB, nil, "LifeRecovery")
output.LifeRegenPercent = round(lifeBase / output.Life * 100, 1)
else
output.LifeRegen = 0
end
end
local esBase = modDB:Sum("BASE", nil, "EnergyShieldRegen")
local esPercent = modDB:Sum("BASE", nil, "EnergyShieldRegenPercent")
if esPercent > 0 then
esBase = esBase + output.EnergyShield * esPercent / 100
end
if esBase > 0 then
output.EnergyShieldRegen = esBase * calcMod(modDB, nil, "EnergyShieldRecovery")
output.EnergyShieldRegenPercent = round(esBase / output.EnergyShield * 100, 1)
else
output.EnergyShieldRegen = 0
end
end
-- Other defences: block, dodge, stun recovery/avoidance
do
output.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
simpleBreakdown(shieldData and shieldData.BlockChance, nil, "BlockChance")
simpleBreakdown(output.BlockChance * modDB:Sum("BASE", nil, "BlockChanceConv") / 100, nil, "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 --
-- ---------------------- --
-- 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.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
if not env.mode_average then
skillFlags.notAverage = true
end
-- Update skill data
for _, value in ipairs(modDB:Sum("LIST", skillCfg, "Misc")) do
if value.type == "SkillData" then
skillData[value.key] = value.value
end
end
-- 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
-- Calculate skill type stats
if skillFlags.projectile then
output.ProjectileCount = modDB:Sum("BASE", skillCfg, "ProjectileCount")
output.PierceChance = m_min(100, modDB:Sum("BASE", skillCfg, "PierceChance"))
output.ProjectileSpeedMod = calcMod(modDB, skillCfg, "ProjectileSpeed")
if breakdown then
breakdown.ProjectileSpeedMod = modBreakdown(skillCfg, "ProjectileSpeed")
end
end
if skillFlags.area then
output.AreaRadiusMod = calcMod(modDB, skillCfg, "AreaRadius")
if breakdown then
breakdown.AreaRadiusMod = modBreakdown(skillCfg, "AreaRadius")
end
end
if skillFlags.trap then
output.ActiveTrapLimit = modDB:Sum("BASE", skillCfg, "ActiveTrapLimit")
output.TrapCooldown = (skillData.trapCooldown or 4) / calcMod(modDB, skillCfg, "TrapCooldownRecovery")
if breakdown then
breakdown.TrapCooldown = {
s_format("%.2fs ^8(base)", skillData.trapCooldown or 4),
s_format("/ %.2f ^8(increased/reduced cooldown recovery)", 1 + modDB:Sum("INC", skillCfg, "TrapCooldownRecovery") / 100),
s_format("= %.2fs", output.TrapCooldown)
}
end
end
if skillFlags.mine then
output.ActiveMineLimit = modDB:Sum("BASE", skillCfg, "ActiveMineLimit")
end
if skillFlags.totem then
output.ActiveTotemLimit = modDB:Sum("BASE", skillCfg, "ActiveTotemLimit")
output.TotemLifeMod = calcMod(modDB, skillCfg, "TotemLife")
output.TotemLife = round(data.monsterLifeTable[skillData.totemLevel] * data.totemLifeMult[env.mainSkill.skillTotemId] * output.TotemLifeMod)
if breakdown then
breakdown.TotemLifeMod = modBreakdown(skillCfg, "TotemLife")
breakdown.TotemLife = {
"Totem level: "..skillData.totemLevel,
data.monsterLifeTable[skillData.totemLevel].." ^8(base life for a level "..skillData.totemLevel.." monster)",
"x "..data.totemLifeMult[env.mainSkill.skillTotemId].." ^8(life multiplier for this totem type)",
"x "..output.TotemLifeMod.." ^8(totem life modifier)",
"= "..output.TotemLife,
}
end
end
-- Skill duration
local debuffDurationMult
if env.mode_effective then
debuffDurationMult = 1 / calcMod(enemyDB, skillCfg, "BuffExpireFaster")
else
debuffDurationMult = 1
end
do
output.DurationMod = calcMod(modDB, skillCfg, "Duration")
if breakdown then
breakdown.DurationMod = modBreakdown(skillCfg, "Duration")
end
local durationBase = skillData.duration or 0
if durationBase > 0 then
output.Duration = durationBase * output.DurationMod
if skillData.debuff then
output.Duration = output.Duration * debuffDurationMult
end
if breakdown and output.Duration ~= durationBase then
breakdown.Duration = {
s_format("%.2fs ^8(base)", durationBase),
}
if output.DurationMod ~= 1 then
t_insert(breakdown.Duration, s_format("x %.2f ^8(duration modifier)", output.DurationMod))
end
if skillData.debuff and debuffDurationMult ~= 1 then
t_insert(breakdown.Duration, s_format("/ %.2f ^8(debuff expires slower/faster)", 1 / debuffDurationMult))
end
t_insert(breakdown.Duration, s_format("= %.2fs", output.Duration))
end
end
end
-- Run skill setup function
do
local setupFunc = env.mainSkill.activeGem.data.setupFunc
if setupFunc then
setupFunc(env, output)
end
end
local isAttack = (env.mode_skillType == "ATTACK")
-- Cache global damage disabling flags
local canDeal = { }
for _, damageType in pairs(dmgTypeList) do
canDeal[damageType] = not modDB:Sum("FLAG", skillCfg, "DealNo"..damageType)
end
-- Calculate enemy resistances
do
local elemResist = enemyDB:Sum("BASE", nil, "ElementalResist")
for _, damageType in pairs({"Lightning","Cold","Fire"}) do
output["Enemy"..damageType.."Resist"] = m_min(enemyDB:Sum("BASE", nil, damageType.."Resist") + elemResist, 75)
end
output.EnemyChaosResist = m_min(enemyDB:Sum("BASE", nil, "ChaosResist"), 75)
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 hit chance
output.Accuracy = calcVal(modDB, "Accuracy", skillCfg)
if breakdown then
simpleBreakdown(nil, skillCfg, "Accuracy")
end
if not isAttack or modDB:Sum("FLAG", skillCfg, "CannotBeEvaded") or env.weaponData1.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
do
local baseSpeed
if skillData.timeOverride then
output.Time = skillData.timeOverride
output.Speed = 1 / output.Time
else
if isAttack then
if skillData.castTimeOverridesAttackTime then
-- Skill is overriding weapon attack speed
baseSpeed = 1 / skillData.castTime * (1 + (env.weaponData1.AttackSpeedInc or 0) / 100)
else
baseSpeed = env.weaponData1.attackRate or 1
end
else
baseSpeed = 1 / (skillData.castTime or 1)
end
output.Speed = baseSpeed * calcMod(modDB, skillCfg, "Speed")
output.Time = 1 / output.Speed
if breakdown then
simpleBreakdown(baseSpeed, skillCfg, "Speed")
end
end
if skillData.hitTimeOverride then
output.HitTime = skillData.hitTimeOverride
output.HitSpeed = 1 / output.HitTime
end
end
-- Calculate crit chance, crit multiplier, and their combined effect
if modDB:Sum("FLAG", nil, "NeverCrit") then
output.CritChance = 0
output.CritMultiplier = 0
output.CritEffect = 1
else
local baseCrit
if isAttack then
baseCrit = env.weaponData1.critChance or 0
else
baseCrit = skillData.critChance or 0
end
if baseCrit == 100 then
output.CritChance = 100
else
local base = modDB:Sum("BASE", skillCfg, "CritChance")
output.CritChance = (baseCrit + base) * calcMod(modDB, skillCfg, "CritChance")
if env.mode_effective then
output.CritChance = output.CritChance + enemyDB:Sum("BASE", nil, "SelfExtraCritChance")
end
output.CritChance = m_min(output.CritChance, 95)
if (baseCrit + base) > 0 then
output.CritChance = m_max(output.CritChance, 5)
end
local actualCritChance = output.CritChance
if env.mode_effective and modDB:Sum("FLAG", skillCfg, "CritChanceLucky") then
output.CritChance = (1 - (1 - output.CritChance / 100) ^ 2) * 100
end
if breakdown and output.CritChance ~= baseCrit then
local inc = modDB:Sum("INC", skillCfg, "CritChance")
local more = modDB:Sum("MORE", skillCfg, "CritChance")
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", actualCritChance))
if env.mode_effective and modDB:Sum("FLAG", skillCfg, "CritChanceLucky") then
t_insert(breakdown.CritChance, "Crit Chance is Lucky:")
t_insert(breakdown.CritChance, s_format("1 - (1 - %.4f) x (1 - %.4f)", actualCritChance / 100, actualCritChance / 100))
t_insert(breakdown.CritChance, s_format("= %.2f", output.CritChance))
end
end
end
if modDB:Sum("FLAG", skillCfg, "NoCritDamage") then
output.CritMultiplier = 0
elseif modDB:Sum("FLAG", skillCfg, "NoCritMultiplier") then
output.CritMultiplier = 1
else
local extraDamage = 0.5 + modDB:Sum("BASE", skillCfg, "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", skillCfg, "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 - %g) ^8(portion of damage from non-crits)", output.CritChance/100),
s_format("+ (%g 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 totalMin, totalMax = 0, 0
do
local hitSource = (env.mode_skillType == "ATTACK") and env.weaponData1 or env.mainSkill.skillData
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, hitSource, 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 (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 isElemental[damageType] then
resist = output["Enemy"..damageType.."Resist"]
pen = modDB:Sum("BASE", skillCfg, damageType.."Penetration", "ElementalPenetration")
taken = taken + enemyDB:Sum("INC", nil, "ElementalDamageTaken")
elseif damageType == "Chaos" then
resist = output.EnemyChaosResist
else
resist = enemyDB:Sum("INC", nil, "PhysicalDamageReduction")
end
if skillFlags.projectile then
taken = taken + enemyDB:Sum("INC", nil, "ProjectileDamageTaken")
end
local effMult = (1 - (resist - pen) / 100) * (1 + taken / 100)
min = min * effMult
max = max * effMult
if env.mode == "CALCS" then
output[damageType.."EffMult"] = effMult
end
if breakdown and effMult ~= 1 then
t_insert(breakdown[damageType], s_format("x %.3f ^8(effective DPS modifier)", effMult))
breakdown[damageType.."EffMult"] = effMultBreakdown(damageType, resist, pen, taken, effMult)
end
end
if breakdown then
t_insert(breakdown[damageType], s_format("= %d to %d", min, max))
end
else
min, max = 0, 0
if breakdown then
breakdown[damageType] = {
"You can't deal "..damageType.." damage"
}
end
end
if env.mode == "CALCS" then
output[damageType.."Min"] = min
output[damageType.."Max"] = max
end
output[damageType.."Average"] = (min + max) / 2
totalMin = totalMin + min
totalMax = totalMax + max
end
end
output.TotalMin = totalMin
output.TotalMax = totalMax
-- Update enemy hit-by-damage-type conditions
enemyDB.conditions.HitByFireDamage = output.FireAverage > 0
enemyDB.conditions.HitByColdDamage = output.ColdAverage > 0
enemyDB.conditions.HitByLightningDamage = output.LightningAverage > 0
-- Calculate average damage and final DPS
output.AverageHit = (totalMin + totalMax) / 2 * output.CritEffect
output.AverageDamage = output.AverageHit * output.HitChance / 100
output.TotalDPS = output.AverageDamage * (output.HitSpeed or output.Speed) * (skillData.dpsMultiplier or 1)
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 output.CritEffect ~= 1 then
breakdown.AverageHit = {
s_format("%.1f ^8(non-crit average)", (totalMin + totalMax) / 2),
s_format("x %.3f ^8(crit effect modifier)", output.CritEffect),
s_format("= %.1f", output.AverageHit),
}
end
if isAttack then
breakdown.AverageDamage = {
s_format("%.1f ^8(average hit)", output.AverageHit),
s_format("x %.2f ^8(chance to hit)", output.HitChance / 100),
s_format("%.1f", output.AverageDamage),
}
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 %d ^8(DPS multiplier for this skill)", skillData.dpsMultiplier))
end
t_insert(breakdown.TotalDPS, s_format("= %.1f", output.TotalDPS))
end
-- 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
-- Calculate skill DOT components
local dotCfg = {
skillName = skillCfg.skillName,
skillPart = skillCfg.skillPart,
slotName = skillCfg.slotName,
flags = bor(band(skillCfg.flags, ModFlag.SourceMask), ModFlag.Dot, skillData.dotIsSpell and ModFlag.Spell or 0),
keywordFlags = skillCfg.keywordFlags
}
env.mainSkill.dotCfg = dotCfg
output.TotalDot = 0
for _, damageType in ipairs(dmgTypeList) do
local baseVal
if canDeal[damageType] then
baseVal = skillData[damageType.."Dot"] or 0
else
baseVal = 0
end
if baseVal > 0 then
skillFlags.dot = true
local effMult = 1
if env.mode_effective then
local resist = 0
local taken = enemyDB:Sum("INC", nil, "DamageTaken", damageType.."DamageTaken", "DotTaken")
if damageType == "Physical" then
resist = enemyDB:Sum("INC", nil, "PhysicalDamageReduction")
else
if isElemental[damageType] then
taken = taken + enemyDB:Sum("INC", nil, "ElementalDamageTaken")
end
if damageType == "Fire" then
taken = taken + enemyDB:Sum("INC", nil, "BurningDamageTaken")
end
resist = output["Enemy"..damageType.."Resist"]
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, effMult, total)
end
end
end
-- Calculate bleeding chance and damage
skillFlags.bleed = false
output.BleedChance = m_min(100, modDB:Sum("BASE", skillCfg, "BleedChance"))
if canDeal.Physical and not modDB:Sum("FLAG", skillCfg, "CannotBleed") and output.BleedChance > 0 and output.PhysicalAverage > 0 then
skillFlags.bleed = true
skillFlags.duration = true
local dotCfg = {
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)
}
env.mainSkill.bleedCfg = dotCfg
local baseVal = output.PhysicalAverage * output.CritEffect * 0.1
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)
output["BleedEffMult"] = effMult
if breakdown and effMult ~= 1 then
breakdown.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
output.BleedDuration = 5 * calcMod(modDB, dotCfg, "Duration") * debuffDurationMult
if breakdown then
breakdown.BleedDPS = {
"Base damage:",
s_format("%.1f ^8(average physical non-crit damage)", output.PhysicalAverage)
}
if output.CritEffect ~= 1 then
t_insert(breakdown.BleedDPS, s_format("x %.3f ^8(crit effect modifier)", output.CritEffect))
end
t_insert(breakdown.BleedDPS, "x 0.1 ^8(bleed deals 10% per second)")
t_insert(breakdown.BleedDPS, s_format("= %.1f", baseVal, 1))
t_insert(breakdown.BleedDPS, "Bleed DPS:")
dotBreakdown(breakdown.BleedDPS, baseVal, inc, more, effMult, output.BleedDPS)
if output.BleedDuration ~= 5 then
breakdown.BleedDuration = {
"5.00s ^8(base duration)"
}
if output.DurationMod ~= 1 then
t_insert(breakdown.BleedDuration, s_format("x %.2f ^8(duration modifier)", output.DurationMod))
end
if debuffDurationMult ~= 1 then
t_insert(breakdown.BleedDuration, s_format("/ %.2f ^8(debuff expires slower/faster)", 1 / debuffDurationMult))
end
t_insert(breakdown.BleedDuration, s_format("= %.2fs", output.BleedDuration))
end
end
end
-- Calculate poison chance and damage
skillFlags.poison = false
output.PoisonChance = m_min(100, modDB:Sum("BASE", skillCfg, "PoisonChance"))
if canDeal.Chaos and output.PoisonChance > 0 and (output.PhysicalAverage > 0 or output.ChaosAverage > 0) then
skillFlags.poison = true
skillFlags.duration = true
local dotCfg = {
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)
}
env.mainSkill.poisonCfg = dotCfg
local poisonCritEffect = 1 - output.CritChance / 100 + output.CritChance / 100 * output.CritMultiplier * modDB:Sum("MORE", skillCfg, "PoisonDamageOnCrit")
local baseVal = (output.PhysicalAverage + output.ChaosAverage) * poisonCritEffect * 0.08
local effMult = 1
if env.mode_effective then
local resist = output["EnemyChaosResist"]
local taken = enemyDB:Sum("INC", nil, "DamageTaken", "ChaosDamageTaken", "DotTaken")
effMult = (1 - resist / 100) * (1 + taken / 100)
output["PoisonEffMult"] = effMult
if breakdown then
breakdown.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
output.PoisonDuration = durationBase * calcMod(modDB, dotCfg, "Duration") * debuffDurationMult
output.PoisonDamage = output.PoisonDPS * output.PoisonDuration
if breakdown then
breakdown.PoisonDPS = { }
if poisonCritEffect ~= output.CritEffect then
t_insert(breakdown.PoisonDPS, "Crit effect modifier for poison base damage:")
t_insert(breakdown.PoisonDPS, s_format("(1 - %g) ^8(portion of damage from non-crits)", output.CritChance/100))
t_insert(breakdown.PoisonDPS, s_format("+ (%g x %g x %g) ^8(portion of damage from crits)", output.CritChance/100, output.CritMultiplier, modDB:Sum("MORE", skillCfg, "PoisonDamageOnCrit")))
t_insert(breakdown.PoisonDPS, s_format("= %.3f", poisonCritEffect))
end
t_insert(breakdown.PoisonDPS, "Base damage:")
t_insert(breakdown.PoisonDPS, s_format("%.1f ^8(average physical + chaos non-crit damage)", output.PhysicalAverage + output.ChaosAverage))
if output.CritEffect ~= 1 then
t_insert(breakdown.PoisonDPS, s_format("x %.3f ^8(crit effect modifier)", poisonCritEffect))
end
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, effMult, output.PoisonDPS)
if output.PoisonDuration ~= 2 then
breakdown.PoisonDuration = {
s_format("%.2fs ^8(base duration)", durationBase)
}
if output.DurationMod ~= 1 then
t_insert(breakdown.PoisonDuration, s_format("x %.2f ^8(duration modifier)", output.DurationMod))
end
if debuffDurationMult ~= 1 then
t_insert(breakdown.PoisonDuration, s_format("/ %.2f ^8(debuff expires slower/faster)", 1 / debuffDurationMult))
end
t_insert(breakdown.PoisonDuration, s_format("= %.2fs", output.PoisonDuration))
end
breakdown.PoisonDamage = {
s_format("%.1f ^8(damage per second)", output.PoisonDPS),
s_format("x %.2fs ^8(poison duration)", output.PoisonDuration),
s_format("= %.1f ^8damage per poison stack", output.PoisonDamage),s
}
end
end
-- Calculate ignite chance and damage
skillFlags.ignite = false
if modDB:Sum("FLAG", skillCfg, "CannotIgnite") then
output.IgniteChance = 0
else
local igniteMode = env.configInput.igniteMode or "AVERAGE"
output.IgniteChance = m_min(100, modDB:Sum("BASE", skillCfg, "EnemyIgniteChance") + enemyDB:Sum("BASE", nil, "SelfIgniteChance"))
local sourceDmg = 0
if canDeal.Fire and not modDB:Sum("FLAG", skillCfg, "FireCannotIgnite") then
sourceDmg = sourceDmg + output.FireAverage
end
if canDeal.Cold and modDB:Sum("FLAG", skillCfg, "ColdCanIgnite") then
sourceDmg = sourceDmg + output.ColdAverage
end
if canDeal.Fire and (output.IgniteChance > 0 or igniteMode == "CRIT") and sourceDmg > 0 then
skillFlags.ignite = true
local dotCfg = {
slotName = skillCfg.slotName,
flags = bor(band(skillCfg.flags, ModFlag.SourceMask), ModFlag.Dot, skillData.dotIsSpell and ModFlag.Spell or 0),
keywordFlags = skillCfg.keywordFlags,
}
env.mainSkill.igniteCfg = dotCfg
local baseVal
if igniteMode == "CRIT" then
baseVal = sourceDmg * output.CritMultiplier * 0.2
else
baseVal = sourceDmg * output.CritEffect * 0.2
end
local effMult = 1
if env.mode_effective then
local resist = output["EnemyFireResist"]
local taken = enemyDB:Sum("INC", dotCfg, "DamageTaken", "FireDamageTaken", "ElementalDamageTaken", "BurningDamageTaken", "DotTaken")
effMult = (1 - resist / 100) * (1 + taken / 100)
output["IgniteEffMult"] = effMult
if breakdown then
breakdown.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)
output.IgniteDPS = baseVal * (1 + inc/100) * more * effMult
local incDur = modDB:Sum("INC", dotCfg, "EnemyIgniteDuration") + enemyDB:Sum("INC", nil, "SelfIgniteDuration")
output.IgniteDuration = 4 * (1 + incDur / 100) * debuffDurationMult
if breakdown then
breakdown.IgniteDPS = {
s_format("Ignite mode: %s ^8(can be changed in the Configuration tab)", igniteMode == "CRIT" and "Crit Damage" or "Average Damage"),
"Base damage:",
s_format("%.1f ^8(average non-crit damage from sources)", sourceDmg),
}
if igniteMode == "CRIT" then
if output.CritMultiplier ~= 1 then
t_insert(breakdown.IgniteDPS, s_format("x %.2f ^8(crit multiplier)", output.CritMultiplier))
end
else
if output.CritEffect ~= 1 then
t_insert(breakdown.IgniteDPS, s_format("x %.3f ^8(crit effect modifier)", output.CritEffect))
end
end
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, effMult, output.IgniteDPS)
if output.IgniteDuration ~= 4 then
breakdown.IgniteDuration = {
s_format("4.00s ^8(base duration)", durationBase)
}
if incDur ~= 0 then
t_insert(breakdown.IgniteDuration, s_format("x %.2f ^8(increased/reduced duration)", 1 + incDur/100))
end
if debuffDurationMult ~= 1 then
t_insert(breakdown.IgniteDuration, s_format("/ %.2f ^8(debuff expires slower/faster)", 1 / debuffDurationMult))
end
t_insert(breakdown.IgniteDuration, s_format("= %.2fs", output.IgniteDuration))
end
end
end
end
-- Calculate shock and freeze chance + duration modifier
skillFlags.shock = false
if modDB:Sum("FLAG", skillCfg, "CannotShock") then
output.ShockChance = 0
else
output.ShockChance = m_min(100, modDB:Sum("BASE", skillCfg, "EnemyShockChance") + enemyDB:Sum("BASE", nil, "SelfShockChance"))
local sourceDmg = 0
if canDeal.Lightning and not modDB:Sum("FLAG", skillCfg, "LightningCannotShock") then
sourceDmg = sourceDmg + output.LightningAverage
end
if canDeal.Physical and modDB:Sum("FLAG", skillCfg, "PhysicalCanShock") then
sourceDmg = sourceDmg + output.PhysicalAverage
end
if canDeal.Fire and modDB:Sum("FLAG", skillCfg, "FireCanShock") then
sourceDmg = sourceDmg + output.FireAverage
end
if canDeal.Chaos and modDB:Sum("FLAG", skillCfg, "ChaosCanShock") then
sourceDmg = sourceDmg + output.ChaosAverage
end
if output.ShockChance > 0 and sourceDmg > 0 then
skillFlags.shock = true
output.ShockDurationMod = 1 + modDB:Sum("INC", dotCfg, "EnemyShockDuration") / 100 + enemyDB:Sum("INC", nil, "SelfShockDuration") / 100
end
end
skillFlags.freeze = false
if modDB:Sum("FLAG", skillCfg, "CannotFreeze") then
output.FreezeChance = 0
else
output.FreezeChance = m_min(100, modDB:Sum("BASE", skillCfg, "EnemyFreezeChance") + enemyDB:Sum("BASE", nil, "SelfFreezeChance"))
local sourceDmg = 0
if canDeal.Cold and not modDB:Sum("FLAG", skillCfg, "ColdCannotFreeze") then
sourceDmg = sourceDmg + output.ColdAverage
end
if canDeal.Lightning and modDB:Sum("FLAG", skillCfg, "LightningCanFreeze") then
sourceDmg = sourceDmg + output.LightningAverage
end
if output.FreezeChance > 0 and sourceDmg > 0 then
skillFlags.freeze = true
output.FreezeDurationMod = 1 + modDB:Sum("INC", dotCfg, "EnemyFreezeDuration") / 100 + enemyDB:Sum("INC", nil, "SelfFreezeDuration") / 100
end
end
-- Calculate enemy stun modifiers
do
local enemyStunThresholdRed = -modDB:Sum("INC", skillCfg, "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", skillCfg, "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
-- Calculate combined DPS estimate, including DoTs
output.CombinedDPS = output[(env.mode_average and "AverageDamage") or "TotalDPS"] + output.TotalDot
if skillFlags.poison then
if env.mode_average then
output.CombinedDPS = output.CombinedDPS + output.PoisonChance / 100 * output.PoisonDamage
output.WithPoisonAverageHit = output.CombinedDPS
else
output.CombinedDPS = output.CombinedDPS + output.PoisonChance / 100 * output.PoisonDamage * (output.HitSpeed or output.Speed)
output.WithPoisonDPS = output.CombinedDPS
end
end
if skillFlags.ignite then
output.CombinedDPS = output.CombinedDPS + output.IgniteDPS
end
if skillFlags.bleed then
output.CombinedDPS = output.CombinedDPS + output.BleedDPS
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
ConPrintf("%s = %s", name, tostring(env.output[name]))
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
if fullInit then
mergeMainMods(env)
end
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)
if not fullInit then
mergeMainMods(env)
end
-- 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 calculator for tree node modifiers
function calcs.getNodeCalculator(build)
return getCalculator(build, true, function(env, nodeList, remove)
-- Build and merge/unmerge modifiers for these nodes
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 item modifiers
function calcs.getItemCalculator(build)
return getCalculator(build, false, function(env, repSlotName, repItem)
-- Merge main mods, replacing the item in the given slot with the given item
mergeMainMods(env, repSlotName, repItem)
end)
end
-- Build output for display in the side bar or calcs tab
function calcs.buildOutput(build, mode)
-- Build output
local env = initEnv(build, mode)
mergeMainMods(env)
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
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.buffModList or activeSkill.auraModList 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.debuffModList or activeSkill.curseModList then
if activeSkill.skillFlags.multiPart then
t_insert(curseList, activeSkill.activeGem.name .. " (" .. activeSkill.skillPartName .. ")")
else
t_insert(curseList, activeSkill.activeGem.name)
end
end
end
output.BuffList = table.concat(buffList, ", ")
output.CombatList = table.concat(combatList, ", ")
output.CurseList = table.concat(curseList, ", ")
infoDump(env)
end
return env
end
return calcs