Files
PathOfBuilding/Modules/Calcs.lua
Openarl fc7d9036a6 Release 1.16
- Fixed bug with jewel attribute totals
2016-10-02 22:50:15 +10:00

2035 lines
73 KiB
Lua

-- Path of Building
--
-- Module: Calcs
-- Performs all the offense and defense calculations.
-- Here be dragons!
--
local grid = ...
local pairs = pairs
local ipairs = ipairs
local t_insert = table.insert
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 mod_listMerge = modLib.listMerge
local mod_listScaleMerge = modLib.listScaleMerge
local mod_dbMerge = modLib.dbMerge
local mod_dbScaleMerge = modLib.dbScaleMerge
local mod_dbUnmerge = modLib.dbUnmerge
local mod_dbMergeList = modLib.dbMergeList
local mod_dbScaleMergeList = modLib.dbScaleMergeList
local mod_dbUnmergeList = modLib.dbUnmergeList
local setViewMode = LoadModule("Modules/CalcsView", grid)
local isElemental = { fire = true, cold = true, lightning = true }
-- List of all damage types, ordered according to the conversion sequence
local dmgTypeList = {"physical", "lightning", "cold", "fire", "chaos"}
-- Combine specified modifiers from all current namespaces
local function sumMods(modDB, mult, ...)
local activeWatchers = modDB._activeWatchers
local val = mult and 1 or 0
for i = 1, select('#', ...) do
local modName = select(i, ...)
if modName then
for space, spaceName in pairs(modDB._spaces) do
local modVal = space[modName]
if modVal then
val = mult and (val * modVal) or (val + modVal)
end
if activeWatchers then
local fullName = (spaceName and spaceName.."_" or "") .. modName
for watchList in pairs(activeWatchers) do
watchList[fullName] = mult and 1 or 0
end
end
end
end
end
return val
end
-- Get value of misc modifier
local function getMiscVal(modDB, spaceName, modName, default)
local space = modDB[spaceName or "global"]
local val = default
if space and space[modName] ~= nil then
val = space[modName]
end
if modDB._activeWatchers then
local fullName = (spaceName and spaceName.."_" or "") .. modName
for watchList in pairs(modDB._activeWatchers) do
watchList[fullName] = default
end
if not space then
modDB[spaceName] = { }
end
end
return val
end
-- Calculate value, optionally adding additional base
local function calcVal(modDB, name, base)
local baseVal = sumMods(modDB, false, name.."Base") + (base or 0)
return baseVal * (1 + (sumMods(modDB, false, name.."Inc")) / 100) * sumMods(modDB, true, name.."More")
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) / 100, 0.95), 0.05)
end
-- Merge gem modifiers with given mod list
local function mergeGemMods(modList, gem)
for k, v in pairs(gem.data.base) do
mod_listMerge(modList, k, v)
end
for k, v in pairs(gem.data.quality) do
mod_listMerge(modList, k, m_floor(v * gem.quality))
end
gem.level = m_min(m_max(gem.level, 1), #gem.data.levels)
for k, v in pairs(gem.data.levels[gem.level]) do
mod_listMerge(modList, k, v)
end
end
-- Generate active namespace table
local function buildSpaceTable(modDB, spaceFlags)
modDB._spaces = { [modDB.global] = false }
if spaceFlags then
for spaceName, val in pairs(spaceFlags) do
if val then
modDB[spaceName] = modDB[spaceName] or { }
if modDB._activeWatchers or next(modDB[spaceName]) then
modDB._spaces[modDB[spaceName]] = spaceName
end
end
end
end
end
-- Start watch section
local function startWatch(env, key, ...)
if env.buildWatch then
-- Running in build mode
-- In this mode, any modifiers read while this section is active will be tracked
env.watchers[key] = { _key = key }
env.modDB._activeWatchers[env.watchers[key] ] = true
return true
else
local watchers = env.watchers
if not watchers or env.spacesChanged then
-- Watch system is disabled or skill namespaces have changed, so all sections will run by default
return true
end
-- This section will be flagged if any modifiers read during build mode have changed since the build mode pass
if not watchers[key] or watchers[key]._flag then
return true
end
for i = 1, select('#', ...) do
-- Check if any dependant sections have been flagged
if watchers[select(i, ...)]._flag then
return true
end
end
return false
end
end
-- End watch section
local function endWatch(env, key)
if env.buildWatch and env.watchers[key] then
env.modDB._activeWatchers[env.watchers[key] ] = nil
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.requireFunc then
setfenv(gem.data.requireFunc, activeSkill.baseFlags)
return gem.data.requireFunc() == true
else
return true
end
end
-- Check if given gem is of the given type ("all", "strength", "melee", etc)
local function gemIsType(gem, type)
return type == "all" or (type == "active" and not gem.data.support) 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,
srcGem = activeGem,
}
activeSkill.gemList = { activeSkill.activeGem }
-- Build base skill flag set ('attack', 'projectile', etc)
local baseFlags = { }
activeSkill.baseFlags = baseFlags
for k, v in pairs(activeSkill.activeGem.data) do
if v == true then
baseFlags[k] = true
end
end
if baseFlags.hit then
baseFlags.damage = true
end
if baseFlags.bow then
baseFlags.projectile = true
end
for _, gem in ipairs(supportList) do
if gem.data.addFlags and gemCanSupport(gem, activeSkill) then
-- Support gem adds flags to supported skills (eg. Remote Mine adds 'mine')
for k in pairs(gem.data.addFlags) do
baseFlags[k] = true
end
end
end
-- Process support gems
for _, gem in ipairs(supportList) do
if gemCanSupport(gem, activeSkill) then
t_insert(activeSkill.gemList, {
name = gem.name,
data = gem.data,
level = gem.level,
quality = gem.quality,
fromItem = gem.fromItem,
srcGem = gem,
})
if gem.isSupporting then
gem.isSupporting[activeGem.name] = true
end
end
end
return activeSkill
end
-- Build list of modifiers for given active skill
local function buildActiveSkillModList(env, activeSkill)
local skillModList = { }
activeSkill.skillModList = skillModList
if activeSkill.socketBonuses then
-- Apply local skill modifiers from the item this skill is socketed into
for type, val in pairs(activeSkill.socketBonuses.gemLevel) do
for _, gem in pairs(activeSkill.gemList) do
if not gem.fromItem and gemIsType(gem, type) then
gem.level = gem.level + val
end
end
end
for type, val in pairs(activeSkill.socketBonuses.gemQuality) do
for _, gem in pairs(activeSkill.gemList) do
if not gem.fromItem and gemIsType(gem, type) then
gem.quality = gem.quality + val
end
end
end
for type, modList in pairs(activeSkill.socketBonuses.modList) do
if gemIsType(activeSkill.activeGem, type) then
for k, v in pairs(modList) do
mod_listMerge(skillModList, k, v)
end
end
end
end
-- Merge skill-specific modifiers
local skillSpace = env.modDB["Skill:"..activeSkill.activeGem.name]
if skillSpace then
for k, v in pairs(skillSpace) do
mod_listMerge(skillModList, k, v)
end
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
activeSkill.activeGem.level = activeSkill.activeGem.level + (skillModList.gemLevel_active or 0)
activeSkill.activeGem.quality = activeSkill.activeGem.quality + (skillModList.gemQuality_active or 0)
-- Add active gem modifiers
mergeGemMods(skillModList, activeSkill.activeGem)
-- Separate auxillary modifiers (mods that can affect defensive stats or other skills)
activeSkill.buffModList = { }
activeSkill.auraModList = { }
activeSkill.curseModList = { }
for k, v in pairs(skillModList) do
local spaceName, modName = modLib.getSpaceName(k)
if spaceName == "BuffEffect" then
mod_listMerge(activeSkill.buffModList, modName, v)
elseif spaceName == "AuraEffect" then
mod_listMerge(activeSkill.auraModList, modName, v)
elseif spaceName == "CurseEffect" then
mod_listMerge(activeSkill.curseModList, modName, v)
end
end
if activeSkill ~= env.mainSkill then
-- Add to auxillary skill list
t_insert(env.auxSkillList, activeSkill)
end
-- Handle multipart skills
activeSkill.skillPartList = { }
local activeGemParts = activeSkill.activeGem.data.parts
if activeGemParts then
for i, part in pairs(activeGemParts) do
activeSkill.skillPartList[i] = part.name or ""
end
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
activeSkill.baseFlags[k] = true
elseif v == false then
activeSkill.baseFlags[k] = nil
end
end
activeSkill.baseFlags.multiPart = #activeGemParts > 1
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 = { }
for _, node in pairs(nodeList) do
-- Merge with output list
for k, v in pairs(node.modList) do
mod_listMerge(modList, k, v)
end
-- Run radius jewels
for _, rad in pairs(env.radiusJewelList) do
local vX, vY = node.x - rad.x, node.y - rad.y
if vX * vX + vY * vY <= rad.rSq then
rad.func(node.modList, modList, rad.data)
end
end
end
if finishJewels then
-- Finalise radius jewels
for _, rad in pairs(env.radiusJewelList) do
rad.func(nil, modList, rad.data)
if env.mode == "MAIN" then
if not rad.item.jewelRadiusData then
rad.item.jewelRadiusData = { }
end
rad.item.jewelRadiusData[rad.nodeId] = rad.data
end
end
end
return modList
end
-- Calculate min/max damage of a hit for the given damage type
local function calcHitDamage(env, output, damageType, ...)
local modDB = env.modDB
local isAttack = (env.mode_skillType == "ATTACK")
local damageTypeMin = damageType.."Min"
local damageTypeMax = damageType.."Max"
-- Calculate base values
local baseMin, baseMax
if isAttack then
baseMin = getMiscVal(modDB, "weapon1", damageTypeMin, 0) + sumMods(modDB, false, damageTypeMin)
baseMax = getMiscVal(modDB, "weapon1", damageTypeMax, 0) + sumMods(modDB, false, damageTypeMax)
else
local damageEffectiveness = getMiscVal(modDB, "skill", "damageEffectiveness", 0)
if damageEffectiveness == 0 then
damageEffectiveness = 1
end
baseMin = getMiscVal(modDB, "skill", damageTypeMin, 0) + sumMods(modDB, false, damageTypeMin) * damageEffectiveness
baseMax = getMiscVal(modDB, "skill", damageTypeMax, 0) + sumMods(modDB, false, damageTypeMax) * damageEffectiveness
end
-- Build lists of applicable modifier names
local addElemental = isElemental[damageType]
local inc = { damageType.."Inc", "damageInc" }
local more = { damageType.."More", "damageMore" }
local damageTypeStr = "total_"..damageType
for i = 1, select('#', ...) do
local dstElem = select(i, ...)
damageTypeStr = damageTypeStr..dstElem
-- Add modifiers for damage types to which this damage is being converted
addElemental = addElemental or isElemental[dstElem]
t_insert(inc, dstElem.."Inc")
t_insert(more, dstElem.."More")
end
if addElemental then
-- Damage is elemental or is being converted to elemental damage, add global elemental modifiers
t_insert(inc, "elementalInc")
t_insert(more, "elementalMore")
end
-- Combine modifiers
local damageTypeStrInc = damageTypeStr.."Inc"
if startWatch(env, damageTypeStrInc) then
output[damageTypeStrInc] = 1 + sumMods(modDB, false, unpack(inc)) / 100
endWatch(env, damageTypeStrInc)
end
local damageTypeStrMore = damageTypeStr.."More"
if startWatch(env, damageTypeStrMore) then
output[damageTypeStrMore] = m_floor(sumMods(modDB, true, unpack(more)) * 100 + 0.50000001) / 100
endWatch(env, damageTypeStrMore)
end
-- Calculate conversions
if startWatch(env, damageTypeStr.."Conv", "conversionTable") then
local addMin, addMax = 0, 0
local conversionTable = output.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, output, otherType, damageType, ...)
addMin = addMin + min * convMult
addMax = addMax + max * convMult
end
end
output[damageTypeStr.."ConvAddMin"] = addMin
output[damageTypeStr.."ConvAddMax"] = addMax
endWatch(env, damageTypeStr.."Conv")
end
local modMult = output[damageTypeStrInc] * output[damageTypeStrMore]
return (m_floor(baseMin * modMult + 0.5) + output[damageTypeStr.."ConvAddMin"]),
(m_floor(baseMax * modMult + 0.5) + output[damageTypeStr.."ConvAddMax"])
end
--
-- The following functions perform various steps in the calculations process.
-- Depending on what is being done with the output, other code may run inbetween steps, however the steps must always be performed in order:
-- 1. Initialise environment (initEnv)
-- 2. Merge main modifiers (mergeMainMods)
-- 3. Finalise modifier database (finaliseMods)
-- 4. Run calculations (performCalcs)
--
-- Thus a basic calculation pass would look like this:
--
-- local env = initEnv(input, build)
-- mergeMainMods(env)
-- finaliseMods(env, output)
-- performCalcs(env, output)
--
-- Initialise environment
-- This will initialise the modifier database
local function initEnv(build, input, mode)
local env = { }
env.build = build
env.input = input
env.mode = mode
env.classId = build.spec.curClassId
-- Initialise modifier database with base values
local modDB = { }
env.modDB = modDB
local classStats = build.tree.characterData[env.classId]
for _, stat in pairs({"str","dex","int"}) do
mod_dbMerge(modDB, "", stat.."Base", classStats["base_"..stat])
end
local level = build.characterLevel
mod_dbMerge(modDB, "", "lifeBase", 38 + level * 12)
mod_dbMerge(modDB, "", "manaBase", 34 + level * 6)
mod_dbMerge(modDB, "", "evasionBase", 53 + level * 3)
mod_dbMerge(modDB, "", "accuracyBase", (level - 1) * 2)
mod_dbMerge(modDB, "", "fireResistMax", 75)
mod_dbMerge(modDB, "", "coldResistMax", 75)
mod_dbMerge(modDB, "", "lightningResistMax", 75)
mod_dbMerge(modDB, "", "chaosResistMax", 75)
mod_dbMerge(modDB, "", "blockChanceMax", 75)
mod_dbMerge(modDB, "", "powerMax", 3)
mod_dbMerge(modDB, "PerPower", "critChanceInc", 50)
mod_dbMerge(modDB, "", "frenzyMax", 3)
mod_dbMerge(modDB, "PerFrenzy", "speedInc", 4)
mod_dbMerge(modDB, "PerFrenzy", "damageMore", 1.04)
mod_dbMerge(modDB, "", "enduranceMax", 3)
mod_dbMerge(modDB, "PerEndurance", "elementalResist", 4)
mod_dbMerge(modDB, "", "activeTrapLimit", 3)
mod_dbMerge(modDB, "", "activeMineLimit", 5)
mod_dbMerge(modDB, "", "projectileCount", 1)
mod_dbMerge(modDB, "CondMod", "DualWielding_attackSpeedMore", 1.1)
mod_dbMerge(modDB, "CondMod", "DualWielding_attack_physicalMore", 1.2)
mod_dbMerge(modDB, "CondMod", "DualWielding_blockChance", 15)
-- Add bandit mods
if build.banditNormal == "Alira" then
mod_dbMerge(modDB, "", "manaBase", 60)
elseif build.banditNormal == "Kraityn" then
mod_dbMerge(modDB, "", "elementalResist", 10)
elseif build.banditNormal == "Oak" then
mod_dbMerge(modDB, "", "lifeBase", 40)
else
mod_dbMerge(modDB, "", "extraPoints", 1)
end
if build.banditCruel == "Alira" then
mod_dbMerge(modDB, "", "castSpeedInc", 5)
elseif build.banditCruel == "Kraityn" then
mod_dbMerge(modDB, "", "attackSpeedInc", 8)
elseif build.banditCruel == "Oak" then
mod_dbMerge(modDB, "", "physicalInc", 16)
else
mod_dbMerge(modDB, "", "extraPoints", 1)
end
if build.banditMerciless == "Alira" then
mod_dbMerge(modDB, "", "powerMax", 1)
elseif build.banditMerciless == "Kraityn" then
mod_dbMerge(modDB, "", "frenzyMax", 1)
elseif build.banditMerciless == "Oak" then
mod_dbMerge(modDB, "", "enduranceMax", 1)
else
mod_dbMerge(modDB, "", "extraPoints", 1)
end
-- Add mods from the input table
mod_dbMergeList(modDB, input)
return env
end
-- This function:
-- 1. Merges modifiers for all items, optionally replacing one item
-- 2. Builds a list of jewels with radius functions
-- 3. Merges modifiers for all allocated passive nodes
-- 4. Builds a list of active skills and their supports
-- 5. Builds modifier lists for all active skills
local function mergeMainMods(env, repSlotName, repItem)
local build = env.build
-- Build and merge item modifiers, and create list of radius jewels
env.itemModList = wipeTable(env.itemModList)
env.radiusJewelList = wipeTable(env.radiusJewelList)
for slotName, slot in pairs(build.itemsTab.slots) do
local item
if slotName == repSlotName then
item = repItem
else
item = build.itemsTab.list[slot.selItemId]
end
if slot.nodeId then
-- Slot is a jewel socket, check if socket is allocated
if not build.spec.allocNodes[slot.nodeId] then
item = nil
elseif item and item.jewelRadiusIndex then
-- Jewel has a radius, add it to the list
local func = item.jewelFunc or function(nodeMods, out, data)
-- Default function just tallies all stats in radius
if nodeMods then
data.strBase = (data.strBase or 0) + (nodeMods.strBase or 0)
data.dexBase = (data.dexBase or 0) + (nodeMods.dexBase or 0)
data.intBase = (data.intBase or 0) + (nodeMods.intBase or 0)
end
end
local radiusInfo = data.jewelRadius[item.jewelRadiusIndex]
local node = build.spec.nodes[slot.nodeId]
t_insert(env.radiusJewelList, {
rSq = radiusInfo.rad * radiusInfo.rad,
x = node.x,
y = node.y,
func = func,
item = item,
nodeId = slot.nodeId,
data = { }
})
end
end
if item then
-- Merge mods for this item into the global item mod list
local srcList = item.modList or item.slotModList[slot.slotNum]
for k, v in pairs(srcList) do
mod_listMerge(env.itemModList, k, v)
end
if item.type ~= "Jewel" then
-- Update item counts
if item.rarity == "UNIQUE" then
mod_listMerge(env.itemModList, "gear_UniqueCount", 1)
elseif item.rarity == "RARE" then
mod_listMerge(env.itemModList, "gear_RareCount", 1)
elseif item.rarity == "MAGIC" then
mod_listMerge(env.itemModList, "gear_MagicCount", 1)
else
mod_listMerge(env.itemModList, "gear_NormalCount", 1)
end
end
end
end
mod_dbMergeList(env.modDB, env.itemModList)
-- Build and merge modifiers for allocated passives
env.specModList = buildNodeModList(env, build.spec.allocNodes, true)
mod_dbMergeList(env.modDB, env.specModList)
-- Determine main skill group
if env.mode == "GRID" then
env.input.skill_number = m_min(m_max(#build.skillsTab.socketGroupList, 1), env.input.skill_number or 1)
env.mainSocketGroup = env.input.skill_number
env.skillPart = env.input.skill_part or 1
env.buffMode = env.input.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 bonuses to socketed gems
local slotSocketBonuses = { }
for slotName in pairs(build.itemsTab.slots) do
if env.modDB["SocketedIn:"..slotName] then
slotSocketBonuses[slotName] = {
supports = { },
gemLevel = { },
gemQuality = { },
modList = { }
}
for k, v in pairs(env.modDB["SocketedIn:"..slotName]) do
local spaceName, modName = modLib.getSpaceName(k)
if spaceName == "supportedBy" then
local level, support = modName:match("(%d+):(.+)")
if level then
for gemName, gemData in pairs(data.gems) do
if support == gemName:lower() then
t_insert(slotSocketBonuses[slotName].supports, { name = gemName, data = gemData, level = tonumber(level), quality = 0, enabled = true, fromItem = true })
break
end
end
end
elseif spaceName == "gemLevel" then
slotSocketBonuses[slotName].gemLevel[modName] = v
elseif spaceName == "gemQuality" then
slotSocketBonuses[slotName].gemQuality[modName] = v
else
if not slotSocketBonuses[slotName].modList[spaceName] then
slotSocketBonuses[slotName].modList[spaceName] = { }
end
mod_listMerge(slotSocketBonuses[slotName].modList[spaceName], modName, v)
end
end
end
end
-- Build list of active skills
env.activeSkillList = { }
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 socketBonuses = socketGroup.slot and slotSocketBonuses[socketGroup.slot]
local supportList = { }
if socketBonuses then
for _, gem in ipairs(socketBonuses.supports) do
t_insert(supportList, gem)
end
end
for _, gem in ipairs(socketGroup.gemList) do
if gem.enabled and gem.data and gem.data.support then
local add = true
for _, otherGem in pairs(supportList) do
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 then
local activeSkill = createActiveSkill(gem, supportList)
activeSkill.socketBonuses = socketBonuses
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 == "GRID" then
env.input.skill_activeNumber = m_min(#socketGroupSkillList, env.input.skill_activeNumber or 1)
activeSkillIndex = env.input.skill_activeNumber
else
socketGroup.mainActiveSkill = m_min(#socketGroupSkillList, socketGroup.mainActiveSkill or 1)
activeSkillIndex = socketGroup.mainActiveSkill
end
env.mainSkill = socketGroupSkillList[activeSkillIndex]
env.mainSkillGroupActiveSkillList = socketGroupSkillList
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
env.setupFunc = env.mainSkill.activeGem.data.setupFunc
-- Build skill modifier lists
env.auxSkillList = { }
for _, activeSkill in pairs(env.activeSkillList) do
buildActiveSkillModList(env, activeSkill)
end
end
-- Prepare environment for calculations
local function finaliseMods(env, output)
local modDB = env.modDB
local weapon1Type = getMiscVal(modDB, "weapon1", "type", "None")
local weapon2Type = getMiscVal(modDB, "weapon2", "type", "")
if weapon1Type == output.weapon1Type and weapon2Type == output.weapon2Type then
env.spacesChanged = false
else
env.spacesChanged = true
output.weapon1Type = weapon1Type
output.weapon2Type = weapon2Type
-- Initialise skill flag set
env.skillFlags = wipeTable(env.skillFlags)
local skillFlags = env.skillFlags
for k, v in pairs(env.mainSkill.baseFlags) do
skillFlags[k] = v
end
-- Set weapon flags
skillFlags.mainIs1H = true
local weapon1Info = data.weaponTypeInfo[weapon1Type]
if weapon1Info then
if not weapon1Info.oneHand then
skillFlags.mainIs1H = nil
end
if skillFlags.attack then
skillFlags.weapon1Attack = true
if weapon1Info.melee and skillFlags.melee then
skillFlags.bow = nil
skillFlags.projectile = nil
elseif not weapon1Info.melee and skillFlags.bow then
skillFlags.melee = nil
end
end
end
local weapon2Info = data.weaponTypeInfo[weapon2Type]
if weapon2Info and skillFlags.mainIs1H then
if skillFlags.attack then
skillFlags.weapon2Attack = true
end
end
-- Build list of namespaces to search for mods
local skillSpaceFlags = wipeTable(env.skillSpaceFlags)
env.skillSpaceFlags = skillSpaceFlags
skillSpaceFlags["hit"] = true
if skillFlags.spell then
skillSpaceFlags["spell"] = true
elseif skillFlags.attack then
skillSpaceFlags["attack"] = true
end
if skillFlags.weapon1Attack then
if getMiscVal(modDB, "weapon1", "varunastra", false) then
skillSpaceFlags["axe"] = true
skillSpaceFlags["claw"] = true
skillSpaceFlags["dagger"] = true
skillSpaceFlags["mace"] = true
skillSpaceFlags["sword"] = true
else
skillSpaceFlags[weapon1Info.space] = true
end
if weapon1Type ~= "None" then
skillSpaceFlags["weapon"] = true
if skillFlags.mainIs1H then
skillSpaceFlags["weapon1h"] = true
if weapon1Info.melee then
skillSpaceFlags["weapon1hMelee"] = true
else
skillSpaceFlags["weaponRanged"] = true
end
else
skillSpaceFlags["weapon2h"] = true
if weapon1Info.melee then
skillSpaceFlags["weapon2hMelee"] = true
else
skillSpaceFlags["weaponRanged"] = true
end
end
end
end
if skillFlags.melee then
skillSpaceFlags["melee"] = true
elseif skillFlags.projectile then
skillSpaceFlags["projectile"] = true
if skillFlags.attack then
skillSpaceFlags["projectileAttack"] = true
end
end
if skillFlags.totem then
skillSpaceFlags["totem"] = true
elseif skillFlags.trap then
skillSpaceFlags["trap"] = true
skillSpaceFlags["trapHit"] = true
elseif skillFlags.mine then
skillSpaceFlags["mine"] = true
skillSpaceFlags["mineHit"] = true
end
if skillFlags.aoe then
skillSpaceFlags["aoe"] = true
end
if skillFlags.debuff then
skillSpaceFlags["debuff"] = true
end
if skillFlags.aura then
skillSpaceFlags["aura"] = true
end
if skillFlags.curse then
skillSpaceFlags["curse"] = true
end
if skillFlags.warcry then
skillSpaceFlags["warcry"] = true
end
if skillFlags.movement then
skillSpaceFlags["movementSkills"] = true
end
if skillFlags.lightning then
skillSpaceFlags["lightningSkills"] = true
skillSpaceFlags["elementalSkills"] = true
end
if skillFlags.cold then
skillSpaceFlags["coldSkills"] = true
skillSpaceFlags["elementalSkills"] = true
end
if skillFlags.fire then
skillSpaceFlags["fireSkills"] = true
skillSpaceFlags["elementalSkills"] = true
end
if skillFlags.chaos then
skillSpaceFlags["chaosSkills"] = true
end
end
if weapon1Type == "None" then
-- Merge unarmed weapon modifiers
for k, v in pairs(data.unarmedWeapon[env.classId]) do
mod_dbMerge(modDB, "weapon1", k, v)
end
end
-- Set modes
if env.mainSkill.baseFlags.attack then
env.mode_skillType = "ATTACK"
else
env.mode_skillType = "SPELL"
end
if env.skillFlags.showAverage then
env.mode_average = true
end
if env.buffMode == "BUFFED" then
env.mode_buffs = true
env.mode_effective = false
elseif env.buffMode == "EFFECTIVE" then
env.mode_buffs = true
env.mode_effective = true
else
env.mode_buffs = false
env.mode_effective = false
end
-- Reset namespaces
buildSpaceTable(modDB)
-- Add boss modifiers
if getMiscVal(modDB, "effective", "enemyIsBoss", false) then
mod_dbMerge(modDB, "", "curseEffectMore", 0.4)
mod_dbMerge(modDB, "effective", "elementalResist", 30)
mod_dbMerge(modDB, "effective", "chaosResist", 15)
end
-- Merge auxillary skill modifiers and calculate skill life and mana reservations
for _, activeSkill in pairs(env.activeSkillList) do
local skillModList = activeSkill.skillModList
-- Merge auxillary modifiers
mod_dbMergeList(modDB, activeSkill.buffModList)
local auraEffect = (1 + (getMiscVal(modDB, nil, "auraEffectInc", 0) + (skillModList.auraEffectInc or 0)) / 100) * getMiscVal(modDB, nil, "auraEffectMore", 1) * (skillModList.auraEffectMore or 1)
mod_dbScaleMergeList(modDB, activeSkill.auraModList, auraEffect)
if env.mode_effective then
local curseEffect = (1 + (getMiscVal(modDB, nil, "curseEffectInc", 0) + (skillModList.curseEffectInc or 0)) / 100) * getMiscVal(modDB, nil, "curseEffectMore", 1) * (skillModList.curseEffectMore or 1)
mod_dbScaleMergeList(modDB, activeSkill.curseModList, curseEffect)
end
-- Calculate reservations
local baseVal, suffix
baseVal = skillModList.skill_manaReservedBase
if baseVal then
suffix = "Base"
else
baseVal = skillModList.skill_manaReservedPercent
if baseVal then
suffix = "Percent"
end
end
if baseVal then
local more = sumMods(modDB, true, "manaReservedMore") * (skillModList.manaReservedMore or 1)
local inc = sumMods(modDB, false, "manaReservedInc") + (skillModList.manaReservedInc or 0)
local cost = m_ceil(m_ceil(m_floor(baseVal * (skillModList.manaCostMore or 1)) * more) * (1 + inc / 100))
if getMiscVal(modDB, nil, "bloodMagic", false) or skillModList.skill_bloodMagic then
mod_dbMerge(modDB, "reserved", "life"..suffix, cost)
else
mod_dbMerge(modDB, "reserved", "mana"..suffix, cost)
end
end
end
-- Merge main skill mods
mod_dbMergeList(modDB, env.mainSkill.skillModList)
if env.mainSkill.baseFlags.multiPart and modDB["SkillPart"..env.mainSkill.skillPart] then
mod_dbMergeList(modDB, modDB["SkillPart"..env.mainSkill.skillPart])
end
-- Merge gear-sourced keystone modifiers
if modDB.gear then
for name, node in pairs(env.build.tree.keystoneMap) do
if getMiscVal(modDB, "gear", "keystone:"..name, false) and not getMiscVal(modDB, nil, "keystone:"..name, false) then
-- Keystone is granted by gear but not allocated on tree, so add its modifiers
mod_dbMergeList(modDB, buildNodeModList(env, { node }))
end
end
end
-- Build condition list
local condList = wipeTable(env.condList)
env.condList = condList
if weapon1Type == "Staff" then
condList["UsingStaff"] = true
end
if env.skillFlags.mainIs1H and weapon2Type == "Shield" then
condList["UsingShield"] = true
end
if data.weaponTypeInfo[weapon1Type] and data.weaponTypeInfo[weapon2Type] then
condList["DualWielding"] = true
end
if weapon1Type == "None" and not data.weaponTypeInfo[weapon2Type] then
condList["Unarmed"] = true
end
if getMiscVal(modDB, "gear", "NormalCount", 0) > 0 then
condList["UsingNormalItem"] = true
end
if getMiscVal(modDB, "gear", "MagicCount", 0) > 0 then
condList["UsingMagicItem"] = true
end
if getMiscVal(modDB, "gear", "RareCount", 0) > 0 then
condList["UsingRareItem"] = true
end
if getMiscVal(modDB, "gear", "UniqueCount", 0) > 0 then
condList["UsingUniqueItem"] = true
end
if getMiscVal(modDB, "reserved", "manaBase", 0) == 0 and getMiscVal(modDB, "reserved", "manaPercent", 0) == 0 then
condList["NoManaReserved"] = true
end
if modDB.Cond then
for k, v in pairs(modDB.Cond) do
condList[k] = v
if v then
env.skillFlags[k] = true
end
end
end
if env.mode_buffs then
if modDB.CondBuff then
for k, v in pairs(modDB.CondBuff) do
condList[k] = v
if v then
env.skillFlags[k] = true
end
end
end
if modDB.CondEff and env.mode_effective then
for k, v in pairs(modDB.CondEff) do
condList[k] = v
if v then
env.skillFlags[k] = true
end
end
mod_dbMerge(modDB, "CondMod", "EnemyShocked_effective_damageTakenInc", 50)
condList["EnemyFrozenShockedIgnited"] = condList["EnemyFrozen"] or condList["EnemyShocked"] or condList["EnemyIgnited"]
condList["EnemyElementalStatus"] = condList["EnemyChilled"] or condList["EnemyFrozen"] or condList["EnemyShocked"] or condList["EnemyIgnited"]
end
if not getMiscVal(modDB, nil, "neverCrit", false) then
condList["CritInPast8Sec"] = true
end
if env.skillFlags.attack then
condList["AttackedRecently"] = true
elseif env.skillFlags.spell then
condList["CastSpellRecently"] = true
end
if env.skillFlags.movement then
condList["UsedMovementSkillRecently"] = true
end
if env.skillFlags.totem then
condList["SummonedTotemRecently"] = true
end
if env.skillFlags.mine then
condList["DetonatedMinesRecently"] = true
end
end
-- Build and merge conditional modifier list
local condModList = wipeTable(env.condModList)
env.condModList = condModList
if modDB.CondMod then
for k, v in pairs(modDB.CondMod) do
local isNot, condName, modName = modLib.getCondName(k)
if (isNot and not condList[condName]) or (not isNot and condList[condName]) then
mod_listMerge(condModList, modName, v)
end
end
end
if modDB.CondEffMod and env.mode_effective then
for k, v in pairs(modDB.CondEffMod) do
local isNot, condName, modName = modLib.getCondName(k)
if (isNot and not condList[condName]) or (not isNot and condList[condName]) then
mod_listMerge(condModList, modName, v)
end
end
end
mod_dbMergeList(modDB, env.condModList)
-- Add per-item-type mods
for spaceName, countName in pairs({["PerNormal"]="NormalCount",["PerMagic"]="MagicCount",["PerRare"]="RareCount",["PerUnique"]="UniqueCount",["PerGrandSpectrum"]="GrandSpectrumCount"}) do
local space = modDB[spaceName]
if space then
local count = getMiscVal(modDB, "gear", countName, 0)
for k, v in pairs(space) do
mod_dbScaleMerge(modDB, "", k, v, count)
end
end
end
-- Calculate maximum charges
if getMiscVal(modDB, "buff", "power", false) then
env.skillFlags.havePower = true
output.powerMax = getMiscVal(modDB, nil, "powerMax", 0)
end
if getMiscVal(modDB, "buff", "frenzy", false) then
env.skillFlags.haveFrenzy = true
output.frenzyMax = getMiscVal(modDB, nil, "frenzyMax", 0)
end
if getMiscVal(modDB, "buff", "endurance", false) then
env.skillFlags.haveEndurance = true
output.enduranceMax = getMiscVal(modDB, nil, "enduranceMax", 0)
end
if env.mode_buffs then
-- Build buff mod list
local buffModList = wipeTable(env.buffModList)
env.buffModList = buffModList
-- Calculate total charge bonuses
if env.skillFlags.havePower then
for k, v in pairs(modDB.PerPower) do
mod_listScaleMerge(buffModList, k, v, output.powerMax)
end
end
if env.skillFlags.haveFrenzy then
for k, v in pairs(modDB.PerFrenzy) do
mod_listScaleMerge(buffModList, k, v, output.frenzyMax)
end
end
if env.skillFlags.haveEndurance then
for k, v in pairs(modDB.PerEndurance) do
mod_listScaleMerge(buffModList, k, v, output.enduranceMax)
end
end
-- Add other buffs
if env.condList["Onslaught"] then
local effect = m_floor(20 * (1 + sumMods(modDB, false, "onslaughtEffectInc") / 100))
mod_listMerge(buffModList, "attackSpeedInc", effect)
mod_listMerge(buffModList, "castSpeedInc", effect)
mod_listMerge(buffModList, "movementSpeedInc", effect)
end
if getMiscVal(modDB, "buff", "pendulum", false) then
mod_listMerge(buffModList, "elementalInc", 100)
mod_listMerge(buffModList, "aoeRadiusInc", 25)
end
-- Merge buff bonuses
mod_dbMergeList(modDB, buffModList)
end
-- Calculate attributes
for _, stat in pairs({"str","dex","int"}) do
output["total_"..stat] = m_floor(calcVal(modDB, stat))
end
-- Add attribute bonuses
mod_dbMerge(modDB, "", "lifeBase", m_floor(output.total_str / 2))
local strDmgBonus = m_floor((output.total_str + getMiscVal(modDB, nil, "dexIntToMeleeBonus", 0)) / 5 + 0.5)
mod_dbMerge(modDB, "melee", "physicalInc", strDmgBonus)
if getMiscVal(modDB, nil, "ironGrip", false) then
mod_dbMerge(modDB, "projectileAttack", "physicalInc", strDmgBonus)
end
if getMiscVal(modDB, nil, "ironWill", false) then
mod_dbMerge(modDB, "spell", "damageInc", strDmgBonus)
end
mod_dbMerge(modDB, "", "accuracyBase", output.total_dex * 2)
if not getMiscVal(modDB, nil, "ironReflexes", false) then
mod_dbMerge(modDB, "", "evasionInc", m_floor(output.total_dex / 5 + 0.5))
end
mod_dbMerge(modDB, "", "manaBase", m_floor(output.total_int / 2 + 0.5))
mod_dbMerge(modDB, "", "energyShieldInc", m_floor(output.total_int / 5 + 0.5))
end
-- Calculate offence and defence stats
local function performCalcs(env, output)
local modDB = env.modDB
-- Calculate life/mana pools
if startWatch(env, "life") then
if getMiscVal(modDB, nil, "chaosInoculation", false) then
output.total_life = 1
else
output.total_life = calcVal(modDB, "life")
end
endWatch(env, "life")
end
if startWatch(env, "mana") then
output.total_mana = calcVal(modDB, "mana")
mod_dbMerge(modDB, "", "manaRegenBase", output.total_mana * 0.0175)
output.total_manaRegen = sumMods(modDB, false, "manaRegenBase") * (1 + sumMods(modDB, false, "manaRegenInc", "manaRecoveryInc") / 100) * sumMods(modDB, true, "manaRegenMore", "manaRecoveryMore")
endWatch(env, "mana")
end
-- Calculate life/mana reservation
for _, pool in pairs({"life", "mana"}) do
if startWatch(env, pool.."Reservation", pool) then
local max = output["total_"..pool]
local reserved = getMiscVal(modDB, "reserved", pool.."Base", 0) + m_floor(max * getMiscVal(modDB, "reserved", pool.."Percent", 0) / 100 + 0.5)
output["total_"..pool.."Reserved"] = reserved
output["total_"..pool.."ReservedPercent"] = reserved / max
output["total_"..pool.."Unreserved"] = max - reserved
output["total_"..pool.."UnreservedPercent"] = (max - reserved) / max
endWatch(env, pool.."Reservation")
end
end
-- Calculate primary defences
if startWatch(env, "energyShield", "mana") then
local convManaToES = getMiscVal(modDB, nil, "manaGainAsEnergyShield", 0)
if convManaToES > 0 then
output.total_energyShield = sumMods(modDB, false, "manaBase") * (1 + sumMods(modDB, false, "energyShieldInc", "defencesInc", "manaInc") / 100) * sumMods(modDB, true, "energyShieldMore", "defencesMore", "manaMore") * convManaToES / 100
else
output.total_energyShield = 0
end
local energyShieldFromReservedMana = getMiscVal(modDB, nil, "energyShieldFromReservedMana", 0)
if energyShieldFromReservedMana > 0 then
output.total_energyShield = output.total_energyShield + output.total_manaReserved * (1 + sumMods(modDB, false, "energyShieldInc", "defencesInc") / 100) * sumMods(modDB, true, "energyShieldMore", "defencesMore") * energyShieldFromReservedMana / 100
end
output.total_gear_energyShieldBase = env.itemModList.energyShieldBase or 0
for _, slot in pairs({"global","slot:Helmet","slot:Body Armour","slot:Gloves","slot:Boots","slot:Shield"}) do
buildSpaceTable(modDB, { [slot] = true })
local energyShieldBase = getMiscVal(modDB, slot, "energyShieldBase", 0)
if energyShieldBase > 0 then
output.total_energyShield = output.total_energyShield + energyShieldBase * (1 + sumMods(modDB, false, "energyShieldInc", "defencesInc") / 100) * sumMods(modDB, true, "energyShieldMore", "defencesMore")
end
if slot ~= "global" then
output.total_gear_energyShieldBase = output.total_gear_energyShieldBase + energyShieldBase
end
end
buildSpaceTable(modDB)
output.total_energyShieldRecharge = output.total_energyShield * 0.2 * (1 + sumMods(modDB, false, "energyShieldRechargeInc", "energyShieldRecoveryInc") / 100) * sumMods(modDB, true, "energyShieldRechargeMore", "energyShieldRecoveryMore")
output.total_energyShieldRechargeDelay = 2 / (1 + getMiscVal(modDB, nil, "energyShieldRechargeFaster", 0) / 100)
endWatch(env, "energyShield")
end
if startWatch(env, "armourEvasion", "life") then
output.total_evasion = 0
local armourFromReservedLife = getMiscVal(modDB, nil, "armourFromReservedLife", 0)
if armourFromReservedLife > 0 then
output.total_armour = output.total_lifeReserved * (1 + sumMods(modDB, false, "armourInc", "armourAndEvasionInc", "defencesInc") / 100) * sumMods(modDB, true, "armourMore", "defencesMore") * armourFromReservedLife / 100
else
output.total_armour = 0
end
output.total_gear_evasionBase = env.itemModList.evasionBase or 0
output.total_gear_armourBase = env.itemModList.armourBase or 0
local ironReflexes = getMiscVal(modDB, nil, "ironReflexes", false)
if getMiscVal(modDB, nil, "unbreakable", false) then
mod_dbMerge(modDB, "slot:Body Armour", "armourBase", getMiscVal(modDB, "slot:Body Armour", "armourBase", 0))
end
for _, slot in pairs({"global","slot:Helmet","slot:Body Armour","slot:Gloves","slot:Boots","slot:Shield"}) do
buildSpaceTable(modDB, { [slot] = true })
local evasionBase = getMiscVal(modDB, slot, "evasionBase", 0)
local bothBase = getMiscVal(modDB, slot, "armourAndEvasionBase", 0)
local armourBase = getMiscVal(modDB, slot, "armourBase", 0)
if ironReflexes then
if evasionBase > 0 or armourBase > 0 or bothBase > 0 then
output.total_armour = output.total_armour + (evasionBase + armourBase + bothBase) * (1 + sumMods(modDB, false, "armourInc", "evasionInc", "armourAndEvasionInc", "defencesInc") / 100) * sumMods(modDB, true, "armourMore", "evasionMore", "defencesMore")
end
else
if evasionBase > 0 or bothBase > 0 then
output.total_evasion = output.total_evasion + (evasionBase + bothBase) * (1 + sumMods(modDB, false, "evasionInc", "armourAndEvasionInc", "defencesInc") / 100) * sumMods(modDB, true, "evasionMore", "defencesMore")
end
if armourBase > 0 or bothBase > 0 then
output.total_armour = output.total_armour + (armourBase + bothBase) * (1 + sumMods(modDB, false, "armourInc", "armourAndEvasionInc", "defencesInc") / 100) * sumMods(modDB, true, "armourMore", "defencesMore")
end
end
if slot ~= "global" then
output.total_gear_evasionBase = output.total_gear_evasionBase + evasionBase + bothBase
output.total_gear_armourBase = output.total_gear_armourBase + armourBase + bothBase
end
end
if getMiscVal(modDB, nil, "cannotEvade", false) then
output.total_evadeChance = 0
else
local attackerLevel = getMiscVal(modDB, "misc", "evadeMonsterLevel", false) and m_min(getMiscVal(modDB, "monster", "level", 1), #data.enemyAccuracyTable) or m_max(m_min(env.build.characterLevel, 80), 1)
output.total_evadeChance = 1 - calcHitChance(output.total_evasion, data.enemyAccuracyTable[attackerLevel])
end
buildSpaceTable(modDB)
endWatch(env, "armourEvasion")
end
if startWatch(env, "lifeEnergyShieldRegen", "life", "energyShield") then
if getMiscVal(modDB, nil, "noLifeRegen", false) then
output.total_lifeRegen = 0
elseif getMiscVal(modDB, nil, "zealotsOath", false) then
output.total_lifeRegen = 0
mod_dbMerge(modDB, "", "energyShieldRegenBase", sumMods(modDB, false, "lifeRegenBase"))
mod_dbMerge(modDB, "", "energyShieldRegenPercent", sumMods(modDB, false, "lifeRegenPercent"))
else
mod_dbMerge(modDB, "", "lifeRegenBase", output.total_life * sumMods(modDB, false, "lifeRegenPercent") / 100)
output.total_lifeRegen = sumMods(modDB, false, "lifeRegenBase") * (1 + sumMods(modDB, false, "lifeRecoveryInc") / 100) * sumMods(modDB, true, "lifeRecoveryMore")
end
mod_dbMerge(modDB, "", "energyShieldRegenBase", output.total_energyShield * sumMods(modDB, false, "energyShieldRegenPercent") / 100)
output.total_energyShieldRegen = sumMods(modDB, false, "energyShieldRegenBase") * (1 + sumMods(modDB, false, "energyShieldRecoveryInc") / 100) * sumMods(modDB, true, "energyShieldRecoveryMore")
endWatch(env, "lifeEnergyShieldRegen")
end
if startWatch(env, "resist") then
for _, elem in pairs({"fire", "cold", "lightning"}) do
output["total_"..elem.."ResistMax"] = sumMods(modDB, false, elem.."ResistMax")
output["total_"..elem.."ResistTotal"] = sumMods(modDB, false, elem.."Resist", "elementalResist") - 60
end
if getMiscVal(modDB, nil, "chaosInoculation", false) then
output.total_chaosResistMax = 100
output.total_chaosResistTotal = 100
else
output.total_chaosResistMax = sumMods(modDB, false, "chaosResistMax")
output.total_chaosResistTotal = sumMods(modDB, false, "chaosResist") - 60
end
for _, elem in pairs({"fire", "cold", "lightning", "chaos"}) do
local total = output["total_"..elem.."ResistTotal"]
local cap = output["total_"..elem.."ResistMax"]
output["total_"..elem.."Resist"] = m_min(total, cap)
output["total_"..elem.."ResistOverCap"] = m_max(0, total - cap)
end
endWatch(env, "resist")
end
if startWatch(env, "otherDef") then
output.total_blockChanceMax = sumMods(modDB, false, "blockChanceMax")
output.total_blockChance = m_min(sumMods(modDB, false, "blockChance") / 100 * (1 + sumMods(modDB, false, "blockChanceInc") / 100) * sumMods(modDB, true, "blockChanceMore"), output.total_blockChanceMax)
output.total_spellBlockChance = m_min(sumMods(modDB, false, "spellBlockChance") / 100 * (1 + sumMods(modDB, false, "spellBlockChanceInc") / 100) * sumMods(modDB, true, "spellBlockChanceMore") + output.total_blockChance * m_min(100, getMiscVal(modDB, nil, "blockChanceConv", 0)) / 100, output.total_blockChanceMax)
if getMiscVal(modDB, nil, "cannotBlockAttacks", false) then
output.total_blockChance = 0
end
output.total_dodgeAttacks = sumMods(modDB, false, "dodgeAttacks") / 100
output.total_dodgeSpells = sumMods(modDB, false, "dodgeSpells") / 100
local stunChance = 1 - sumMods(modDB, false, "avoidStun", 0) / 100
if output.total_energyShield > output.total_life * 2 then
stunChance = stunChance * 0.5
end
output.stun_avoidChance = 1 - stunChance
if output.stun_avoidChance >= 1 then
output.stun_duration = 0
output.stun_blockDuration = 0
else
output.stun_duration = 0.35 / (1 + sumMods(modDB, false, "stunRecoveryInc") / 100)
output.stun_blockDuration = 0.35 / (1 + sumMods(modDB, false, "stunRecoveryInc", "blockRecoveryInc") / 100)
end
endWatch(env, "otherDef")
end
-- Enable skill namespaces
buildSpaceTable(modDB, env.skillSpaceFlags)
-- Calculate projectile stats
if env.skillFlags.projectile then
if startWatch(env, "projectile") then
output.total_projectileCount = sumMods(modDB, false, "projectileCount")
output.total_pierce = m_min(100, sumMods(modDB, false, "pierceChance")) / 100
output.total_projectileSpeedMod = (1 + sumMods(modDB, false, "projectileSpeedInc") / 100) * sumMods(modDB, true, "projectileSpeedMore")
endWatch(env, "projectile")
end
if getMiscVal(modDB, nil, "drillneck", false) then
mod_dbMerge(modDB, "projectile", "damageInc", output.total_pierce * 100)
end
end
-- Run skill setup function
if env.setupFunc then
env.setupFunc(function(mod, val) mod_dbMerge(modDB, nil, mod, val) end, output)
end
local isAttack = (env.mode_skillType == "ATTACK")
-- Calculate enemy resistances
if startWatch(env, "enemyResist") then
local elemResist = getMiscVal(modDB, "effective", "elementalResist", 0)
for _, damageType in pairs({"lightning","cold","fire"}) do
output["enemy_"..damageType.."Resist"] = m_min(elemResist + getMiscVal(modDB, "effective", damageType.."Resist", 0), 75)
end
output.enemy_chaosResist = m_min(getMiscVal(modDB, "effective", "chaosResist", 0), 75)
endWatch(env, "enemyResist")
end
-- Cache global damage disabling flags
if startWatch(env, "canDeal") then
output.canDeal = { }
for _, damageType in pairs(dmgTypeList) do
output.canDeal[damageType] = not getMiscVal(modDB, nil, "dealNo"..damageType, false)
end
endWatch(env, "canDeal")
end
local canDeal = output.canDeal
-- Calculate damage conversion percentages
if startWatch(env, "conversionTable") then
output.conversionTable = { }
for damageTypeIndex = 1, 4 do
local damageType = dmgTypeList[damageTypeIndex]
local globalConv = { }
local skillConv = { }
local add = { }
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] = sumMods(modDB, false, damageType.."ConvertTo"..otherType)
globalTotal = globalTotal + globalConv[otherType]
skillConv[otherType] = getMiscVal(modDB, "skill", damageType.."ConvertTo"..otherType, 0)
skillTotal = skillTotal + skillConv[otherType]
add[otherType] = sumMods(modDB, false, damageType.."GainAs"..otherType)
end
if 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] = globalConv[type] * 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 - (globalTotal + skillTotal) / 100
output.conversionTable[damageType] = dmgTable
end
output.conversionTable["chaos"] = { mult = 1 }
endWatch(env, "conversionTable")
end
-- Calculate damage for each damage type
local combMin, combMax = 0, 0
for _, damageType in pairs(dmgTypeList) do
local min, max
if startWatch(env, damageType, "enemyResist", "canDeal", "conversionTable") then
if canDeal[damageType] then
min, max = calcHitDamage(env, output, damageType)
local convMult = output.conversionTable[damageType].mult
min = min * convMult
max = max * convMult
if env.mode_effective then
-- Apply resistances
local preMult
local taken = getMiscVal(modDB, "effective", damageType.."TakenInc", 0) + getMiscVal(modDB, "effective", "damageTakenInc", 0)
if isElemental[damageType] then
local resist = output["enemy_"..damageType.."Resist"] - sumMods(modDB, false, damageType.."Pen", "elementalPen")
preMult = 1 - resist / 100
taken = taken + getMiscVal(modDB, "effective", "elementalTakenInc", 0)
elseif damageType == "chaos" then
preMult = 1 - output.enemy_chaosResist / 100
else
preMult = 1
taken = taken - getMiscVal(modDB, "effective", "physicalRed", 0)
end
if env.skillSpaceFlags.projectile then
taken = taken + getMiscVal(modDB, "effective", "projectileTakenInc", 0)
end
local mult = preMult * (1 + taken / 100)
min = min * mult
max = max * mult
end
else
min, max = 0, 0
end
output["total_"..damageType.."Min"] = min
output["total_"..damageType.."Max"] = max
output["total_"..damageType.."Avg"] = (min + max) / 2
endWatch(env, damageType)
else
min = output["total_"..damageType.."Min"]
max = output["total_"..damageType.."Max"]
end
combMin = combMin + min
combMax = combMax + max
end
output.total_combMin = combMin
output.total_combMax = combMax
-- Calculate crit chance, crit multiplier, and their combined effect
if startWatch(env, "dps_crit") then
if getMiscVal(modDB, nil, "neverCrit", false) then
output.total_critChance = 0
output.total_critMultiplier = 0
output.total_critEffect = 1
else
local baseCrit
if isAttack then
baseCrit = getMiscVal(modDB, "weapon1", "critChanceBase", 0)
else
baseCrit = getMiscVal(modDB, "skill", "critChanceBase", 0)
end
if baseCrit == 100 then
output.total_critChance = 1
else
output.total_critChance = calcVal(modDB, "critChance", baseCrit) / 100
if env.mode_effective then
output.total_critChance = output.total_critChance + getMiscVal(modDB, "effective", "additionalCritChance", 0) / 100
end
output.total_critChance = m_min(output.total_critChance, 0.95)
if baseCrit > 0 then
output.total_critChance = m_max(output.total_critChance, 0.05)
end
end
if getMiscVal(modDB, nil, "noCritMult", false) then
output.total_critMultiplier = 1
else
output.total_critMultiplier = 1.5 + sumMods(modDB, false, "critMultiplier") / 100
end
output.total_critEffect = 1 - output.total_critChance + output.total_critChance * output.total_critMultiplier
end
endWatch(env, "dps_crit")
end
-- Calculate skill speed
if startWatch(env, "dps_speed") then
if isAttack then
local baseSpeed
local attackTime = getMiscVal(modDB, "skill", "attackTime", 0)
if attackTime > 0 then
-- Skill is overriding weapon attack speed
baseSpeed = 1 / attackTime * (1 + getMiscVal(modDB, "weapon1", "attackSpeedInc", 0) / 100)
else
baseSpeed = getMiscVal(modDB, "weapon1", "attackRate", 0)
end
output.total_speed = baseSpeed * (1 + sumMods(modDB, false, "speedInc", "attackSpeedInc") / 100) * sumMods(modDB, true, "speedMore", "attackSpeedMore")
else
local baseSpeed = 1 / getMiscVal(modDB, "skill", "castTime", 0)
output.total_speed = baseSpeed * (1 + sumMods(modDB, false, "speedInc", "castSpeedInc") / 100) * sumMods(modDB, true, "speedMore", "castSpeedMore")
end
output.total_time = 1 / output.total_speed
endWatch(env, "dps_speed")
end
-- Calculate hit chance
if startWatch(env, "dps_hitChance") then
if not isAttack or getMiscVal(modDB, "skill", "cannotBeEvaded", false) or getMiscVal(modDB, nil, "cannotBeEvaded", false) or getMiscVal(modDB, "weapon1", "cannotBeEvaded", false) then
output.total_hitChance = 1
else
output.total_accuracy = calcVal(modDB, "accuracy")
local targetLevel = getMiscVal(modDB, "misc", "hitMonsterLevel", false) and m_min(getMiscVal(modDB, "monster", "level", 1), #data.enemyEvasionTable) or m_max(m_min(env.build.characterLevel, 79), 1)
local targetEvasion = data.enemyEvasionTable[targetLevel]
if env.mode_effective then
targetEvasion = targetEvasion * getMiscVal(modDB, "effective", "evasionMore", 1)
end
output.total_hitChance = calcHitChance(targetEvasion, output.total_accuracy)
end
endWatch(env, "dps_hitChance")
end
-- Calculate average damage and final DPS
output.total_averageHit = (combMin + combMax) / 2 * output.total_critEffect
output.total_averageDamage = output.total_averageHit * output.total_hitChance
output.total_dps = output.total_averageDamage * output.total_speed * getMiscVal(modDB, "skill", "dpsMultiplier", 1)
-- Calculate mana cost (may be slightly off due to rounding differences)
output.total_manaCost = m_floor(m_max(0, getMiscVal(modDB, "skill", "manaCostBase", 0) * (1 + m_floor((sumMods(modDB, true, "manaCostMore") - 1) * 100 + 0.0001) / 100) * (1 + sumMods(modDB, false, "manaCostInc") / 100) - sumMods(modDB, false, "manaCostBase")))
-- Calculate AoE stats
if env.skillFlags.aoe then
output.total_aoeRadiusMod = (1 + sumMods(modDB, false, "aoeRadiusInc") / 100) * sumMods(modDB, true, "aoeRadiusMore")
end
-- Calculate skill duration
if startWatch(env, "duration") then
local durationBase = getMiscVal(modDB, "skill", "durationBase", 0)
output.total_durationMod = (1 + sumMods(modDB, false, "durationInc") / 100) * sumMods(modDB, true, "durationMore")
if durationBase > 0 then
output.total_duration = durationBase * output.total_durationMod
end
endWatch(env, "duration")
end
-- Calculate trap, mine and totem stats
if startWatch(env, "trapMineTotem") then
if env.skillFlags.trap then
output.total_trapCooldown = getMiscVal(modDB, "skill", "trapCooldown", 4) / (1 + getMiscVal(modDB, nil, "trapCooldownRecoveryInc", 0) / 100)
output.total_activeTrapLimit = sumMods(modDB, false, "activeTrapLimit")
end
if env.skillFlags.mine then
output.total_activeMineLimit = sumMods(modDB, false, "activeMineLimit")
end
if env.skillFlags.totem then
output.totem_lifeMod = (1 + sumMods(modDB, false, "totemLifeInc") / 100) * sumMods(modDB, true, "totemLifeMore")
end
endWatch(env, "trapMineTotem")
end
-- Calculate enemy stun modifiers
if startWatch(env, "enemyStun") then
local enemyStunThresholdRed = -sumMods(modDB, false, "stunEnemyThresholdInc")
if enemyStunThresholdRed > 75 then
output.stun_enemyThresholdMod = 1 - (75 + (enemyStunThresholdRed - 75) * 25 / (enemyStunThresholdRed - 50)) / 100
else
output.stun_enemyThresholdMod = 1 - enemyStunThresholdRed / 100
end
output.stun_enemyDuration = 0.35 * (1 + sumMods(modDB, false, "stunEnemyDurationInc") / 100)
endWatch(env, "enemyStun")
end
-- Calculate skill DOT components
output.total_damageDot = 0
for _, damageType in pairs(dmgTypeList) do
if startWatch(env, damageType.."Dot", "enemyResist", "canDeal") then
local baseVal
if canDeal[damageType] then
baseVal = getMiscVal(modDB, "skill", damageType.."DotBase", 0)
else
baseVal = 0
end
if baseVal > 0 then
env.skillFlags.dot = true
buildSpaceTable(modDB, {
dot = true,
debuff = env.skillSpaceFlags.debuff,
spell = getMiscVal(modDB, "skill", "dotIsSpell", false),
projectile = env.skillSpaceFlags.projectile,
aoe = env.skillSpaceFlags.aoe,
totem = env.skillSpaceFlags.totem,
trap = env.skillSpaceFlags.trap,
mine = env.skillSpaceFlags.mine,
})
local effMult = 1
if env.mode_effective then
local preMult
local taken = getMiscVal(modDB, "effective", damageType.."TakenInc", 0) + getMiscVal(modDB, "effective", "damageTakenInc", 0) + getMiscVal(modDB, "effective", "dotTakenInc", 0)
if damageType == "physical" then
taken = taken - getMiscVal(modDB, "effective", "physicalRed", 0)
preMult = 1
else
if isElemental[damageType] then
taken = taken + getMiscVal(modDB, "effective", "elementalTakenInc", 0)
end
preMult = 1 - output["enemy_"..damageType.."Resist"] / 100
end
effMult = preMult * (1 + taken / 100)
end
output["total_"..damageType.."Dot"] = baseVal
* (1 + sumMods(modDB, false, "damageInc", damageType.."Inc", isElemental[damageType] and "elementalInc" or nil) / 100)
* sumMods(modDB, true, "damageMore", damageType.."More", isElemental[damageType] and "elementalMore" or nil)
* effMult
end
buildSpaceTable(modDB, env.skillSpaceFlags)
endWatch(env, damageType.."Dot")
end
output.total_damageDot = output.total_damageDot + (output["total_"..damageType.."Dot"] or 0)
end
-- Calculate bleeding chance and damage
env.skillFlags.bleed = false
if startWatch(env, "bleed", "canDeal", "physical", "dps_crit") then
output.bleed_chance = m_min(100, sumMods(modDB, false, "bleedChance")) / 100
if canDeal.physical and output.bleed_chance > 0 and output.total_physicalAvg > 0 then
env.skillFlags.dot = true
env.skillFlags.bleed = true
env.skillFlags.duration = true
buildSpaceTable(modDB, {
dot = true,
debuff = true,
bleed = true,
projectile = env.skillSpaceFlags.projectile,
aoe = env.skillSpaceFlags.aoe,
totem = env.skillSpaceFlags.totem,
trap = env.skillSpaceFlags.trap,
mine = env.skillSpaceFlags.mine,
})
local baseVal = output.total_physicalAvg * output.total_critEffect * 0.1
local effMult = 1
if env.mode_effective then
local taken = getMiscVal(modDB, "effective", "physicalTakenInc", 0) + getMiscVal(modDB, "effective", "damageTakenInc", 0) + getMiscVal(modDB, "effective", "dotTakenInc", 0) - getMiscVal(modDB, "effective", "physicalRed", 0)
effMult = 1 + taken / 100
end
output.bleed_dps = baseVal * (1 + sumMods(modDB, false, "damageInc", "physicalInc") / 100) * sumMods(modDB, true, "damageMore", "physicalMore") * effMult
output.bleed_duration = 5 * (1 + sumMods(modDB, false, "durationInc") / 100) * sumMods(modDB, true, "durationMore")
buildSpaceTable(modDB, env.skillSpaceFlags)
end
endWatch(env, "bleed")
end
-- Calculate poison chance and damage
env.skillFlags.poison = false
if startWatch(env, "poison", "canDeal", "physical", "chaos", "dps_crit", "enemyResist") then
output.poison_chance = m_min(100, sumMods(modDB, false, "poisonChance")) / 100
if canDeal.chaos and output.poison_chance > 0 and (output.total_physicalAvg > 0 or output.total_chaosAvg > 0) then
env.skillFlags.dot = true
env.skillFlags.poison = true
env.skillFlags.duration = true
buildSpaceTable(modDB, {
dot = true,
debuff = true,
spell = getMiscVal(modDB, "skill", "dotIsSpell", false),
poison = true,
projectile = env.skillSpaceFlags.projectile,
aoe = env.skillSpaceFlags.aoe,
totem = env.skillSpaceFlags.totem,
trap = env.skillSpaceFlags.trap,
mine = env.skillSpaceFlags.mine,
})
local baseVal = (output.total_physicalAvg + output.total_chaosAvg) * output.total_critEffect * 0.08
local effMult = 1
if env.mode_effective then
local taken = getMiscVal(modDB, "effective", "chaosTakenInc", 0) + getMiscVal(modDB, "effective", "damageTakenInc", 0) + getMiscVal(modDB, "effective", "dotTakenInc", 0)
effMult = (1 - output["enemy_chaosResist"] / 100) * (1 + taken / 100)
end
output.poison_dps = baseVal * (1 + sumMods(modDB, false, "damageInc", "chaosInc") / 100) * sumMods(modDB, true, "damageMore", "chaosMore") * effMult
output.poison_duration = 2 * (1 + sumMods(modDB, false, "durationInc") / 100) * sumMods(modDB, true, "durationMore")
output.poison_damage = output.poison_dps * output.poison_duration
buildSpaceTable(modDB, env.skillSpaceFlags)
end
endWatch(env, "poison")
end
-- Calculate ignite chance and damage
env.skillFlags.ignite = false
if startWatch(env, "ignite", "canDeal", "fire", "cold", "dps_crit", "enemyResist") then
if getMiscVal(modDB, nil, "cannotIgnite", false) then
output.ignite_chance = 0
else
output.ignite_chance = m_min(100, sumMods(modDB, false, "igniteChance")) / 100
local sourceDmg = 0
if canDeal.fire and not getMiscVal(modDB, nil, "fireCannotIgnite", false) then
sourceDmg = sourceDmg + output.total_fireAvg
end
if canDeal.cold and getMiscVal(modDB, nil, "coldCanIgnite", false) then
sourceDmg = sourceDmg + output.total_coldAvg
end
if canDeal.fire and output.ignite_chance > 0 and sourceDmg > 0 then
env.skillFlags.dot = true
env.skillFlags.ignite = true
buildSpaceTable(modDB, {
dot = true,
debuff = true,
spell = getMiscVal(modDB, "skill", "dotIsSpell", false),
ignite = true,
projectile = env.skillSpaceFlags.projectile,
aoe = env.skillSpaceFlags.aoe,
totem = env.skillSpaceFlags.totem,
trap = env.skillSpaceFlags.trap,
mine = env.skillSpaceFlags.mine,
})
local baseVal = sourceDmg * output.total_critEffect * 0.2
local effMult = 1
if env.mode_effective then
local taken = getMiscVal(modDB, "effective", "fireTakenInc", 0) + getMiscVal(modDB, "effective", "elementalTakenInc", 0) + getMiscVal(modDB, "effective", "damageTakenInc", 0) + getMiscVal(modDB, "effective", "dotTakenInc", 0)
effMult = (1 - output["enemy_fireResist"] / 100) * (1 + taken / 100)
end
output.ignite_dps = baseVal * (1 + sumMods(modDB, false, "damageInc", "fireInc", "elementalInc") / 100) * sumMods(modDB, true, "damageMore", "fireMore", "elementalMore") * effMult
output.ignite_duration = 4 * (1 + getMiscVal(modDB, "ignite", "durationInc", 0) / 100)
buildSpaceTable(modDB, env.skillSpaceFlags)
end
end
endWatch(env, "ignite")
end
-- Calculate shock and freeze chance + duration modifier
if startWatch(env, "shock", "canDeal", "lightning", "fire", "chaos") then
if getMiscVal(modDB, nil, "cannotShock", false) then
output.shock_chance = 0
else
output.shock_chance = m_min(100, sumMods(modDB, false, "shockChance")) / 100
local sourceDmg = 0
if canDeal.lightning and not getMiscVal(modDB, nil, "lightningCannotShock", false) then
sourceDmg = sourceDmg + output.total_lightningAvg
end
if canDeal.physical and getMiscVal(modDB, nil, "physicalCanShock", false) then
sourceDmg = sourceDmg + output.total_physicalAvg
end
if canDeal.fire and getMiscVal(modDB, nil, "fireCanShock", false) then
sourceDmg = sourceDmg + output.total_fireAvg
end
if canDeal.chaos and getMiscVal(modDB, nil, "chaosCanShock", false) then
sourceDmg = sourceDmg + output.total_chaosAvg
end
if output.shock_chance > 0 and sourceDmg > 0 then
env.skillFlags.shock = true
output.shock_durationMod = 1 + getMiscVal(modDB, "shock", "durationInc", 0) / 100
end
end
endWatch(env, "shock")
end
if startWatch(env, "freeze", "canDeal", "cold", "lightning") then
if getMiscVal(modDB, nil, "cannotFreeze", false) then
output.freeze_chance = 0
else
output.freeze_chance = m_min(100, sumMods(modDB, false, "freezeChance")) / 100
local sourceDmg = 0
if canDeal.cold and not getMiscVal(modDB, nil, "coldCannotFreeze", false) then
sourceDmg = sourceDmg + output.total_coldAvg
end
if canDeal.lightning and getMiscVal(modDB, nil, "lightningCanFreeze", false) then
sourceDmg = sourceDmg + output.total_lightningAvg
end
if output.freeze_chance > 0 and sourceDmg > 0 then
env.skillFlags.freeze = true
output.freeze_durationMod = 1 + getMiscVal(modDB, "freeze", "durationInc", 0) / 100
end
end
endWatch(env, "freeze")
end
-- Calculate combined DPS estimate, including DoTs
output.total_combinedDPS = output[(env.mode_average and "total_averageDamage") or "total_dps"] + output.total_damageDot
if env.skillFlags.poison then
if env.mode_average then
output.total_combinedDPS = output.total_combinedDPS + output.poison_chance * output.poison_dps * output.poison_duration
else
output.total_combinedDPS = output.total_combinedDPS + output.poison_chance * output.poison_dps * output.poison_duration * output.total_speed
end
end
if env.skillFlags.ignite then
output.total_combinedDPS = output.total_combinedDPS + output.ignite_dps
end
if env.skillFlags.bleed then
output.total_combinedDPS = output.total_combinedDPS + output.bleed_dps
end
end
local calcs = { }
-- Wipe mod database and repopulate with base mods
local function resetModDB(modDB, base)
for spaceName, spaceMods in pairs(modDB) do
local baseSpace = base[spaceName]
if baseSpace then
for k in pairs(spaceMods) do
spaceMods[k] = baseSpace[k]
end
else
wipeTable(spaceMods)
end
end
end
-- Print various tables to the console
local function infoDump(env, output)
ConPrintf("== Modifier Database ==")
modLib.dbPrint(env.modDB)
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 Mods ==")
modLib.listPrint(env.mainSkill.skillModList)
ConPrintf("== Main Skill Flags ==")
modLib.listPrint(env.skillFlags)
ConPrintf("== Namespaces ==")
modLib.listPrint(env.skillSpaceFlags)
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("== Buff Skill Mods ==")
modLib.listPrint(env.buffSkillModList)
ConPrintf("== Aura Skill Mods ==")
modLib.listPrint(env.auraSkillModList)
ConPrintf("== Curse Skill Mods ==")
modLib.listPrint(env.curseSkillModList)]]
if env.buffModList then
ConPrintf("== Other Buff Mods ==")
modLib.listPrint(env.buffModList)
end
ConPrintf("== Spec Mods ==")
modLib.listPrint(env.specModList)
ConPrintf("== Item Mods ==")
modLib.listPrint(env.itemModList)
ConPrintf("== Conditions ==")
modLib.listPrint(env.condList)
ConPrintf("== Conditional Modifiers ==")
modLib.listPrint(env.condModList)
ConPrintf("== Conversion Table ==")
modLib.dbPrint(output.conversionTable)
end
-- Generate a function for calculating the effect of some modification to the environment
local function getCalculator(build, input, fullInit, modFunc)
-- Initialise environment
local env = initEnv(build, input, "CALCULATOR")
-- Save a copy of the initial mod database
if fullInit then
mergeMainMods(env)
end
local initModDB = copyTable(env.modDB)
if not fullInit then
mergeMainMods(env)
end
-- Finialise modifier database and make a copy for later comparison
local baseOutput = { }
local outputMeta = { __index = baseOutput }
finaliseMods(env, baseOutput)
local baseModDB = copyTable(env.modDB)
-- Run base calculation pass while building watch lists
env.watchers = { }
env.buildWatch = true
env.modDB._activeWatchers = { }
performCalcs(env, baseOutput)
env.buildWatch = false
env.modDB._activeWatchers = nil
-- Generate list of watched mods
local watchedModList = { }
for _, watchList in pairs(env.watchers) do
for k, default in pairs(watchList) do
-- Add this watcher to the mod's watcher list
local spaceName, modName = modLib.getSpaceName(k)
if not watchedModList[spaceName] then
watchedModList[spaceName] = { }
end
if not baseModDB[spaceName] then
baseModDB[spaceName] = { }
end
if not initModDB[spaceName] then
initModDB[spaceName] = { }
end
if not watchedModList[spaceName][modName] then
watchedModList[spaceName][modName] = { baseModDB[spaceName][modName], { } }
end
watchedModList[spaceName][modName][2][watchList] = true
if initModDB[spaceName][modName] == nil and baseModDB[spaceName][modName] ~= nil then
-- Ensure that the initial mod list has at least a default value for any modifiers present in the base database
initModDB[spaceName][modName] = default
end
end
end
local flagged = { }
return function(...)
-- Restore initial mod database
resetModDB(env.modDB, initModDB)
-- Call function to make modifications to the enviroment
modFunc(env, ...)
-- Prepare for calculation
local output = setmetatable({ }, outputMeta)
finaliseMods(env, output)
--[[local debugThis = type(...) == "table" and #(...) == 1 and (...)[1].id == 17735
if debugThis then
ConPrintf("+++++++++++++++++++++++++++++++++++++++")
end]]
-- Check if any watched mods have changed
local active = false
for spaceName, watchedMods in pairs(watchedModList) do
for k, v in pairs(env.modDB[spaceName]) do
local watchedMod = watchedMods[k]
if watchedMod and v ~= watchedMod[1] then
-- Modifier value has changed, flag all watchers for this mod
for watchList in pairs(watchedMod[2]) do
watchList._flag = true
flagged[watchList] = true
end
--[[if debugThis then
ConPrintf("%s:%s %s %s", spaceName, k, watchedMod[1], v)
end]]
active = true
end
end
end
if not active then
return baseOutput
end
-- Run the calculations
performCalcs(env, output)
--[[if debugThis then
infoDump(env, output)
end]]
-- Reset watcher flags
for watchList in pairs(flagged) do
watchList._flag = false
flagged[watchList] = nil
end
return output
end, baseOutput
end
-- Get calculator for tree node modifiers
function calcs.getNodeCalculator(build, input)
return getCalculator(build, input, true, function(env, nodeList, remove)
-- Build and merge/unmerge modifiers for these nodes
local nodeModList = buildNodeModList(env, nodeList)
if remove then
mod_dbUnmergeList(env.modDB, nodeModList)
else
mod_dbMergeList(env.modDB, nodeModList)
end
end)
end
-- Get calculator for item modifiers
function calcs.getItemCalculator(build, input)
return getCalculator(build, input, false, function(env, repSlotName, repItem)
-- Merge main mods, replacing the item in the given slot with the given item
mergeMainMods(env, repSlotName, repItem)
end)
end
-- Build output for display in the grid or side bar
function calcs.buildOutput(build, input, output, mode)
-- Build output
local env = initEnv(build, input, mode)
mergeMainMods(env)
finaliseMods(env, output)
performCalcs(env, output)
output.total_extraPoints = getMiscVal(env.modDB, nil, "extraPoints", 0)
-- Add extra display-only stats
for k, v in pairs(env.specModList) do
output["spec_"..k] = v
end
for k, v in pairs(env.itemModList) do
output["gear_"..k] = v
end
if mode == "MAIN" then
elseif mode == "GRID" then
for i, aux in pairs(env.auxSkillList) do
output["buff_label"..i] = aux.activeGem.name
end
for _, damageType in pairs(dmgTypeList) do
-- Add damage ranges
if output["total_"..damageType.."Max"] > 0 then
output["total_"..damageType] = formatRound(output["total_"..damageType.."Min"]) .. " - " .. formatRound(output["total_"..damageType.."Max"])
else
output["total_"..damageType] = 0
end
end
output.total_damage = formatRound(output.total_combMin) .. " - " .. formatRound(output.total_combMax)
-- Calculate XP modifier
if input.monster_level and input.monster_level > 0 then
local playerLevel = build.characterLevel
local diff = m_abs(playerLevel - input.monster_level) - 3 - m_floor(playerLevel / 16)
if diff <= 0 then
output.monster_xp = 1
else
output.monster_xp = m_max(0.01, ((playerLevel + 5) / (playerLevel + 5 + diff ^ 2.5)) ^ 1.5)
end
end
-- Configure view mode
setViewMode(env, build.skillsTab.socketGroupList, env.mainSkillGroupActiveSkillList or { })
--infoDump(env, output)
end
end
return calcs