1405 lines
44 KiB
Lua
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
|