Files
PathOfBuilding/Modules/Calcs.lua
Openarl aa192f7bcd Release 1.3.6
- Added skill part to Barrage
- Added crit overcap info to crit breakdown
- Default gem level now varies according to the gem's max level
- Fixed quality bug introduced in 1.3.5
2017-02-21 22:19:12 +10:00

3138 lines
116 KiB
Lua

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