Files
PathOfBuilding/Classes/TreeTab.lua

719 lines
28 KiB
Lua

-- Path of Building
--
-- Module: Tree Tab
-- Passive skill tree tab for the current build.
--
local ipairs = ipairs
local t_insert = table.insert
local t_sort = table.sort
local m_max = math.max
local m_min = math.min
local m_floor = math.floor
local s_format = string.format
local TreeTabClass = newClass("TreeTab", "ControlHost", function(self, build)
self.ControlHost()
self.build = build
self.viewer = new("PassiveTreeView")
self.specList = { }
self.specList[1] = new("PassiveSpec", build, build.targetVersionData.latestTreeVersion)
self:SetActiveSpec(1)
self.anchorControls = new("Control", nil, 0, 0, 0, 20)
self.controls.specSelect = new("DropDownControl", {"LEFT",self.anchorControls,"RIGHT"}, 0, 0, 190, 20, nil, function(index, value)
if self.specList[index] then
self.build.modFlag = true
self:SetActiveSpec(index)
else
self:OpenSpecManagePopup()
end
end)
self.controls.specSelect.tooltipFunc = function(tooltip, mode, selIndex, selVal)
tooltip:Clear()
if mode ~= "OUT" then
local spec = self.specList[selIndex]
if spec then
local used, ascUsed, sockets = spec:CountAllocNodes()
tooltip:AddLine(16, "Class: "..spec.curClassName)
tooltip:AddLine(16, "Ascendancy: "..spec.curAscendClassName)
tooltip:AddLine(16, "Points used: "..used)
if sockets > 0 then
tooltip:AddLine(16, "Jewel sockets: "..sockets)
end
if selIndex ~= self.activeSpec then
local calcFunc, calcBase = self.build.calcsTab:GetMiscCalculator()
if calcFunc then
local output = calcFunc({ spec = spec })
self.build:AddStatComparesToTooltip(tooltip, calcBase, output, "^7Switching to this tree will give you:")
end
if spec.curClassId == self.build.spec.curClassId then
local respec = 0
for nodeId, node in pairs(self.build.spec.allocNodes) do
if node.type ~= "ClassStart" and node.type ~= "AscendClassStart" and not spec.allocNodes[nodeId] then
if node.ascendancyName then
respec = respec + 5
else
respec = respec + 1
end
end
end
if respec > 0 then
tooltip:AddLine(16, "^7Switching to this tree requires "..respec.." refund points.")
end
end
end
tooltip:AddLine(16, "Game Version: "..treeVersions[spec.treeVersion].short)
end
end
end
self.controls.reset = new("ButtonControl", {"LEFT",self.controls.specSelect,"RIGHT"}, 8, 0, 60, 20, "Reset", function()
main:OpenConfirmPopup("Reset Tree", "Are you sure you want to reset your passive tree?", "Reset", function()
self.build.spec:ResetNodes()
self.build.spec:AddUndoState()
self.build.buildFlag = true
end)
end)
self.controls.import = new("ButtonControl", {"LEFT",self.controls.reset,"RIGHT"}, 8, 0, 90, 20, "Import Tree", function()
self:OpenImportPopup()
end)
self.controls.export = new("ButtonControl", {"LEFT",self.controls.import,"RIGHT"}, 8, 0, 90, 20, "Export Tree", function()
self:OpenExportPopup()
end)
self.controls.treeSearch = new("EditControl", {"LEFT",self.controls.export,"RIGHT"}, 8, 0, 300, 20, "", "Search", "%c%(%)", 100, function(buf)
self.viewer.searchStr = buf
end)
self.controls.treeHeatMap = new("CheckBoxControl", {"LEFT",self.controls.treeSearch,"RIGHT"}, 130, 0, 20, "Show Node Power:", function(state)
self.viewer.showHeatMap = state
self.controls.treeHeatMapStatSelect.shown = state
end)
self.controls.treeHeatMapStatSelect = new("DropDownControl", {"LEFT",self.controls.treeHeatMap,"RIGHT"}, 8, 0, 150, 20, nil, function(index, value)
self:SetPowerCalc(value)
end)
self.controls.treeHeatMap.tooltipText = function()
local offCol, defCol = main.nodePowerTheme:match("(%a+)/(%a+)")
return "When enabled, an estimate of the offensive and defensive strength of\neach unallocated passive is calculated and displayed visually.\nOffensive power shows as "..offCol:lower()..", defensive power as "..defCol:lower().."."
end
self.powerStatList = { }
for _, stat in ipairs(data.powerStatList) do
if not stat.ignoreForNodes then
t_insert(self.powerStatList, stat)
end
end
self.controls.treeHeatMapTopStat = new("CheckBoxControl", {"LEFT", self.controls.treeHeatMapStatSelect,"RIGHT"}, 110, 0, 20, "Show top node:", function(state)
self.viewer.heatMapTopPick = state
end )
self.controls.treeHeatMapTopStat.tooltipText = function()
return "When enabled, only the strongest node for the selected stat will be highlighted."
end
self.controls.treeHeatMapStatPerPoint = new("CheckBoxControl", {"LEFT", self.controls.treeHeatMapTopStat,"RIGHT"}, 115, 0, 20, "Power per point:", function(state)
self.viewer.heatMapStatPerPoint = state
end )
self.controls.treeHeatMapStatPerPoint.tooltipText = function()
return "When enabled, node power is divided by the point cost it would take to get there,\nso closer points are considered stronger"
end
self.controls.powerReport = new("ButtonControl", {"LEFT", self.controls.treeHeatMapStatPerPoint, "RIGHT"}, 8, 0, 120, 20, "Power Report", function()
self:ShowPowerReport()
end)
self.controls.powerReport.tooltipText = function()
return "View a report of node efficacy based on current heat map selection"
end
self.controls.specConvertText = new("LabelControl", {"BOTTOMLEFT",self.controls.specSelect,"TOPLEFT"}, 0, -14, 0, 16, "^7This is an older tree version, which may not be fully compatible with the current game version.")
self.controls.specConvertText.shown = function()
return self.showConvert
end
self.controls.specConvert = new("ButtonControl", {"LEFT",self.controls.specConvertText,"RIGHT"}, 8, 0, 120, 20, "^2Convert to "..treeVersions[self.build.targetVersionData.latestTreeVersion].short, function()
local newSpec = new("PassiveSpec", self.build, self.build.targetVersionData.latestTreeVersion)
newSpec.title = self.build.spec.title
newSpec.jewels = copyTable(self.build.spec.jewels)
newSpec:RestoreUndoState(self.build.spec:CreateUndoState())
newSpec:BuildClusterJewelGraphs()
t_insert(self.specList, self.activeSpec + 1, newSpec)
self:SetActiveSpec(self.activeSpec + 1)
self.modFlag = true
main:OpenMessagePopup("Tree Converted", "The tree has been converted to "..treeVersions[self.build.targetVersionData.latestTreeVersion].short..".\nNote that some or all of the passives may have been de-allocated due to changes in the tree.\n\nYou can switch back to the old tree using the tree selector at the bottom left.")
end)
self.jumpToNode = false
self.jumpToX = 0
self.jumpToY = 0
end)
function TreeTabClass:Draw(viewPort, inputEvents)
self.anchorControls.x = viewPort.x + 4
self.anchorControls.y = viewPort.y + viewPort.height - 24
for id, event in ipairs(inputEvents) do
if event.type == "KeyDown" then
if event.key == "z" and IsKeyDown("CTRL") then
self.build.spec:Undo()
self.build.buildFlag = true
inputEvents[id] = nil
elseif event.key == "y" and IsKeyDown("CTRL") then
self.build.spec:Redo()
self.build.buildFlag = true
inputEvents[id] = nil
elseif event.key == "f" and IsKeyDown("CTRL") then
self:SelectControl(self.controls.treeSearch)
end
end
end
self:ProcessControlsInput(inputEvents, viewPort)
-- Determine positions if one line of controls doesn't fit in the screen width
local twoLineHeight = self.controls.treeHeatMap.y == 24 and 26 or 0
if(select(1, self.controls.treeHeatMapStatPerPoint:GetPos()) + select(1, self.controls.treeHeatMapStatPerPoint:GetSize()) > viewPort.x + viewPort.width) then
twoLineHeight = 26
self.controls.treeHeatMap:SetAnchor("BOTTOMLEFT",self.controls.specSelect,"BOTTOMLEFT",nil,nil,nil)
self.controls.treeHeatMap.y = 24
self.controls.treeHeatMap.x = 125
self.controls.specSelect.y = -24
self.controls.specConvertText.y = -16
elseif viewPort.x + viewPort.width - (select(1, self.controls.treeSearch:GetPos()) + select(1, self.controls.treeSearch:GetSize())) > (select(1, self.controls.treeHeatMapStatPerPoint:GetPos()) + select(1, self.controls.treeHeatMapStatPerPoint:GetSize())) - viewPort.x then
twoLineHeight = 0
self.controls.treeHeatMap:SetAnchor("LEFT",self.controls.treeSearch,"RIGHT",nil,nil,nil)
self.controls.treeHeatMap.y = 0
self.controls.treeHeatMap.x = 130
self.controls.specSelect.y = 0
self.controls.specConvertText.y = -14
end
--
local treeViewPort = { x = viewPort.x, y = viewPort.y, width = viewPort.width, height = viewPort.height - (self.showConvert and 64 + twoLineHeight or 32 + twoLineHeight)}
if self.jumpToNode then
self.viewer:Focus(self.jumpToX, self.jumpToY, treeViewPort, self.build)
self.jumpToNode = false
end
self.viewer:Draw(self.build, treeViewPort, inputEvents)
self.controls.specSelect.selIndex = self.activeSpec
wipeTable(self.controls.specSelect.list)
for id, spec in ipairs(self.specList) do
t_insert(self.controls.specSelect.list, (spec.treeVersion ~= self.build.targetVersionData.latestTreeVersion and ("["..treeVersions[spec.treeVersion].short.."] ") or "")..(spec.title or "Default"))
end
t_insert(self.controls.specSelect.list, "Manage trees...")
if not self.controls.treeSearch.hasFocus then
self.controls.treeSearch:SetText(self.viewer.searchStr)
end
self.controls.treeHeatMap.state = self.viewer.showHeatMap
self.controls.treeHeatMapStatSelect.list = self.powerStatList
self.controls.treeHeatMapStatSelect.selIndex = 1
if self.build.calcsTab.powerStat then
self.controls.treeHeatMapStatSelect:SelByValue(self.build.calcsTab.powerStat.stat, "stat")
end
SetDrawLayer(1)
SetDrawColor(0.05, 0.05, 0.05)
DrawImage(nil, viewPort.x, viewPort.y + viewPort.height - (28 + twoLineHeight), viewPort.width, 28 + twoLineHeight)
SetDrawColor(0.85, 0.85, 0.85)
DrawImage(nil, viewPort.x, viewPort.y + viewPort.height - (32 + twoLineHeight), viewPort.width, 4)
if self.showConvert then
SetDrawColor(0.05, 0.05, 0.05)
DrawImage(nil, viewPort.x, viewPort.y + viewPort.height - (60 + twoLineHeight), viewPort.width, 28)
SetDrawColor(0.85, 0.85, 0.85)
DrawImage(nil, viewPort.x, viewPort.y + viewPort.height - (64 + twoLineHeight), viewPort.width, 4)
end
self:DrawControls(viewPort)
end
function TreeTabClass:Load(xml, dbFileName)
self.specList = { }
if xml.elem == "Spec" then
-- Import single spec from old build
self.specList[1] = new("PassiveSpec", self.build, self.build.targetVersionData.defaultTreeVersion)
self.specList[1]:Load(xml, dbFileName)
self.activeSpec = 1
self.build.spec = self.specList[1]
return
end
for _, node in pairs(xml) do
if type(node) == "table" then
if node.elem == "Spec" then
if node.attrib.treeVersion and not treeVersions[node.attrib.treeVersion] then
main:OpenMessagePopup("Unknown Passive Tree Version", "The build you are trying to load uses an unrecognised version of the passive skill tree.\nYou may need to update the program before loading this build.")
return true
end
local newSpec = new("PassiveSpec", self.build, node.attrib.treeVersion or self.build.targetVersionData.defaultTreeVersion)
newSpec:Load(node, dbFileName)
t_insert(self.specList, newSpec)
end
end
end
if not self.specList[1] then
self.specList[1] = new("PassiveSpec", self.build, self.build.targetVersionData.latestTreeVersion)
end
self:SetActiveSpec(tonumber(xml.attrib.activeSpec) or 1)
end
function TreeTabClass:PostLoad()
for _, spec in ipairs(self.specList) do
spec:PostLoad()
end
end
function TreeTabClass:Save(xml)
xml.attrib = {
activeSpec = tostring(self.activeSpec)
}
for specId, spec in ipairs(self.specList) do
if specId == self.activeSpec then
-- Update this spec's jewels from the socket slots
for _, slot in pairs(self.build.itemsTab.slots) do
if slot.nodeId then
spec.jewels[slot.nodeId] = slot.selItemId
end
end
end
local child = {
elem = "Spec"
}
spec:Save(child)
t_insert(xml, child)
end
self.modFlag = false
end
function TreeTabClass:SetActiveSpec(specId)
local prevSpec = self.build.spec
self.activeSpec = m_min(specId, #self.specList)
local curSpec = self.specList[self.activeSpec]
self.build.spec = curSpec
self.build.buildFlag = true
self.build.spec:SetWindowTitleWithBuildClass()
for _, slot in pairs(self.build.itemsTab.slots) do
if slot.nodeId then
if prevSpec then
-- Update the previous spec's jewel for this slot
prevSpec.jewels[slot.nodeId] = slot.selItemId
end
if curSpec.jewels[slot.nodeId] then
-- Socket the jewel for the new spec
slot.selItemId = curSpec.jewels[slot.nodeId]
end
end
end
self.showConvert = curSpec.treeVersion ~= self.build.targetVersionData.latestTreeVersion
if self.build.itemsTab.itemOrderList[1] then
-- Update item slots if items have been loaded already
self.build.itemsTab:PopulateSlots()
end
end
function TreeTabClass:SetPowerCalc(selection)
self.viewer.showHeatMap = true
self.build.buildFlag = true
self.build.powerBuildFlag = true
self.build.calcsTab.powerStat = selection
self.build.calcsTab:BuildPower()
end
function TreeTabClass:OpenSpecManagePopup()
main:OpenPopup(370, 290, "Manage Passive Trees", {
new("PassiveSpecListControl", nil, 0, 50, 350, 200, self),
new("ButtonControl", nil, 0, 260, 90, 20, "Done", function()
main:ClosePopup()
end),
})
end
function TreeTabClass:OpenImportPopup()
local controls = { }
local function decodeTreeLink(treeLink)
local errMsg = self.build.spec:DecodeURL(treeLink)
if errMsg then
controls.msg.label = "^1"..errMsg
else
self.build.spec:AddUndoState()
self.build.buildFlag = true
main:ClosePopup()
end
end
controls.editLabel = new("LabelControl", nil, 0, 20, 0, 16, "Enter passive tree link:")
controls.edit = new("EditControl", nil, 0, 40, 350, 18, "", nil, nil, nil, function(buf)
controls.msg.label = ""
end)
controls.msg = new("LabelControl", nil, 0, 58, 0, 16, "")
controls.import = new("ButtonControl", nil, -45, 80, 80, 20, "Import", function()
local treeLink = controls.edit.buf
if #treeLink == 0 then
return
end
if treeLink:match("poeurl%.com/") then
controls.import.enabled = false
controls.msg.label = "Resolving PoEURL link..."
local id = LaunchSubScript([[
local treeLink = ...
local curl = require("lcurl.safe")
local easy = curl.easy()
easy:setopt_url(treeLink)
easy:setopt_writefunction(function(data)
return true
end)
easy:perform()
local redirect = easy:getinfo(curl.INFO_REDIRECT_URL)
easy:close()
if not redirect or redirect:match("poeurl%.com/") then
return nil, "Failed to resolve PoEURL link"
end
return redirect
]], "", "", treeLink)
if id then
launch:RegisterSubScript(id, function(treeLink, errMsg)
if errMsg then
controls.msg.label = "^1"..errMsg
controls.import.enabled = true
else
decodeTreeLink(treeLink)
end
end)
end
else
decodeTreeLink(treeLink)
end
end)
controls.cancel = new("ButtonControl", nil, 45, 80, 80, 20, "Cancel", function()
main:ClosePopup()
end)
main:OpenPopup(380, 110, "Import Tree", controls, "import", "edit")
end
function TreeTabClass:OpenExportPopup()
local treeLink = self.build.spec:EncodeURL(treeVersions[self.build.spec.treeVersion].export)
local popup
local controls = { }
controls.label = new("LabelControl", nil, 0, 20, 0, 16, "Passive tree link:")
controls.edit = new("EditControl", nil, 0, 40, 350, 18, treeLink, nil, "%Z")
controls.shrink = new("ButtonControl", nil, -90, 70, 140, 20, "Shrink with PoEURL", function()
controls.shrink.enabled = false
controls.shrink.label = "Shrinking..."
launch:DownloadPage("http://poeurl.com/shrink.php?url="..treeLink, function(page, errMsg)
controls.shrink.label = "Done"
if errMsg or not page:match("%S") then
main:OpenMessagePopup("PoEURL Shortener", "Failed to get PoEURL link. Try again later.")
else
treeLink = "http://poeurl.com/"..page
controls.edit:SetText(treeLink)
popup:SelectControl(controls.edit)
end
end)
end)
controls.copy = new("ButtonControl", nil, 30, 70, 80, 20, "Copy", function()
Copy(treeLink)
end)
controls.done = new("ButtonControl", nil, 120, 70, 80, 20, "Done", function()
main:ClosePopup()
end)
popup = main:OpenPopup(380, 100, "Export Tree", controls, "done", "edit")
end
function TreeTabClass:ModifyNodePopup(selectedNode)
local controls = { }
local modGroups = { }
local smallAdditions = {"Strength", "Dex", "Devotion"}
if not self.build.latestTree.legion.editedNodes then
self.build.latestTree.legion.editedNodes = { }
end
local function buildMods(selectedNode)
wipeTable(modGroups)
for _, node in pairs(self.build.latestTree.legion.nodes) do
if node.id:match("^"..selectedNode.conqueredBy.conqueror.type.."_.+") and node["not"] == (selectedNode.isNotable or false) and not node.ks then
t_insert(modGroups, {
label = node.dn,
descriptions = copyTable(node.sd),
type = selectedNode.conqueredBy.conqueror.type,
id = node.id,
})
end
end
for _, addition in pairs(self.build.latestTree.legion.additions) do
-- exclude passives that are already added (vaal, attributes, devotion)
if addition.id:match("^"..selectedNode.conqueredBy.conqueror.type.."_.+") and not isValueInArray(smallAdditions, addition.dn) and selectedNode.conqueredBy.conqueror.type ~= "vaal" then
t_insert(modGroups, {
label = addition.dn,
descriptions = copyTable(addition.sd),
type = selectedNode.conqueredBy.conqueror.type,
id = addition.id,
})
end
end
end
local function addModifier(selectedNode)
local newLegionNode = self.build.latestTree.legion.nodes[modGroups[controls.modSelect.selIndex].id]
-- most nodes only replace or add 1 mod, so we need to just get the first control
local modDesc = string.gsub(controls[1].label, "%^7", "")
if selectedNode.conqueredBy.conqueror.type == "eternal" or selectedNode.conqueredBy.conqueror.type == "templar" then
self.specList[1]:NodeAdditionOrReplacementFromString(selectedNode, modDesc, true)
selectedNode.dn = newLegionNode.dn
selectedNode.sprites = newLegionNode.sprites
elseif selectedNode.conqueredBy.conqueror.type == "vaal" then
selectedNode.dn = newLegionNode.dn
selectedNode.sprites = newLegionNode.sprites
self.specList[1]:NodeAdditionOrReplacementFromString(selectedNode, modDesc, true)
-- Vaal is the exception
if controls[2] then
modDesc = string.gsub(controls[2].label, "%^7", "")
self.specList[1]:NodeAdditionOrReplacementFromString(selectedNode, modDesc, false)
end
else
-- Replace the node first before adding the new line so we don't get multiple lines
if self.build.latestTree.legion.editedNodes[selectedNode.conqueredBy.id] and self.build.latestTree.legion.editedNodes[selectedNode.conqueredBy.id][selectedNode.id] then
self.specList[1]:ReplaceNode(selectedNode, self.build.latestTree.nodes[selectedNode.id])
end
self.specList[1]:NodeAdditionOrReplacementFromString(selectedNode, modDesc, false)
end
if not self.build.latestTree.legion.editedNodes[selectedNode.conqueredBy.id] then
t_insert(self.build.latestTree.legion.editedNodes, selectedNode.conqueredBy.id, {})
end
t_insert(self.build.latestTree.legion.editedNodes[selectedNode.conqueredBy.id], selectedNode.id, copyTable(selectedNode, true))
end
local function constructUI(modGroup)
local totalHeight = 43
local i = 1
while controls[i] or controls["slider"..i] do
controls[i] = nil
controls["slider"..i] = nil
i = i + 1
end
for idx, desc in ipairs(modGroup.descriptions) do
controls[idx] = new("LabelControl", {"TOPLEFT", controls["slider"..idx-1] or controls[idx-1] or controls.modSelect,"TOPLEFT"}, 0, 20, 600, 16, "^7"..desc)
totalHeight = totalHeight + 20
if desc:match("%(%-?[%d%.]+%-[%d%.]+%)") then
controls["slider"..idx] = new("SliderControl", {"TOPLEFT",controls[idx],"BOTTOMLEFT"}, 0, 2, 300, 16, function(val)
controls[idx].label = itemLib.applyRange(modGroup.descriptions[idx], val)
end)
controls["slider"..idx]:SetVal(.5)
controls["slider"..idx].width = function()
return controls["slider"..idx].divCount and 300 or 100
end
totalHeight = totalHeight + 20
end
end
main.popups[1].height = totalHeight + 30
controls.save.y = totalHeight
controls.reset.y = totalHeight
controls.close.y = totalHeight
end
buildMods(selectedNode)
controls.modSelectLabel = new("LabelControl", {"TOPRIGHT",nil,"TOPLEFT"}, 150, 25, 0, 16, "^7Modifier:")
controls.modSelect = new("DropDownControl", {"TOPLEFT",nil,"TOPLEFT"}, 155, 25, 579, 18, modGroups, function(idx) constructUI(modGroups[idx]) end)
controls.modSelect.tooltipFunc = function(tooltip, mode, index, value)
tooltip:Clear()
if mode ~= "OUT" and value then
for _, line in ipairs(value.descriptions) do
tooltip:AddLine(16, "^7"..line)
end
end
end
controls.save = new("ButtonControl", nil, -90, 75, 80, 20, "Add", function()
addModifier(selectedNode)
main:ClosePopup()
end)
controls.reset = new("ButtonControl", nil, 0, 75, 80, 20, "Reset Node", function()
if self.build.latestTree.legion.editedNodes[selectedNode.conqueredBy.id] then
self.build.latestTree.legion.editedNodes[selectedNode.conqueredBy.id][selectedNode.id] = nil
end
if selectedNode.conqueredBy.conqueror.type == "vaal" and selectedNode.type == "Normal" then
local legionNode = self.build.latestTree.legion.nodes["vaal_small_fire_resistance"]
selectedNode.dn = "Vaal small node"
selectedNode.sd = {"Right click to set mod"}
selectedNode.sprites = legionNode.sprites
selectedNode.mods = {""}
selectedNode.modList = new("ModList")
selectedNode.modKey = ""
elseif selectedNode.conqueredBy.conqueror.type == "vaal" and selectedNode.type == "Notable" then
local legionNode = self.build.latestTree.legion.nodes["vaal_notable_curse_1"]
selectedNode.dn = "Vaal notable node"
selectedNode.sd = {"Right click to set mod"}
selectedNode.sprites = legionNode.sprites
selectedNode.mods = {""}
selectedNode.modList = new("ModList")
selectedNode.modKey = ""
else
self.specList[1]:ReplaceNode(selectedNode, self.build.latestTree.nodes[selectedNode.id])
if selectedNode.conqueredBy.conqueror.type == "templar" then
self.specList[1]:NodeAdditionOrReplacementFromString(selectedNode,"+5 to Devotion")
end
end
main:ClosePopup()
end)
controls.close = new("ButtonControl", nil, 90, 75, 80, 20, "Cancel", function()
main:ClosePopup()
end)
main:OpenPopup(800, 105, "Replace Modifier of Node", controls, "save")
constructUI(modGroups[1])
end
function TreeTabClass:ShowPowerReport()
local report = {}
local currentStat = self.build.calcsTab.powerStat
-- the report doesn't support listing the "offense/defense" hybrid heatmap, as it is not a single scalar and im unsure how to quantify numerically
-- especially given the heatmap's current approach of using the sqrt() of both components. that number is cryptic to users, i suspect.
if not currentStat or not currentStat.stat then
main:OpenMessagePopup("Select a specific stat", "This feature does not report for the \"Offense/Defense\" heat map. Select a specific stat from the dropdown.")
return
end
-- locate formatting information for the type of heat map being used.
-- maybe a better place to find this? At the moment, it is the only place
-- in the code that has this information in a tidy place.
local currentStatLabel = currentStat.label
local displayStat = nil
for index, ds in ipairs(self.build.displayStats) do
if ds.stat == currentStat.stat then
displayStat = ds
break
end
end
-- not every heat map has an associated "stat" in the displayStats table
-- this is due to not every stat being displayed in the sidebar, I believe.
-- But, we do want to use the formatting knowledge stored in that table rather than duplicating it here.
-- If no corresponding stat is found, just default to a generic stat display (>0=good, one digit of precision).
if not displayStat then
displayStat = {
fmt = ".1f"
}
end
-- search all nodes, ignoring ascendcies, sockets, etc.
for nodeId, node in pairs(self.build.spec.nodes) do
local isAlloc = node.alloc or self.build.calcsTab.mainEnv.grantedPassives[nodeId]
if not isAlloc and (node.type == "Normal" or node.type == "Keystone" or node.type == "Notable") and not node.ascendancyName then
local nodePower = (node.power.singleStat or 0) * ((displayStat.pc or displayStat.mod) and 100 or 1)
local nodePowerStr = s_format("%"..displayStat.fmt, nodePower)
if main.showThousandsCalcs then
nodePowerStr = formatNumSep(nodePowerStr)
end
if (nodePower > 0 and not displayStat.lowerIsBetter) or (nodePower < 0 and displayStat.lowerIsBetter) then
nodePowerStr = colorCodes.POSITIVE .. nodePowerStr
elseif (nodePower < 0 and not displayStat.lowerIsBetter) or (nodePower > 0 and displayStat.lowerIsBetter) then
nodePowerStr = colorCodes.NEGATIVE .. nodePowerStr
end
t_insert(report, {
name = node.dn,
power = nodePower,
powerStr = nodePowerStr,
id = nodeId,
x = node.x,
y = node.y,
type = node.type,
pathDist = node.pathDist
})
end
end
-- search all cluster notables and add to the list
for nodeName, node in pairs(self.build.spec.tree.clusterNodeMap) do
local isAlloc = node.alloc
if not isAlloc then
local calcFunc, calcBase = self.build.calcsTab:GetMiscCalculator()
local cache = { }
local newPowerMax = {
singleStat = 0,
singleStatPerPoint = 0,
offence = 0,
offencePerPoint = 0,
defence = 0,
defencePerPoint = 0
}
if not node.power then
node.power = { }
if node.modKey ~= "" and not self.build.calcsTab.mainEnv.grantedPassives[nodeId] then
if not cache[node.modKey] then
cache[node.modKey] = calcFunc({ addNodes = { [node] = true } })
end
local output = cache[node.modKey]
if self.build.calcsTab.powerStat and self.build.calcsTab.powerStat.stat and not self.build.calcsTab.powerStat.ignoreForNodes then
node.power.singleStat = self.build.calcsTab:CalculatePowerStat(self.build.calcsTab.powerStat, output, calcBase)
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
end
end
end
local nodePower = (node.power.singleStat or 0) * ((displayStat.pc or displayStat.mod) and 100 or 1)
local nodePowerStr = s_format("%"..displayStat.fmt, nodePower)
if main.showThousandsCalcs then
nodePowerStr = formatNumSep(nodePowerStr)
end
if (nodePower > 0 and not displayStat.lowerIsBetter) or (nodePower < 0 and displayStat.lowerIsBetter) then
nodePowerStr = colorCodes.POSITIVE .. nodePowerStr
elseif (nodePower < 0 and not displayStat.lowerIsBetter) or (nodePower > 0 and displayStat.lowerIsBetter) then
nodePowerStr = colorCodes.NEGATIVE .. nodePowerStr
end
t_insert(report, {
name = node.dn,
power = nodePower,
powerStr = nodePowerStr,
id = node.id,
type = node.type,
pathDist = "Cluster"
})
end
end
-- sort it
if displayStat.lowerIsBetter then
t_sort(report, function (a,b)
return (a.power) < (b.power)
end)
else
t_sort(report, function (a,b)
return (a.power) > (b.power)
end)
end
-- present the UI
local controls = {}
controls.list = new("PowerReportListControl", nil, 0, 40, 550, 400, report, currentStatLabel, function(selectedNode)
-- this code is called by the list control when the user "selects" one of the passives in the list.
-- we use this to set a flag which causes the next Draw() to recenter the passive tree on the desired node.
if(selectedNode.x) then
self.jumpToNode = true
self.jumpToX = selectedNode.x
self.jumpToY = selectedNode.y
main:ClosePopup()
end
end)
controls.done = new("ButtonControl", nil, 0, 450, 100, 20, "Close", function()
main:ClosePopup()
end)
popup = main:OpenPopup(600, 500, "Power Report: " .. currentStatLabel, controls, "done", "list")
end