Files
PathOfBuilding/Modules/CalcsControl.lua
2016-05-05 02:11:27 +10:00

1405 lines
44 KiB
Lua

-- Path of Building
--
-- Module: CalcsControl
-- Control script for calculations
--
local grid = ...
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 pairs = pairs
local ipairs = ipairs
local t_insert = table.insert
local mod_listMerge = mod.listMerge
local mod_dbMerge = mod.dbMerge
local mod_dbUnmerge = mod.dbUnmerge
local mod_dbMergeList = mod.dbMergeList
local mod_dbUnmergeList = mod.dbUnmergeList
local setViewMode = LoadModule("Modules/CalcsView", grid)
local isElemental = { fire = true, cold = true, lightning = true }
local dmgTypeList = {"physical", "lightning", "cold", "fire", "chaos"}
-- Parse gem list specification
local function parseGemSpec(spec, out)
for nameSpec, numSpec in spec:gmatch("(%a[%a ]*)%s+([%d/\\]+)") do
-- Search for gem name using increasingly broad search patterns
local patternList = {
"^"..nameSpec.."$", -- Exact match
"^"..nameSpec:gsub("%l", "%l*%0"), -- Abbreviated words ("CldFr" -> "Cold to Fire")
"^"..nameSpec:gsub("%a", ".*%0") -- Global abbreviation ("CtoF" -> "Cold to Fire")
}
local gemName, gemData
for _, pattern in ipairs(patternList) do
for name, data in pairs(data.gems) do
if name:match(pattern) then
if gemName then
return "Ambiguous gem name '"..nameSpec.."'\nMatches '"..gemName.."', '"..name.."'"
end
gemName = name
gemData = data
end
end
if gemName then
break
end
end
if not gemName then
return "Unrecognised gem name '"..nameSpec.."'"
end
if gemData.unsupported then
return "Gem '"..gemName.."' is unsupported"
end
-- Parse level/quality specification
local level, qual = numSpec:match("(%d+)[/\\](%d+)")
if level then
level = tonumber(level)
qual = tonumber(qual)
else
level = tonumber(numSpec)
qual = 0
end
if not level or level < 1 or level > #gemData.levels or qual < 0 then
return "Invalid level or level/quality specification '"..numSpec.."'"
end
-- Add to output list
t_insert(out, {
name = gemName,
level = level,
qual = qual,
data = gemData
})
end
end
-- Combine specified modifiers from all current namespaces
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
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 or increased
function calcVal(modDB, name, base, inc)
local baseVal = sumMods(modDB, false, name.."Base") + (base or 0)
return baseVal * (1 + (sumMods(modDB, false, name.."Inc") + (inc or 0)) / 100) * sumMods(modDB, true, name.."More")
end
-- Merge gem modifiers
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.qual))
end
for k, v in pairs(gem.data.levels[gem.level]) do
mod_listMerge(modList, k, v)
end
end
-- Merge modifiers for all items, optionally replacing one item
local function mergeItemMods(env, build, repSlot, repItem)
-- Build and merge item mods
env.itemModList = wipeTable(env.itemModList)
for slotName, slot in pairs(build.items.slots) do
local item
if slotName == repSlot then
item = repItem
else
item = build.items.list[slot.selItem]
end
if item then
local armourType = data.itemBases[item.baseName].armour and item.type
for k, v in pairs(item.modList) do
if slotName == "Weapon 1" then
k = k:gsub("weaponX_","weapon1_")
elseif slotName == "Weapon 2" then
k = k:gsub("weaponX_","weapon2_")
end
if armourType and (k == "armourBase" or k == "evasionBase" or k == "energyShieldBase") then
k = armourType.."_"..k
end
mod_listMerge(env.itemModList, k, v)
end
end
end
mod_dbMergeList(env.modDB, env.itemModList)
-- Find radius jewels
env.radList = wipeTable(env.radList)
for nodeId, node in pairs(build.spec.allocNodes) do
if node.type == "socket" then
local socket, jewel = build.items:GetSocketJewel(nodeId)
if socket.slotName == repSlot then
jewel = repItem
end
if jewel and jewel.radius and jewel.jewelFunc then
t_insert(env.radList, {
rSq = data.jewelRadius[jewel.radius].rad * data.jewelRadius[jewel.radius].rad,
x = node.x,
y = node.y,
func = jewel.jewelFunc,
data = { }
})
end
end
end
end
-- Build list of modifiers from the listed tree nodes
local function buildNodeModList(env, nodeList, finishJewels)
-- Initialise radius jewewls
for _, rad in pairs(env.radList) do
wipeTable(rad.data)
end
-- Add node modifers
local modList = { }
local nodeModList = { }
for _, node in pairs(nodeList) do
-- Build list of mods from this node
for _, mod in pairs(node.mods) do
if mod.list and not mod.extra then
for k, v in pairs(mod.list) do
mod_listMerge(nodeModList, k, v)
end
end
end
-- Run radius jewels
for _, rad in pairs(env.radList) do
local vX, vY = node.x - rad.x, node.y - rad.y
if vX * vX + vY * vY <= rad.rSq then
rad.func(nodeModList, modList, rad.data)
end
end
-- Merge with output list
for k, v in pairs(nodeModList) do
mod_listMerge(modList, k, v)
nodeModList[k] = nil
end
if node.passivePointsGranted > 0 then
mod_listMerge(modList, "extraPoints", node.passivePointsGranted)
end
end
if finishJewels then
-- Finish radius jewels
for _, rad in pairs(env.radList) do
rad.func(nil, modList, rad.data)
end
end
return modList
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 next(modDB[spaceName]) then
modDB._spaces[modDB[spaceName]] = spaceName
end
end
end
end
end
-- Start watched section
local function startWatch(env, key, ...)
if env.buildWatch then
env.watchers[key] = { _key = key }
env.modDB._activeWatchers[env.watchers[key]] = true
return true
else
if not env.watchers or env.spacesChanged or not env.watchers[key] or env.watchers[key]._flag then
return true
end
for i = 1, select('#', ...) do
if env.watchers[select(i, ...)]._flag then
return true
end
end
end
end
-- End watched section
local function endWatch(env, key)
if env.buildWatch and env.watchers[key] then
env.modDB._activeWatchers[env.watchers[key]] = nil
end
end
-- Calculate damage for the given damage type at the given limit ('Min'/'Max')
local function calcDamage(env, output, damageType, limit, ...)
local modDB = env.modDB
local isAttack = (env.mode == "ATTACK")
local damageTypeLimit = damageType..limit
-- Calculate base value
local baseVal
if isAttack then
baseVal = getMiscVal(modDB, "weapon1", damageTypeLimit, 0) + sumMods(modDB, false, damageTypeLimit)
else
baseVal = getMiscVal(modDB, "skill", damageTypeLimit, 0) + sumMods(modDB, false, damageTypeLimit) * getMiscVal(modDB, "skill", "damageEff", 1)
end
-- Build lists of applicable modifier names
local addElemental = isElemental[damageType]
local inc = { damageType.."Inc", "damageInc" }
local more = { damageType.."More", "damageMore" }
local damageTypeStr = "total_"..damageTypeLimit
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, "elemInc")
t_insert(more, "elemMore")
end
-- Combine modifiers
local damageTypeStrInc = damageTypeStr.."Inc"
local damageTypeStrMore = damageTypeStr.."More"
if startWatch(env, damageTypeStrInc) then
output[damageTypeStrInc] = sumMods(modDB, false, unpack(inc))
endWatch(env, damageTypeStrInc)
end
if startWatch(env, damageTypeStrMore) then
output[damageTypeStrMore] = sumMods(modDB, true, unpack(more))
endWatch(env, damageTypeStrMore)
end
-- Apply modifiers
local val = baseVal * (1 + output[damageTypeStrInc] / 100) * output[damageTypeStrMore]
-- Apply conversions
if startWatch(env, damageTypeStr.."Conv") then
local add = 0
local mult = 1
for _, otherType in pairs(dmgTypeList) do
if otherType ~= damageType then
-- Damage added or converted from the other damage type
local gain = sumMods(modDB, false, otherType.."GainAs"..damageType, otherType.."ConvertTo"..damageType) / 100
if gain > 0 then
add = add + calcDamage(env, output, otherType, limit, damageType, ...) * gain
end
if not (...) then
-- Some of this damage type is being converted to the other type
-- Not applied to damage being calculated for conversion
local convTo = sumMods(modDB, false, damageType.."ConvertTo"..otherType) / 100
if convTo > 0 then
mult = mult - convTo
end
end
end
end
output[damageTypeStr.."ConvAdd"] = add
output[damageTypeStr.."ConvMult"] = mult
endWatch(env, damageTypeStr.."Conv")
end
-- Apply resistances
if not (...) and startWatch(env, damageTypeStr.."Resist") then
if addElemental and env.mode_effective then
output[damageTypeStr.."EffMult"] = 1 - m_min(getMiscVal(modDB, "effective", "elemResist", 0), 75) / 100 + sumMods(modDB, false, damageType.."Pen", "elemPen") / 100
else
output[damageTypeStr.."EffMult"] = 1
end
endWatch(env, damageTypeStr.."Resist")
end
return (val + output[damageTypeStr.."ConvAdd"]) * output[damageTypeStr.."ConvMult"] * ((...) and 1 or sumMods(modDB, true, damageType.."FinalMore") * output[damageTypeStr.."EffMult"])
end
-- Initialise environment with skill, input and spec data
local function initEnv(input, build)
local env = { }
-- Parse gem specification
local gemList = { }
env.gemList = gemList
local errMsg = parseGemSpec(input.skill_spec or "", gemList)
if errMsg then
return nil, errMsg
end
-- Find active skill gem
local activeGem
for _, gem in ipairs(gemList) do
if not gem.data.support then
if activeGem then
return nil, "Multiple active gems specified:\n"..activeGem.name..", "..gem.name
end
activeGem = gem
end
end
-- Default attack if no active gem provided
if not activeGem then
activeGem = {
name = "Default Attack",
level = 1,
qual = 0,
data = data.gems._default
}
gemList = { activeGem }
end
env.skillName = activeGem.name
env.setupFunc = activeGem.data.setupFunc
-- Build base skill flag set ('attack', 'projectile', etc)
local baseFlags = { }
env.baseFlags = baseFlags
for k, v in pairs(activeGem.data) do
if v == true then
baseFlags[k] = true
end
end
for _, gem in ipairs(gemList) do
if gem.data.support and gem.data.addFlags 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
-- Build skill modifier list
local skillModList = { }
env.skillModList = skillModList
for _, gem in ipairs(gemList) do
if gem.data.support and
(gem.data.attack and not baseFlags.attack) or
(gem.data.spell and not baseFlags.spell) or
(gem.data.melee and not baseFlags.melee) or
(gem.data.projectile and not baseFlags.projectile) or
(gem.data.totem and not baseFlags.totem) or
(gem.data.trap and not baseFlags.trap and not (gem.data.mine and baseFlags.mine)) or
(gem.data.mine and not baseFlags.mine and not (gem.data.trap and baseFlags.trap)) then
-- This support doesn't apply
gem.cantSupport = true
else
mergeGemMods(skillModList, gem)
end
end
-- Handle multipart skills
if activeGem.data.parts then
input.skill_part = m_max(1, m_min(#activeGem.data.parts, input.skill_part or 1))
local part = activeGem.data.parts[input.skill_part]
env.skillPartName = part.name
for k, v in pairs(part) do
if v == true then
baseFlags[k] = true
elseif v == false then
baseFlags[k] = nil
end
end
baseFlags.multiPart = #activeGem.data.parts > 1
else
env.skillPartName = ""
end
-- Set skill mode
if baseFlags.attack then
env.mode = "ATTACK"
else
env.mode = "SPELL"
end
-- Process auras and buff skills
local auraSkillModList = { }
local buffSkillModList = { }
env.auraSkillModList = auraSkillModList
env.buffSkillModList = buffSkillModList
for i = 1, 10 do
local spec = input["buff_spec"..i]
if spec and #spec > 0 then
-- Parse gem specification
local gemList = { }
local errMsg = parseGemSpec(spec, gemList)
if errMsg then
return nil, "In aura "..i..": "..errMsg
end
-- Find active skill
local activeGem
for _, gem in ipairs(gemList) do
if not gem.data.support then
if activeGem then
return nil, "Multiple active gems specified in aura "..i..":\n"..activeGem.name..", "..gem.name
end
activeGem = gem
end
end
-- Merge modifiers
if activeGem then
if activeGem.data.aura then
mergeGemMods(auraSkillModList, activeGem)
else
mergeGemMods(buffSkillModList, activeGem)
end
end
end
end
-- Initialise modifier database with base values
local modDB = { }
env.modDB = modDB
env.classId = build.spec.curClassId
local classStats = build.tree.characterData[tostring(env.classId)]
for _, stat in pairs({"str","dex","int"}) do
mod_dbMerge(modDB, "", stat.."Base", classStats["base_"..stat])
end
local level = input.player_level or 1
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, "", "blockChanceMax", 75)
mod_dbMerge(modDB, "", "powerMax", 3)
mod_dbMerge(modDB, "power", "critChanceInc", 50)
mod_dbMerge(modDB, "", "frenzyMax", 3)
mod_dbMerge(modDB, "frenzy", "speedInc", 4)
mod_dbMerge(modDB, "", "enduranceMax", 3)
mod_dbMerge(modDB, "endurance", "fireResist", 4)
mod_dbMerge(modDB, "endurance", "coldResist", 4)
mod_dbMerge(modDB, "endurance", "lightningResist", 4)
-- Add bandit mods
if input.misc_banditNormal == "Alira" then
mod_dbMerge(modDB, "", "manaBase", 60)
elseif input.misc_banditNormal == "Kraityn" then
mod_dbMerge(modDB, "", "fireResist", 10)
mod_dbMerge(modDB, "", "coldResist", 10)
mod_dbMerge(modDB, "", "lightningResist", 10)
elseif input.misc_banditNormal == "Oak" then
mod_dbMerge(modDB, "", "lifeBase", 40)
else
mod_dbMerge(modDB, "", "extraPoints", 1)
end
if input.misc_banditCruel == "Alira" then
mod_dbMerge(modDB, "", "castSpeedInc", 5)
elseif input.misc_banditCruel == "Kraityn" then
mod_dbMerge(modDB, "", "attackSpeedInc", 8)
elseif input.misc_banditCruel == "Oak" then
mod_dbMerge(modDB, "", "physicalInc", 16)
else
mod_dbMerge(modDB, "", "extraPoints", 1)
end
if input.misc_banditMerc == "Alira" then
mod_dbMerge(modDB, "", "powerMax", 1)
elseif input.misc_banditMerc == "Kraityn" then
mod_dbMerge(modDB, "", "frenzyMax", 1)
elseif input.misc_banditMerc == "Oak" then
mod_dbMerge(modDB, "", "enduranceMax", 1)
else
mod_dbMerge(modDB, "", "extraPoints", 1)
end
-- Merge skill mods
mod_dbMergeList(modDB, skillModList)
if baseFlags.multiPart and modDB["part"..input.skill_part] then
-- Merge active skill part mods
mod_dbMergeList(modDB, modDB["part"..input.skill_part])
end
-- Merge buff skill modifiers (auras are added later)
for k, v in pairs(buffSkillModList) do
if k:match("^buff_") then
mod_dbMerge(modDB, nil, k:gsub("buff_",""), v)
end
end
-- Add mods from the input table
for modName, modVal in pairs(input) do
-- Strip namespaces that only the input table uses
local newModName = modName:gsub("^other_","")
mod_dbMerge(modDB, nil, newModName, modVal)
end
return env
end
-- Prepare environment for calculations
local function calcSetup(env, output)
local modDB = env.modDB
local weapon1Type = getMiscVal(modDB, "weapon1", "type", "None")
local weapon2Type = getMiscVal(modDB, "weapon2", "type", "")
if weapon1Type == env.weapon1Type and weapon2Type == env.weapon2Type then
env.spacesChanged = false
else
env.spacesChanged = true
env.weapon1Type = weapon1Type
env.weapon2Type = weapon2Type
-- Initialise skill flag set
local skillFlags = wipeTable(env.skillFlags)
for k, v in pairs(env.baseFlags) do
skillFlags[k] = v
end
env.skillFlags = skillFlags
-- 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 then
skillFlags.bow = nil
skillFlags.projectile = nil
else
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
if skillFlags.spell then
skillSpaceFlags["spell"] = true
elseif skillFlags.attack then
skillSpaceFlags["attack"] = true
end
if skillFlags.weapon1Attack then
skillSpaceFlags[weapon1Info.space] = true
if weapon1Type ~= "None" then
skillSpaceFlags["weapon"] = true
if skillFlags.mainIs1H then
skillSpaceFlags["weapon1h"] = true
if skillFlags.melee then
skillSpaceFlags["weapon1hMelee"] = true
end
else
skillSpaceFlags["weapon2h"] = true
if skillFlags.melee then
skillSpaceFlags["weapon2hMelee"] = true
end
end
end
end
if skillFlags.melee then
skillSpaceFlags["melee"] = true
elseif skillFlags.projectile then
skillSpaceFlags["projectile"] = true
end
if skillFlags.totem then
skillSpaceFlags["totem"] = true
elseif skillFlags.trap then
skillSpaceFlags["trap"] = true
elseif skillFlags.mine then
skillSpaceFlags["mine"] = true
end
if skillFlags.aoe then
skillSpaceFlags["aoe"] = true
end
if skillFlags.movement then
skillSpaceFlags["movement"] = true
end
-- These are for skill type modifiers such as "Increased Critical Strike Chance with Fire Skills"
if skillFlags.lightning then
skillSpaceFlags["lightning"] = true
elseif skillFlags.cold then
skillSpaceFlags["cold"] = true
elseif skillFlags.fire then
skillSpaceFlags["fire"] = true
elseif skillFlags.chaos then
skillSpaceFlags["chaos"] = true
end
end
if weapon1Type == "None" then
for k, v in pairs(data.unarmedWeap[env.classId]) do
mod_dbMerge(modDB, "weapon1", k, v)
end
end
-- Set modes
output.mode = env.mode
if env.skillFlags.showAverage then
output.mode_average = true
end
local buffMode = getMiscVal(modDB, "misc", "buffMode", "")
if buffMode == "With buffs" then
env.mode_buffs = true
env.mode_effective = false
elseif buffMode == "Effective DPS with buffs" then
env.mode_buffs = true
env.mode_effective = true
else
env.mode_buffs = false
env.mode_effective = false
end
-- Reset namespaces
buildSpaceTable(modDB)
-- Calculate attributes
for _, stat in pairs({"str","dex","int"}) do
output["total_"..stat] = calcVal(modDB, stat)
end
-- Add attribute bonuses
mod_dbMerge(modDB, "", "lifeBase", output.total_str / 2)
local strDmgBonus = m_floor((output.total_str + getMiscVal(modDB, nil, "dexIntToMeleeBonus", 0)) / 5)
mod_dbMerge(modDB, "melee", "physicalInc", strDmgBonus)
if getMiscVal(modDB, nil, "ironGrip", false) then
mod_dbMerge(modDB, "projectile", "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_ceil(output.total_dex / 5))
end
mod_dbMerge(modDB, "", "manaBase", m_ceil(output.total_int / 2))
mod_dbMerge(modDB, "", "energyShieldInc", m_floor(output.total_int / 5))
-- Merge skill-specific modifiers
if modDB["skill:"..env.skillName] then
mod_dbMergeList(modDB, modDB["skill:"..env.skillName])
end
-- Build condition list
local condList = { }
env.condList = condList
if env.weapon1Type == "Staff" then
condList["UsingStaff"] = true
end
if env.skillFlags.mainIs1H then
if env.weapon2Type == "Shield" then
condList["UsingShield"] = true
end
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_damageMore", 1.5)
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, "noCrit", 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 = { }
env.condModList = condModList
if modDB.condMod then
for k, v in pairs(modDB.condMod) do
local isNot, condName, modName = mod.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)
-- 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.power) do
mod_listMerge(buffModList, k, v * output.powerMax)
end
end
if env.skillFlags.haveFrenzy then
for k, v in pairs(modDB.frenzy) do
mod_listMerge(buffModList, k, v * output.frenzyMax)
end
mod_listMerge(buffModList, "damageMore", 1 + output.frenzyMax * 0.04)
end
if env.skillFlags.haveEndurance then
for k, v in pairs(modDB.endurance) do
mod_listMerge(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
-- Merge buff bonuses
mod_dbMergeList(modDB, buffModList)
end
-- Merge aura modifiers
local auraEffectMod = 1 + getMiscVal(modDB, nil, "auraEffectInc", 0) / 100
for k, v in pairs(env.auraSkillModList) do
if not k:match("skill_") then
if mod.isModMult[k] then
mod_dbMerge(modDB, nil, k, m_floor(v * auraEffectMod * 100) / 100)
elseif k:match("Inc$") then
mod_dbMerge(modDB, nil, k, m_floor(v * auraEffectMod))
else
mod_dbMerge(modDB, nil, k, v * auraEffectMod)
end
end
end
end
-- Calculate primary stats: damage and defences
local function calcPrimary(env, output)
local modDB = env.modDB
-- Calculate defences
if startWatch(env, "life") then
if getMiscVal(modDB, nil, "chaosInoculation", false) then
output.total_life = 1
else
output.total_life = calcVal(modDB, "life")
end
output.total_lifeRegen = sumMods(modDB, false, "lifeRegenBase") + sumMods(modDB, false, "lifeRegenPercent") / 100 * output.total_life
endWatch(env, "life")
end
if startWatch(env, "mana") then
output.total_mana = calcVal(modDB, "mana")
output.total_manaRegen = calcVal(modDB, "manaRegen", output.total_mana * 0.0175)
endWatch(env, "mana")
end
if startWatch(env, "energyShield") then
output.total_energyShield = sumMods(modDB, false, "manaBase") * (1 + sumMods(modDB, false, "energyShieldInc", "defencesInc", "manaInc") / 100) * sumMods(modDB, true, "energyShieldMore", "defencesMore", "manaMore") * getMiscVal(modDB, nil, "manaGainAsES", 0) / 100
output.total_gear_energyShieldBase = env.itemModList.energyShieldBase or 0
for _, slot in pairs({"global","Helmet","Body Armour","Gloves","Boots","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)
endWatch(env, "energyShield")
end
if startWatch(env, "otherDef") then
output.total_evasion = 0
output.total_armour = 0
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)
for _, slot in pairs({"global","Helmet","Body Armour","Gloves","Boots","Shield"}) do
buildSpaceTable(modDB, { [slot] = true })
local evasionBase = getMiscVal(modDB, slot, "evasionBase", 0)
local armourBase = getMiscVal(modDB, slot, "armourBase", 0)
if ironReflexes then
if evasionBase > 0 or armourBase > 0 then
output.total_armour = output.total_armour + (evasionBase + armourBase) * (1 + sumMods(modDB, false, "armourInc", "evasionInc", "armourAndEvasionInc", "defencesInc") / 100) * sumMods(modDB, true, "armourMore", "evasionMore", "defencesMore")
end
else
if evasionBase > 0 then
output.total_evasion = output.total_evasion + evasionBase * (1 + sumMods(modDB, false, "evasionInc", "armourAndEvasionInc", "defencesInc") / 100) * sumMods(modDB, true, "evasionMore", "defencesMore")
end
if armourBase > 0 then
output.total_armour = output.total_armour + armourBase * (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
output.total_gear_armourBase = output.total_gear_armourBase + armourBase
end
end
output.total_blockChance = sumMods(modDB, false, "blockChance")
buildSpaceTable(modDB)
endWatch(env, "otherDef")
end
if startWatch(env, "resist") then
output.total_fireResist = sumMods(modDB, false, "fireResist", "elemResist") - 60
output.total_coldResist = sumMods(modDB, false, "coldResist", "elemResist") - 60
output.total_lightningResist = sumMods(modDB, false, "lightningResist", "elemResist") - 60
if getMiscVal(modDB, nil, "chaosInoculation", false) then
output.total_chaosResist = 100
else
output.total_chaosResist = sumMods(modDB, false, "chaosResist") - 60
end
endWatch(env, "resist")
end
-- Enable skill namespaces
buildSpaceTable(modDB, env.skillSpaceFlags)
-- Calculate pierce chance
if startWatch(env, "pierce") then
output.total_pierce = m_min(100, sumMods(modDB, false, "pierceChance")) / 100
endWatch(env, "pierce")
end
if getMiscVal(modDB, nil, "drillneck", false) then
mod_dbMerge(modDB, "projectile", "damageInc", output.total_pierce * 100)
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 == "ATTACK")
-- Calculate damage for each damage type
local combMin, combMax = 0, 0
for _, damageType in pairs(dmgTypeList) do
local min, max
if startWatch(env, damageType) then
min = calcDamage(env, output, damageType, "Min")
max = calcDamage(env, output, damageType, "Max")
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
if startWatch(env, "dps_crit") then
-- Calculate crit chance, crit multiplier, and their combined effect
if getMiscVal(modDB, nil, "noCrit", 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
output.total_critChance = m_min(calcVal(modDB, "critChance", baseCrit) / 100, 0.95)
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
if startWatch(env, "dps_speed") then
-- Calculate skill speed
if isAttack then
local baseSpeed = getMiscVal(modDB, "weapon1", "attackRate", 0)
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
if startWatch(env, "dps_hitChance") then
-- Calculate hit chance
if not isAttack or getMiscVal(modDB, "skill", "noEvade", false) or getMiscVal(modDB, nil, "noEvade", false) or getMiscVal(modDB, "weapon1", "noEvade", 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.evasionTable) or m_min(getMiscVal(modDB, "player", "level", 1), 79)
local rawChance = output.total_accuracy / (output.total_accuracy + (data.evasionTable[targetLevel] / 4) ^ 0.8) * 100
output.total_hitChance = m_max(m_min(m_floor(rawChance + 0.5) / 100, 0.95), 0.05)
end
endWatch(env, "dps_hitChance")
end
-- Calculate average damage and final DPS
output.total_avg = (combMin + combMax) / 2 * output.total_critEffect
output.total_dps = output.total_avg * output.total_speed * output.total_hitChance
-- Calculate mana cost (may be slightly off due to rounding differences)
output.total_manaCost = m_max(0, getMiscVal(modDB, "skill", "manaCostBase", 0) * (1 + sumMods(modDB, false, "manaCostInc") / 100) * sumMods(modDB, true, "manaCostMore") - sumMods(modDB, false, "manaCostBase"))
-- Calculate skill duration
if startWatch(env, "duration") then
local durationBase = getMiscVal(modDB, "skill", "durationBase", 0)
if durationBase > 0 then
output.total_duration = durationBase * (1 + sumMods(modDB, false, "durationInc") / 100) * sumMods(modDB, true, "durationMore")
end
endWatch(env, "duration")
end
if env.skillFlags.trap then
output.total_trapCooldown = 3 / (1 + getMiscVal(modDB, nil, "trapCooldownRecoveryInc", 0) / 100)
end
-- Calculate stun modifiers
if startWatch(env, "stun") then
if getMiscVal(modDB, nil, "stunImmunity", false) 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, "blockRecoveryInc") / 100)
end
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, "stun")
end
-- Calculate skill DOT components
for _, damageType in pairs(dmgTypeList) do
local baseVal = getMiscVal(modDB, "skill", damageType.."DotBase", 0)
if baseVal > 0 then
env.skillFlags.dot = true
buildSpaceTable(modDB, {
dot = not getMiscVal(modDB, "skill", "dotIsDegen", false),
degen = true,
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,
})
output["total_"..damageType.."Dot"] = baseVal * (1 + sumMods(modDB, false, "damageInc", damageType.."Inc", isElemental[damageType] and "elemInc" or nil) / 100) * sumMods(modDB, true, "damageMore", damageType.."More", isElemental[damageType] and "elemMore" or nil)
end
end
-- Calculate bleeding chance and damage
if startWatch(env, "bleed", "physical", "dps_crit") then
output.bleed_chance = m_min(100, sumMods(modDB, false, "bleedChance")) / 100
if output.total_physicalAvg > 0 then
env.skillFlags.canBleed = true
if output.bleed_chance > 0 then
env.skillFlags.dot = true
env.skillFlags.bleed = true
env.skillFlags.duration = true
buildSpaceTable(modDB, {
dot = true,
degen = 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
output.bleed_dps = baseVal * (1 + sumMods(modDB, false, "damageInc", "physicalInc") / 100) * sumMods(modDB, true, "damageMore", "physicalMore")
output.bleed_duration = 5 * (1 + sumMods(modDB, false, "durationInc") / 100) * sumMods(modDB, true, "durationMore")
end
end
endWatch(env, "bleed")
end
-- Calculate poison chance and damage
if startWatch(env, "poison", "physical", "chaos", "dps_crit") then
output.poison_chance = m_min(100, sumMods(modDB, false, "poisonChance")) / 100
if output.total_physicalAvg > 0 or output.total_chaosAvg > 0 then
env.skillFlags.canPoison = true
if output.poison_chance > 0 then
env.skillFlags.dot = true
env.skillFlags.poison = true
env.skillFlags.duration = true
buildSpaceTable(modDB, {
dot = true,
degen = true,
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.1
output.poison_dps = baseVal * (1 + sumMods(modDB, false, "damageInc", "chaosInc") / 100) * sumMods(modDB, true, "damageMore", "chaosMore")
output.poison_duration = 2 * (1 + sumMods(modDB, false, "durationInc") / 100) * sumMods(modDB, true, "durationMore")
end
end
endWatch(env, "poison")
end
-- Calculate ignite chance and damage
if startWatch(env, "ignite", "fire", "dps_crit") then
output.ignite_chance = m_min(100, sumMods(modDB, false, "igniteChance")) / 100
if output.total_fireAvg > 0 then
env.skillFlags.canIgnite = true
if output.ignite_chance > 0 then
env.skillFlags.dot = true
env.skillFlags.ignite = true
buildSpaceTable(modDB, {
dot = true,
degen = true,
ignite = 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_fireAvg * output.total_critEffect * 0.2
output.ignite_dps = baseVal * (1 + sumMods(modDB, false, "damageInc", "fireInc", "elemInc") / 100) * sumMods(modDB, true, "damageMore", "fireMore", "elemMore")
output.ignite_duration = 4 * (1 + getMiscVal(modDB, "ignite", "durationInc", 0) / 100)
end
end
endWatch(env, "ignite")
end
end
local control = { }
-- 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
-- Generate a function for calculating the effect of some modification to the environment
local function getCalculator(input, build, fullInit, modFunc)
-- Initialise environment
local env, errMsg = initEnv(input, build)
if errMsg then
return
end
-- Save a copy of the initial mod list
if fullInit then
mergeItemMods(env, build)
env.specModList = buildNodeModList(env, build.spec.allocNodes, true)
mod_dbMergeList(env.modDB, env.specModList)
end
local initModDB = copyTable(env.modDB)
if not fullInit then
mergeItemMods(env, build)
env.specModList = buildNodeModList(env, build.spec.allocNodes, true)
mod_dbMergeList(env.modDB, env.specModList)
end
-- Run base calculation pass while building watch lists
local base = { }
local outputMeta = { __index = base }
env.watchers = { }
env.buildWatch = true
env.modDB._activeWatchers = { }
calcSetup(env, base)
local baseModDB = copyTable(env.modDB)
baseModDB._activeWatchers = nil
calcPrimary(env, base)
env.buildWatch = false
env.modDB._activeWatchers = nil
-- Generate list of watched mods
env.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 = mod.getSpaceName(k)
if not env.watchedModList[spaceName] then
env.watchedModList[spaceName] = { }
end
if not baseModDB[spaceName] then
baseModDB[spaceName] = { }
end
if not initModDB[spaceName] then
initModDB[spaceName] = { }
end
if not env.watchedModList[spaceName][modName] then
env.watchedModList[spaceName][modName] = { baseModDB[spaceName][modName], { } }
end
env.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 list
resetModDB(env.modDB, initModDB)
-- Call function to make modifications to the enviroment
modFunc(env, ...)
-- Prepare for calculation
local output = setmetatable({ }, outputMeta)
calcSetup(env, output)
-- Check if any watched variables have changed
local active = false
for spaceName, watchedMods in pairs(env.watchedModList) do
for k, v in pairs(env.modDB[spaceName]) do
local watchedMod = watchedMods[k]
if watchedMod and v ~= watchedMod[1] then
for watchList in pairs(watchedMod[2]) do
watchList._flag = true
flagged[watchList] = true
end
active = true
end
end
end
if not active then
return base
end
-- Run the calculations
calcPrimary(env, output)
-- Reset watcher flags
for watchList in pairs(flagged) do
watchList._flag = false
flagged[watchList] = nil
end
return output
end, base
end
-- Get calculator for tree node modifiers
function control.getNodeCalculator(input, build)
return getCalculator(input, build, true, function(env, nodeList, remove)
-- Build and merge 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 control.getItemCalculator(input, build)
return getCalculator(input, build, false, function(env, repSlot, repItem)
-- Build and merge item mod list
mergeItemMods(env, build, repSlot, repItem)
-- Build and merge spec mod list
env.specModList = buildNodeModList(env, build.spec.allocNodes, true)
mod_dbMergeList(env.modDB, env.specModList)
end)
end
-- Build output for display in the grid
function control.buildOutput(input, output, build)
-- Initialise environment
local env, errMsg = initEnv(input, build)
if errMsg then
setViewMode({ })
return errMsg
end
-- Calculate primary stats
mergeItemMods(env, build)
env.specModList = buildNodeModList(env, build.spec.allocNodes, true)
mod_dbMergeList(env.modDB, env.specModList)
calcSetup(env, output)
calcPrimary(env, output)
-- 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
output.skill_partName = env.skillPartName
output.total_extraPoints = getMiscVal(env.modDB, nil, "extraPoints", 0)
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
-- Calculate weapon DPS for display
for _, weapon in pairs({"weapon1","weapon2"}) do
local weaponDPS = (getMiscVal(env.modDB, weapon, damageType.."Min", 0) + getMiscVal(env.modDB, weapon, damageType.."Max", 0)) / 2 * getMiscVal(env.modDB, weapon, "attackRate", 1)
output[weapon.."_damageDPS"] = (output[weapon.."damageDPS"] or 0) + weaponDPS
if isElemental[damageType] then
output[weapon.."_elemDPS"] = (output[weapon.."_elemDPS"] or 0) + weaponDPS
end
output[weapon.."_"..damageType.."DPS"] = weaponDPS
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 = input.player_level or 1
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.skillFlags)
ConPrintf("== Skill Gems ==")
for _, gem in ipairs(env.gemList) do
if gem.cantSupport then
ConPrintf("^1%s %d/%d", gem.name, gem.level, gem.qual)
else
ConPrintf("%s %d/%d", gem.name, gem.level, gem.qual)
end
end
ConPrintf("== Namespaces ==")
mod.listPrint(env.skillSpaceFlags)
ConPrintf("== Skill Mods ==")
mod.listPrint(env.skillModList)
ConPrintf("== Spec Mods ==")
mod.listPrint(env.specModList)
ConPrintf("== Item Mods ==")
mod.listPrint(env.itemModList)
ConPrintf("== Conditions ==")
mod.listPrint(env.condList)
ConPrintf("== Conditional Modifiers ==")
mod.listPrint(env.condModList)
if env.buffModList then
ConPrintf("== Buff Mods ==")
mod.listPrint(env.buffModList)
end
end
return control