Adds pantheons from temmings' pull request Adds partial Impale support from baranio's pull request Adds updated uniques from PJacek's pull request and my own Adds more tree highlighting options for node power from coldino's pull request Adds support for fossil mods in the crafting window. Including correct parsing for some mods that previously didn't work
516 lines
17 KiB
Lua
516 lines
17 KiB
Lua
-- Path of Building
|
|
--
|
|
-- Module: Calcs Tab
|
|
-- Calculations breakdown tab for the current build.
|
|
--
|
|
local pairs = pairs
|
|
local ipairs = ipairs
|
|
local t_insert = table.insert
|
|
local t_remove = table.remove
|
|
local m_max = math.max
|
|
local m_min = math.min
|
|
local m_floor = math.floor
|
|
local band = bit.band
|
|
|
|
local calcs = { }
|
|
local sectionData = { }
|
|
for _, targetVersion in ipairs(targetVersionList) do
|
|
calcs[targetVersion] = LoadModule("Modules/Calcs", targetVersion)
|
|
sectionData[targetVersion] = LoadModule("Modules/CalcSections-"..targetVersion)
|
|
end
|
|
|
|
local buffModeDropList = {
|
|
{ label = "Unbuffed", buffMode = "UNBUFFED" },
|
|
{ label = "Buffed", buffMode = "BUFFED" },
|
|
{ label = "In Combat", buffMode = "COMBAT" },
|
|
{ label = "Effective DPS", buffMode = "EFFECTIVE" }
|
|
}
|
|
|
|
local CalcsTabClass = newClass("CalcsTab", "UndoHandler", "ControlHost", "Control", function(self, build)
|
|
self.UndoHandler()
|
|
self.ControlHost()
|
|
self.Control()
|
|
|
|
self.build = build
|
|
|
|
self.calcs = calcs[build.targetVersion]
|
|
|
|
self.input = { }
|
|
self.input.skill_number = 1
|
|
self.input.misc_buffMode = "EFFECTIVE"
|
|
|
|
self.colWidth = 230
|
|
self.sectionList = { }
|
|
|
|
-- Special section for skill/mode selection
|
|
self:NewSection(3, "SkillSelect", 1, "View Skill Details", colorCodes.NORMAL, {
|
|
{ label = "Socket Group", { controlName = "mainSocketGroup",
|
|
control = new("DropDownControl", nil, 0, 0, 300, 16, nil, function(index, value)
|
|
self.input.skill_number = index
|
|
self:AddUndoState()
|
|
self.build.buildFlag = true
|
|
end)
|
|
}, },
|
|
{ label = "Active Skill", { controlName = "mainSkill",
|
|
control = new("DropDownControl", nil, 0, 0, 300, 16, nil, function(index, value)
|
|
local mainSocketGroup = self.build.skillsTab.socketGroupList[self.input.skill_number]
|
|
mainSocketGroup.mainActiveSkillCalcs = index
|
|
self.build.buildFlag = true
|
|
end)
|
|
}, },
|
|
{ label = "Skill Part", playerFlag = "multiPart", { controlName = "mainSkillPart",
|
|
control = new("DropDownControl", nil, 0, 0, 150, 16, nil, function(index, value)
|
|
local mainSocketGroup = self.build.skillsTab.socketGroupList[self.input.skill_number]
|
|
local srcInstance = mainSocketGroup.displaySkillListCalcs[mainSocketGroup.mainActiveSkillCalcs].activeEffect.srcInstance
|
|
srcInstance.skillPartCalcs = index
|
|
self:AddUndoState()
|
|
self.build.buildFlag = true
|
|
end)
|
|
}, },
|
|
{ label = "Show Minion Stats", flag = "haveMinion", { controlName = "showMinion",
|
|
control = new("CheckBoxControl", nil, 0, 0, 18, nil, function(state)
|
|
self.input.showMinion = state
|
|
self:AddUndoState()
|
|
end, "Show stats for the minion instead of the player.")
|
|
}, },
|
|
{ label = "Minion", flag = "minion", { controlName = "mainSkillMinion",
|
|
control = new("DropDownControl", nil, 0, 0, 160, 16, nil, function(index, value)
|
|
local mainSocketGroup = self.build.skillsTab.socketGroupList[self.input.skill_number]
|
|
local srcInstance = mainSocketGroup.displaySkillListCalcs[mainSocketGroup.mainActiveSkillCalcs].activeEffect.srcInstance
|
|
if value.itemSetId then
|
|
srcInstance.skillMinionItemSetCalcs = value.itemSetId
|
|
else
|
|
srcInstance.skillMinionCalcs = value.minionId
|
|
end
|
|
self:AddUndoState()
|
|
self.build.buildFlag = true
|
|
end)
|
|
} },
|
|
{ label = "Spectre Library", flag = "spectre", { controlName = "mainSkillMinionLibrary",
|
|
control = new("ButtonControl", nil, 0, 0, 100, 16, "Manage Spectres...", function()
|
|
self.build:OpenSpectreLibrary()
|
|
end)
|
|
} },
|
|
{ label = "Minion Skill", flag = "haveMinion", { controlName = "mainSkillMinionSkill",
|
|
control = new("DropDownControl", nil, 0, 0, 200, 16, nil, function(index, value)
|
|
local mainSocketGroup = self.build.skillsTab.socketGroupList[self.input.skill_number]
|
|
local srcInstance = mainSocketGroup.displaySkillListCalcs[mainSocketGroup.mainActiveSkillCalcs].activeEffect.srcInstance
|
|
srcInstance.skillMinionSkillCalcs = index
|
|
self:AddUndoState()
|
|
self.build.buildFlag = true
|
|
end)
|
|
} },
|
|
{ label = "Calculation Mode", {
|
|
controlName = "mode",
|
|
control = new("DropDownControl", nil, 0, 0, 100, 16, buffModeDropList, function(index, value)
|
|
self.input.misc_buffMode = value.buffMode
|
|
self:AddUndoState()
|
|
self.build.buildFlag = true
|
|
end, [[
|
|
This controls the calculation of the stats shown in this tab.
|
|
The stats in the sidebar are always shown in Effective DPS mode, regardless of this setting.
|
|
|
|
Unbuffed: No auras, buffs, or other support skills or effects will apply. This is equivelant to standing in town.
|
|
Buffed: Aura and buff skills apply. This is equivelant to standing in your hideout with auras and buffs turned on.
|
|
In Combat: Charges and combat buffs such as Onslaught will also apply. This will show your character sheet stats in combat.
|
|
Effective DPS: Curses and enemy properties (such as resistances and status conditions) will also apply. This estimates your true DPS.]])
|
|
}, },
|
|
{ label = "Aura and Buff Skills", flag = "buffs", textSize = 12, { format = "{output:BuffList}", { breakdown = "SkillBuffs" } }, },
|
|
{ label = "Combat Buffs", flag = "combat", textSize = 12, { format = "{output:CombatList}" }, },
|
|
{ label = "Curses and Debuffs", flag = "effective", textSize = 12, { format = "{output:CurseList}", { breakdown = "SkillDebuffs" } }, },
|
|
}, function(section)
|
|
self.build:RefreshSkillSelectControls(section.controls, self.input.skill_number, "Calcs")
|
|
section.controls.showMinion.state = self.input.showMinion
|
|
section.controls.mode:SelByValue(self.input.misc_buffMode, "buffMode")
|
|
end)
|
|
self.sectionList[1].controls.mainSocketGroup.tooltipFunc = function(tooltip, mode, index, value)
|
|
local socketGroup = self.build.skillsTab.socketGroupList[index]
|
|
if socketGroup and tooltip:CheckForUpdate(socketGroup, self.build.outputRevision) then
|
|
self.build.skillsTab:AddSocketGroupTooltip(tooltip, socketGroup)
|
|
end
|
|
end
|
|
|
|
-- Add sections from the CalcSections module
|
|
for _, section in ipairs(sectionData[build.targetVersion]) do
|
|
self:NewSection(unpack(section))
|
|
end
|
|
|
|
self.controls.breakdown = new("CalcBreakdownControl", self)
|
|
|
|
self.controls.scrollBar = new("ScrollBarControl", {"TOPRIGHT",self,"TOPRIGHT"}, 0, 0, 18, 0, 50, "VERTICAL", true)
|
|
end)
|
|
|
|
function CalcsTabClass:Load(xml, dbFileName)
|
|
for _, node in ipairs(xml) do
|
|
if type(node) == "table" then
|
|
if node.elem == "Input" then
|
|
if not node.attrib.name then
|
|
launch:ShowErrMsg("^1Error parsing '%s': 'Input' element missing name attribute", fileName)
|
|
return true
|
|
end
|
|
if node.attrib.number then
|
|
self.input[node.attrib.name] = tonumber(node.attrib.number)
|
|
elseif node.attrib.string then
|
|
self.input[node.attrib.name] = node.attrib.string
|
|
elseif node.attrib.boolean then
|
|
self.input[node.attrib.name] = node.attrib.boolean == "true"
|
|
else
|
|
launch:ShowErrMsg("^1Error parsing '%s': 'Input' element missing number, string or boolean attribute", fileName)
|
|
return true
|
|
end
|
|
elseif node.elem == "Section" then
|
|
if not node.attrib.id then
|
|
launch:ShowErrMsg("^1Error parsing '%s': 'Section' element missing id attribute", fileName)
|
|
return true
|
|
end
|
|
for _, section in ipairs(self.sectionList) do
|
|
if section.id == node.attrib.id then
|
|
section.collapsed = (node.attrib.collapsed == "true")
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
self:ResetUndo()
|
|
end
|
|
|
|
function CalcsTabClass:Save(xml)
|
|
for k, v in pairs(self.input) do
|
|
local child = { elem = "Input", attrib = {name = k} }
|
|
if type(v) == "number" then
|
|
child.attrib.number = tostring(v)
|
|
elseif type(v) == "boolean" then
|
|
child.attrib.boolean = tostring(v)
|
|
else
|
|
child.attrib.string = tostring(v)
|
|
end
|
|
t_insert(xml, child)
|
|
end
|
|
for _, section in ipairs(self.sectionList) do
|
|
t_insert(xml, { elem = "Section", attrib = {
|
|
id = section.id,
|
|
collapsed = tostring(section.collapsed),
|
|
} })
|
|
end
|
|
self.modFlag = false
|
|
end
|
|
|
|
function CalcsTabClass:Draw(viewPort, inputEvents)
|
|
self.x = viewPort.x
|
|
self.y = viewPort.y
|
|
self.width = viewPort.width
|
|
self.height = viewPort.height
|
|
|
|
-- Arrange the sections
|
|
local baseX = viewPort.x + 4
|
|
local baseY = viewPort.y + 4
|
|
local maxCol = m_floor(viewPort.width / (self.colWidth + 8))
|
|
local colY = { }
|
|
local maxY = 0
|
|
for _, section in ipairs(self.sectionList) do
|
|
section:UpdateSize()
|
|
if section.enabled then
|
|
local col
|
|
if section.group == 1 then
|
|
-- Group 1: Offense
|
|
-- This group is put into the first 3 columns, with each section placed into the highest available location
|
|
col = 1
|
|
local minY = colY[col] or baseY
|
|
for c = 2, 3 do
|
|
if (colY[c] or baseY) < minY then
|
|
col = c
|
|
minY = colY[c] or baseY
|
|
end
|
|
end
|
|
elseif section.group == 2 then
|
|
-- Group 2: Defense (the first 4 sections)
|
|
-- This group is put entirely into the 4th column
|
|
col = 4
|
|
elseif section.group == 3 then
|
|
-- Group 3: Defense (the remaining sections)
|
|
-- This group is put into a 5th column if there's room for one, otherwise they are handled separately
|
|
if maxCol >= 5 then
|
|
col = 5
|
|
end
|
|
end
|
|
if col then
|
|
section.x = baseX + (self.colWidth + 8) * (col - 1)
|
|
section.y = colY[col] or baseY
|
|
for c = col, col + section.widthCols - 1 do
|
|
colY[c] = section.y + section.height + 8
|
|
end
|
|
maxY = m_max(maxY, colY[col])
|
|
end
|
|
end
|
|
end
|
|
if maxCol < 5 then
|
|
-- There's no room for a 5th column
|
|
-- Each section from group 3 will instead be placed into column 4 if there's room, otherwise they'll be put in columns 1-3
|
|
for c = 1, 3 do
|
|
colY[c] = m_max(colY[1], colY[2], colY[3])
|
|
end
|
|
for _, section in ipairs(self.sectionList) do
|
|
if section.enabled and section.group == 3 then
|
|
local col = 4
|
|
if colY[col] + section.height + 4 >= m_max(viewPort.y + viewPort.height, maxY) then
|
|
-- No room in the 4th column, find the highest available location in columns 1-4
|
|
local minY = colY[col]
|
|
for c = 3, 1, -1 do
|
|
if colY[c] < minY then
|
|
col = c
|
|
minY = colY[c]
|
|
end
|
|
end
|
|
end
|
|
section.x = baseX + (self.colWidth + 8) * (col - 1)
|
|
section.y = colY[col]
|
|
colY[col] = section.y + section.height + 8
|
|
maxY = m_max(maxY, colY[col])
|
|
end
|
|
end
|
|
end
|
|
self.controls.scrollBar.height = viewPort.height
|
|
self.controls.scrollBar:SetContentDimension(maxY - baseY, viewPort.height)
|
|
for _, section in ipairs(self.sectionList) do
|
|
-- Give sections their actual Y position and let them update
|
|
section.y = section.y - self.controls.scrollBar.offset
|
|
section:UpdatePos()
|
|
end
|
|
|
|
for id, event in ipairs(inputEvents) do
|
|
if event.type == "KeyDown" then
|
|
if event.key == "z" and IsKeyDown("CTRL") then
|
|
self:Undo()
|
|
self.build.buildFlag = true
|
|
elseif event.key == "y" and IsKeyDown("CTRL") then
|
|
self:Redo()
|
|
self.build.buildFlag = true
|
|
end
|
|
end
|
|
end
|
|
self:ProcessControlsInput(inputEvents, viewPort)
|
|
for id, event in ipairs(inputEvents) do
|
|
if event.type == "KeyUp" then
|
|
if event.key == "WHEELDOWN" then
|
|
self.controls.scrollBar:Scroll(1)
|
|
elseif event.key == "WHEELUP" then
|
|
self.controls.scrollBar:Scroll(-1)
|
|
end
|
|
end
|
|
end
|
|
|
|
main:DrawBackground(viewPort)
|
|
|
|
if not self.displayPinned then
|
|
self.displayData = nil
|
|
end
|
|
|
|
self:DrawControls(viewPort)
|
|
|
|
if self.displayData then
|
|
if self.displayPinned and not self.selControl then
|
|
self:SelectControl(self.controls.breakdown)
|
|
end
|
|
else
|
|
self.controls.breakdown:SetBreakdownData()
|
|
end
|
|
end
|
|
|
|
function CalcsTabClass:NewSection(width, ...)
|
|
local section = new("CalcSectionControl", self, width * self.colWidth + 8 * (width - 1), ...)
|
|
section.widthCols = width
|
|
t_insert(self.controls, section)
|
|
t_insert(self.sectionList, section)
|
|
end
|
|
|
|
function CalcsTabClass:ClearDisplayStat()
|
|
self.displayData = nil
|
|
self.displayPinned = nil
|
|
self.controls.breakdown:SetBreakdownData()
|
|
end
|
|
|
|
function CalcsTabClass:SetDisplayStat(displayData, pin)
|
|
if not displayData or (not pin and self.displayPinned) then
|
|
return
|
|
end
|
|
self.displayData = displayData
|
|
self.displayPinned = pin
|
|
self.controls.breakdown:SetBreakdownData(displayData, pin)
|
|
end
|
|
|
|
function CalcsTabClass:CheckFlag(obj)
|
|
local actor = self.input.showMinion and self.calcsEnv.minion or self.calcsEnv.player
|
|
local skillFlags = actor.mainSkill.skillFlags
|
|
if obj.flag and not skillFlags[obj.flag] then
|
|
return
|
|
end
|
|
if obj.flagList then
|
|
for _, flag in ipairs(obj.flagList) do
|
|
if not skillFlags[flag] then
|
|
return
|
|
end
|
|
end
|
|
end
|
|
if obj.playerFlag and not self.calcsEnv.player.mainSkill.skillFlags[obj.playerFlag] then
|
|
return
|
|
end
|
|
if obj.notFlag and skillFlags[obj.notFlag] then
|
|
return
|
|
end
|
|
if obj.notFlagList then
|
|
for _, flag in ipairs(obj.notFlagList) do
|
|
if skillFlags[flag] then
|
|
return
|
|
end
|
|
end
|
|
end
|
|
if obj.haveOutput then
|
|
local ns, var = obj.haveOutput:match("^(%a+)%.(%a+)$")
|
|
if ns then
|
|
if not actor.output[ns] or not actor.output[ns][var] or actor.output[ns][var] == 0 then
|
|
return
|
|
end
|
|
elseif not actor.output[obj.haveOutput] or actor.output[obj.haveOutput] == 0 then
|
|
return
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- Build the calculation output tables
|
|
function CalcsTabClass:BuildOutput()
|
|
self.powerBuildFlag = true
|
|
|
|
--[[
|
|
local start = GetTime()
|
|
SetProfiling(true)
|
|
for i = 1, 1000 do
|
|
self.calcs.buildOutput(self.build, "MAIN")
|
|
end
|
|
SetProfiling(false)
|
|
ConPrintf("Calc time: %d msec", GetTime() - start)
|
|
--]]
|
|
|
|
for _, node in pairs(self.build.spec.nodes) do
|
|
-- Set default final mod list for all nodes; some may not be set during the main pass
|
|
node.finalModList = node.modList
|
|
end
|
|
|
|
self.mainEnv = self.calcs.buildOutput(self.build, "MAIN")
|
|
self.mainOutput = self.mainEnv.player.output
|
|
self.calcsEnv = self.calcs.buildOutput(self.build, "CALCS")
|
|
self.calcsOutput = self.calcsEnv.player.output
|
|
|
|
if self.displayData then
|
|
self.controls.breakdown:SetBreakdownData()
|
|
self.controls.breakdown:SetBreakdownData(self.displayData, self.displayPinned)
|
|
end
|
|
|
|
-- Retrieve calculator functions
|
|
self.nodeCalculator = { self.calcs.getNodeCalculator(self.build) }
|
|
self.miscCalculator = { self.calcs.getMiscCalculator(self.build) }
|
|
end
|
|
|
|
-- Controls the coroutine that calculations node power
|
|
function CalcsTabClass:BuildPower()
|
|
if self.powerBuildFlag then
|
|
self.powerBuildFlag = false
|
|
self.powerBuilder = coroutine.create(self.PowerBuilder)
|
|
end
|
|
if self.powerBuilder then
|
|
collectgarbage("stop")
|
|
local res, errMsg = coroutine.resume(self.powerBuilder, self)
|
|
if launch.devMode and not res then
|
|
error(errMsg)
|
|
end
|
|
if coroutine.status(self.powerBuilder) == "dead" then
|
|
self.powerBuilder = nil
|
|
end
|
|
collectgarbage("restart")
|
|
end
|
|
end
|
|
|
|
-- Estimate the offensive and defensive power of all unallocated nodes
|
|
function CalcsTabClass:PowerBuilder()
|
|
local calcFunc, calcBase = self:GetNodeCalculator()
|
|
local cache = { }
|
|
local newPowerMax = {
|
|
singleStat = 0,
|
|
offence = 0,
|
|
defence = 0
|
|
}
|
|
if not self.powerMax then
|
|
self.powerMax = newPowerMax
|
|
end
|
|
if coroutine.running() then
|
|
coroutine.yield()
|
|
end
|
|
local start = GetTime()
|
|
for _, node in pairs(self.build.spec.nodes) do
|
|
wipeTable(node.power)
|
|
if not node.alloc and node.modKey ~= "" then
|
|
if not cache[node.modKey] then
|
|
cache[node.modKey] = calcFunc({node})
|
|
end
|
|
local output = cache[node.modKey]
|
|
if self.powerStat and self.powerStat.stat and not self.powerStat.ignoreForNodes then
|
|
node.power.singleStat = self:CalculatePowerStat(self.powerStat, output, calcBase)
|
|
if node.path then
|
|
newPowerMax.singleStat = m_max(newPowerMax.singleStat, node.power.singleStat)
|
|
end
|
|
else
|
|
if calcBase.Minion then
|
|
node.power.offence = (output.Minion.CombinedDPS - calcBase.Minion.CombinedDPS) / calcBase.Minion.CombinedDPS
|
|
else
|
|
node.power.offence = (output.CombinedDPS - calcBase.CombinedDPS) / calcBase.CombinedDPS
|
|
end
|
|
node.power.defence = (output.LifeUnreserved - calcBase.LifeUnreserved) / m_max(3000, calcBase.Life) +
|
|
(output.Armour - calcBase.Armour) / m_max(10000, calcBase.Armour) +
|
|
(output.EnergyShield - calcBase.EnergyShield) / m_max(3000, calcBase.EnergyShield) +
|
|
(output.Evasion - calcBase.Evasion) / m_max(10000, calcBase.Evasion) +
|
|
(output.LifeRegen - calcBase.LifeRegen) / 500 +
|
|
(output.EnergyShieldRegen - calcBase.EnergyShieldRegen) / 1000
|
|
if node.path then
|
|
newPowerMax.offence = m_max(newPowerMax.offence, node.power.offence)
|
|
newPowerMax.defence = m_max(newPowerMax.defence, node.power.defence)
|
|
end
|
|
end
|
|
end
|
|
if coroutine.running() and GetTime() - start > 100 then
|
|
coroutine.yield()
|
|
start = GetTime()
|
|
end
|
|
end
|
|
self.powerMax = newPowerMax
|
|
end
|
|
|
|
function CalcsTabClass:CalculatePowerStat(selection, original, modified)
|
|
originalValue = original[selection.stat] or 0
|
|
modifiedValue = modified[selection.stat] or 0
|
|
if selection.transform then
|
|
originalValue = selection.transform(originalValue)
|
|
modifiedValue = selection.transform(modifiedValue)
|
|
end
|
|
return originalValue - modifiedValue
|
|
end
|
|
|
|
function CalcsTabClass:GetNodeCalculator()
|
|
return unpack(self.nodeCalculator)
|
|
end
|
|
|
|
function CalcsTabClass:GetMiscCalculator()
|
|
return unpack(self.miscCalculator)
|
|
end
|
|
|
|
function CalcsTabClass:CreateUndoState()
|
|
return copyTable(self.input)
|
|
end
|
|
|
|
function CalcsTabClass:RestoreUndoState(state)
|
|
wipeTable(self.input)
|
|
for k, v in pairs(state) do
|
|
self.input[k] = v
|
|
end
|
|
end
|