2436 lines
104 KiB
Lua
2436 lines
104 KiB
Lua
-- Path of Building
|
|
--
|
|
-- Module: Tree Tab
|
|
-- Passive skill tree tab for the current build.
|
|
--
|
|
local ipairs = ipairs
|
|
local pairs = pairs
|
|
local next = next
|
|
local t_insert = table.insert
|
|
local t_remove = table.remove
|
|
local t_sort = table.sort
|
|
local t_concat = table.concat
|
|
local m_max = math.max
|
|
local m_min = math.min
|
|
local m_floor = math.floor
|
|
local m_abs = math.abs
|
|
local s_format = string.format
|
|
local s_gsub = string.gsub
|
|
local s_byte = string.byte
|
|
local dkjson = require "dkjson"
|
|
|
|
local TreeTabClass = newClass("TreeTab", "ControlHost", function(self, build)
|
|
self.ControlHost()
|
|
|
|
self.build = build
|
|
self.isComparing = false;
|
|
self.isCustomMaxDepth = false;
|
|
|
|
self.viewer = new("PassiveTreeView")
|
|
|
|
self.specList = { }
|
|
self.specList[1] = new("PassiveSpec", build, latestTreeVersion)
|
|
self:SetActiveSpec(1)
|
|
self:SetCompareSpec(1)
|
|
|
|
self.anchorControls = new("Control", nil, {0, 0, 0, 20})
|
|
|
|
-- Tree list dropdown
|
|
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.maxDroppedWidth = 1000
|
|
self.controls.specSelect.enableDroppedWidth = true
|
|
self.controls.specSelect.enableChangeBoxWidth = true
|
|
self.controls.specSelect.controls.scrollBar.enabled = true
|
|
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, secondaryAscUsed, 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
|
|
-- Assumption: Nodes >= 65536 are small cluster passives.
|
|
if node.type ~= "ClassStart" and node.type ~= "AscendClassStart"
|
|
and (self.build.spec.tree.clusterNodeMap[node.dn] == nil or node.isKeystone or node.isJewelSocket) and nodeId < 65536
|
|
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].display)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Compare checkbox
|
|
self.controls.compareCheck = new("CheckBoxControl", { "LEFT", self.controls.specSelect, "RIGHT" }, { 74, 0, 20 }, "Compare:", function(state)
|
|
self.isComparing = state
|
|
self:SetCompareSpec(self.activeCompareSpec)
|
|
self.controls.compareSelect.shown = state
|
|
if state then
|
|
self.controls.reset:SetAnchor("LEFT", self.controls.compareSelect, "RIGHT", nil, nil, nil)
|
|
else
|
|
self.controls.reset:SetAnchor("LEFT", self.controls.compareCheck, "RIGHT", nil, nil, nil)
|
|
end
|
|
end)
|
|
|
|
-- Compare tree dropdown
|
|
self.controls.compareSelect = new("DropDownControl", { "LEFT", self.controls.compareCheck, "RIGHT" }, { 8, 0, 190, 20 }, nil, function(index, value)
|
|
if self.specList[index] then
|
|
self:SetCompareSpec(index)
|
|
else
|
|
self:OpenSpecManagePopup()
|
|
end
|
|
end)
|
|
self.controls.compareSelect.shown = false
|
|
self.controls.compareSelect.maxDroppedWidth = 1000
|
|
self.controls.compareSelect.enableDroppedWidth = true
|
|
self.controls.compareSelect.enableChangeBoxWidth = true
|
|
self.controls.reset = new("ButtonControl", { "LEFT", self.controls.compareCheck, "RIGHT" }, { 8, 0, 145, 20 }, "Reset Tree/Tattoos", function()
|
|
local controls = { }
|
|
local buttonY = 65
|
|
controls.warningLabel = new("LabelControl", nil, { 0, 30, 0, 16 }, "^7Warning: resetting your passive tree or removing all tattoos cannot be undone.\n")
|
|
controls.reset = new("ButtonControl", nil, { -130, buttonY, 100, 20 }, "Reset Tree", function()
|
|
self.build.spec:ResetNodes()
|
|
self.build.spec:BuildAllDependsAndPaths()
|
|
self.build.spec:AddUndoState()
|
|
self.build.buildFlag = true
|
|
main:ClosePopup()
|
|
end)
|
|
controls.removeTattoo = new("ButtonControl", nil, { 0, buttonY, 144, 20 }, "Remove All Tattoos", function()
|
|
for id, node in pairs(self.build.spec.hashOverrides) do --hashOverrides will contain only the nodes that have been tattoo-ed
|
|
if node.isTattoo then
|
|
self:RemoveTattooFromNode(self.build.spec.nodes[id])
|
|
end
|
|
end
|
|
self.modFlag = true
|
|
self.build.buildFlag = true
|
|
main:ClosePopup()
|
|
end)
|
|
controls.cancel = new("ButtonControl", nil, { 130, buttonY, 100, 20 }, "Cancel", function()
|
|
main:ClosePopup()
|
|
end)
|
|
main:OpenPopup(570, 100, "Reset Tree/Tattoos", controls, nil, "edit", "cancel")
|
|
end)
|
|
|
|
-- Tree Version Dropdown
|
|
self.treeVersions = { }
|
|
for _, num in ipairs(treeVersionList) do
|
|
local value = {
|
|
label = treeVersions[num].display,
|
|
value = num
|
|
}
|
|
t_insert(self.treeVersions, value)
|
|
end
|
|
self.controls.versionText = new("LabelControl", { "LEFT", self.controls.reset, "RIGHT" }, { 8, 0, 0, 16 }, "Version:")
|
|
self.controls.versionSelect = new("DropDownControl", { "LEFT", self.controls.versionText, "RIGHT" }, { 8, 0, 100, 20 }, self.treeVersions, function(index, selected)
|
|
if selected.value ~= self.build.spec.treeVersion then
|
|
self:OpenVersionConvertPopup(selected.value, true)
|
|
end
|
|
end)
|
|
self.controls.versionSelect.maxDroppedWidth = 1000
|
|
self.controls.versionSelect.enableDroppedWidth = true
|
|
self.controls.versionSelect.enableChangeBoxWidth = true
|
|
self.controls.versionSelect:CheckDroppedWidth(true)
|
|
self.controls.versionSelect.selIndex = #self.treeVersions
|
|
|
|
-- Tree Search Textbox
|
|
self.controls.treeSearch = new("EditControl", { "LEFT", self.controls.versionSelect, "RIGHT" }, { 8, 0, main.portraitMode and 200 or 300, 20 }, "", "Search", "%c", 100, function(buf)
|
|
self.viewer.searchStr = buf
|
|
self.searchFlag = buf ~= self.viewer.searchStrSaved
|
|
end, nil, nil, true)
|
|
self.controls.treeSearch.tooltipText = "Uses Lua pattern matching for complex searches\nPrefix your search with \"oil:\" to search by anoint recipe"
|
|
|
|
self.tradeLeaguesList = { }
|
|
-- Find Timeless Jewel Button
|
|
self.controls.findTimelessJewel = new("ButtonControl", { "LEFT", self.controls.treeSearch, "RIGHT" }, { 8, 0, 150, 20 }, "Find Timeless Jewel", function()
|
|
self:FindTimelessJewel()
|
|
end)
|
|
|
|
--Default index for Tattoos
|
|
self.defaultTattoo = { }
|
|
|
|
-- Show Node Power Checkbox
|
|
self.controls.treeHeatMap = new("CheckBoxControl", { "LEFT", self.controls.findTimelessJewel, "RIGHT" }, { 130, 0, 20 }, "Show Node Power:", function(state)
|
|
self.viewer.showHeatMap = state
|
|
self.controls.treeHeatMapStatSelect.shown = state
|
|
|
|
if state == false then
|
|
self.controls.powerReportList.shown = false
|
|
end
|
|
end)
|
|
|
|
-- Control for setting max node depth to limit calculation time of the heat map
|
|
self.controls.nodePowerMaxDepthSelect = new("DropDownControl", { "LEFT", self.controls.treeHeatMap, "RIGHT" }, { 8, 0, 55, 20 }, { "All", 5, 10, 15, "Custom" }, function(index, value)
|
|
-- Show custom value control and resize/move elements
|
|
self.isCustomMaxDepth = value == "Custom"
|
|
if self.isCustomMaxDepth then
|
|
self.controls.nodePowerMaxDepthSelect.width = 70
|
|
self.controls.nodePowerMaxDepthCustom.shown = true
|
|
self.controls.treeHeatMapStatSelect:SetAnchor("LEFT", self.controls.nodePowerMaxDepthCustom, "RIGHT", nil, nil, nil)
|
|
return
|
|
end
|
|
|
|
self.controls.nodePowerMaxDepthSelect.width = 55
|
|
self.controls.nodePowerMaxDepthCustom.shown = false
|
|
self.controls.treeHeatMapStatSelect:SetAnchor("LEFT", self.controls.nodePowerMaxDepthSelect, "RIGHT", nil, nil, nil)
|
|
|
|
local oldMax = self.build.calcsTab.nodePowerMaxDepth
|
|
|
|
if type(value) == "number" then
|
|
self.build.calcsTab.nodePowerMaxDepth = value
|
|
else
|
|
self.build.calcsTab.nodePowerMaxDepth = nil
|
|
end
|
|
|
|
-- If the heat map is shown, tell it to recalculate
|
|
-- if the new value is larger than the old
|
|
if oldMax ~= value and self.viewer.showHeatMap then
|
|
if oldMax ~= nil and (self.build.calcsTab.nodePowerMaxDepth == nil or self.build.calcsTab.nodePowerMaxDepth > oldMax) then
|
|
self:SetPowerCalc(self.build.calcsTab.powerStat)
|
|
end
|
|
end
|
|
end)
|
|
self.controls.nodePowerMaxDepthSelect.tooltipText = "Limit of Node distance to search (lower = faster)"
|
|
|
|
-- Control for setting max node depth by custom value
|
|
self.controls.nodePowerMaxDepthCustom = new("EditControl", { "LEFT", self.controls.nodePowerMaxDepthSelect, "RIGHT" }, { 8, 0, 70, 20 }, "0", nil, "%D", nil, function(value)
|
|
self.build.calcsTab.nodePowerMaxDepth = tonumber(value)
|
|
|
|
-- If the heat map is shown, recalculate it with new value
|
|
if self.viewer.showHeatMap then
|
|
self:SetPowerCalc(self.build.calcsTab.powerStat)
|
|
end
|
|
end)
|
|
self.controls.nodePowerMaxDepthCustom.shown = false
|
|
|
|
-- Control for selecting the power stat to sort by (Defense, DPS, etc)
|
|
self.controls.treeHeatMapStatSelect = new("DropDownControl", { "LEFT", self.controls.nodePowerMaxDepthSelect, "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
|
|
|
|
-- Show/Hide Power Report Button
|
|
self.controls.powerReport = new("ButtonControl", { "LEFT", self.controls.treeHeatMapStatSelect, "RIGHT" }, { 8, 0, 150, 20 },
|
|
function() return self.controls.powerReportList.shown and "Hide Power Report" or "Show Power Report" end, function()
|
|
self.controls.powerReportList.shown = not self.controls.powerReportList.shown
|
|
end)
|
|
|
|
-- Power Report List
|
|
local yPos = self.controls.treeHeatMap.y == 0 and self.controls.specSelect.height + 4 or self.controls.specSelect.height * 2 + 8
|
|
self.controls.powerReportList = new("PowerReportListControl", { "TOPLEFT", self.controls.specSelect, "BOTTOMLEFT" }, { 0, yPos, 700, 170 }, 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
|
|
end
|
|
end)
|
|
self.controls.powerReportList.shown = false
|
|
self.build.powerBuilderCallback = function()
|
|
local powerStat = self.build.calcsTab.powerStat or data.powerStatList[1]
|
|
local report = self:BuildPowerReportList(powerStat)
|
|
self.controls.powerReportList:SetReport(powerStat, report)
|
|
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
|
|
local function getLatestTreeVersion()
|
|
return latestTreeVersion .. (self.specList[self.activeSpec].treeVersion:match("^" .. latestTreeVersion .. "(.*)") or "")
|
|
end
|
|
local function buildConvertButtonLabel()
|
|
return colorCodes.POSITIVE.."Convert to "..treeVersions[getLatestTreeVersion()].display
|
|
end
|
|
local function buildConvertAllButtonLabel()
|
|
return colorCodes.POSITIVE.."Convert all trees to "..treeVersions[getLatestTreeVersion()].display
|
|
end
|
|
self.controls.specConvert = new("ButtonControl", { "LEFT", self.controls.specConvertText, "RIGHT" }, { 8, 0, function() return DrawStringWidth(16, "VAR", buildConvertButtonLabel()) + 20 end, 20 }, buildConvertButtonLabel, function()
|
|
self:ConvertToVersion(getLatestTreeVersion(), false, true)
|
|
end)
|
|
self.controls.specConvertAll = new("ButtonControl", { "LEFT", self.controls.specConvert, "RIGHT" }, { 8, 0, function() return DrawStringWidth(16, "VAR", buildConvertAllButtonLabel()) + 20 end, 20 }, buildConvertAllButtonLabel, function()
|
|
self:OpenVersionConvertAllPopup(getLatestTreeVersion())
|
|
end)
|
|
self.jumpToNode = false
|
|
self.jumpToX = 0
|
|
self.jumpToY = 0
|
|
end)
|
|
|
|
function TreeTabClass:RemoveTattooFromNode(node)
|
|
self.build.spec.tree.nodes[node.id].isTattoo = false
|
|
self.build.spec.hashOverrides[node.id] = nil
|
|
self.build.spec:ReplaceNode(node, self.build.spec.tree.nodes[node.id])
|
|
self.build.spec:BuildAllDependsAndPaths()
|
|
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 == "UP" then
|
|
local index = self.activeSpec - 1
|
|
if self.specList[index] and not self.controls.specSelect:IsMouseOver() and not self.controls.specSelect.dropped then
|
|
self.build.modFlag = true
|
|
self:SetActiveSpec(index)
|
|
end
|
|
elseif event.key == "DOWN" and not self.controls.specSelect:IsMouseOver() and not self.controls.specSelect.dropped then
|
|
local index = self.activeSpec + 1
|
|
if self.specList[index] then
|
|
self.build.modFlag = true
|
|
self:SetActiveSpec(index)
|
|
end
|
|
elseif event.key == "f" and IsKeyDown("CTRL") then
|
|
self:SelectControl(self.controls.treeSearch)
|
|
elseif event.key == "m" and IsKeyDown("CTRL") then
|
|
self:OpenSpecManagePopup()
|
|
end
|
|
end
|
|
end
|
|
self:ProcessControlsInput(inputEvents, viewPort)
|
|
|
|
-- Determine positions if one line of controls doesn't fit in the screen width
|
|
local linesHeight = 24
|
|
local rightMargin = 10
|
|
local widthFirstLineControls = self.controls.specSelect.width + 8
|
|
+ self.controls.compareCheck.width + self.controls.compareCheck.x
|
|
+ self.controls.reset.width + self.controls.reset.x
|
|
+ self.controls.versionText.width() + self.controls.versionText.x
|
|
+ self.controls.versionSelect.width + self.controls.versionSelect.x
|
|
+ (self.isComparing and (self.controls.compareSelect.width + self.controls.compareSelect.x) or 0)
|
|
|
|
local widthSecondLineControls = self.controls.treeSearch.width + 8
|
|
+ self.controls.findTimelessJewel.width + self.controls.findTimelessJewel.x
|
|
+ self.controls.treeHeatMap.width + 130
|
|
+ self.controls.nodePowerMaxDepthSelect.width + self.controls.nodePowerMaxDepthSelect.x
|
|
+ (self.isCustomMaxDepth and (self.controls.nodePowerMaxDepthCustom.width + self.controls.nodePowerMaxDepthCustom.x) or 0)
|
|
+ (self.viewer.showHeatMap and (self.controls.treeHeatMapStatSelect.width + self.controls.treeHeatMapStatSelect.x
|
|
+ self.controls.powerReport.width + self.controls.powerReport.x) or 0)
|
|
|
|
-- Check first line
|
|
if viewPort.width >= widthFirstLineControls + widthSecondLineControls + rightMargin then
|
|
linesHeight = 0
|
|
self.controls.treeSearch:SetAnchor("LEFT", self.controls.versionSelect, "RIGHT", 8, 0)
|
|
self.controls.powerReportList:SetAnchor("TOPLEFT", self.controls.specSelect, "BOTTOMLEFT", 0, self.controls.specSelect.height + 6)
|
|
else
|
|
self.controls.treeSearch:SetAnchor("TOPLEFT", self.controls.specSelect, "BOTTOMLEFT", 0, 4)
|
|
self.controls.powerReportList:SetAnchor("TOPLEFT", self.controls.treeSearch, "BOTTOMLEFT", 0, self.controls.treeSearch.height + 6)
|
|
end
|
|
|
|
-- Check second line
|
|
if viewPort.width >= widthSecondLineControls + rightMargin then
|
|
self.controls.treeHeatMap:SetAnchor("LEFT", self.controls.findTimelessJewel, "RIGHT", 130, 0)
|
|
else
|
|
linesHeight = linesHeight * 2
|
|
self.controls.treeHeatMap:SetAnchor("TOPLEFT", self.controls.treeSearch, "BOTTOMLEFT", 124, 4)
|
|
self.controls.powerReportList:SetAnchor("TOPLEFT", self.controls.treeHeatMap, "BOTTOMLEFT", -124, self.controls.treeHeatMap.height + 6)
|
|
end
|
|
|
|
-- determine positions for convert line of controls
|
|
local convertTwoLineHeight = 24
|
|
local convertMaxWidth = 900
|
|
if viewPort.width >= convertMaxWidth then
|
|
convertTwoLineHeight = 0
|
|
self.controls.specConvert:SetAnchor("LEFT", self.controls.specConvertText, "RIGHT", 8, 0)
|
|
self.controls.specConvertText:SetAnchor("BOTTOMLEFT", self.controls.specSelect, "TOPLEFT", 0, -14)
|
|
else
|
|
self.controls.specConvert:SetAnchor("TOPLEFT", self.controls.specConvertText, "BOTTOMLEFT", 0, 4)
|
|
self.controls.specConvertText:SetAnchor("BOTTOMLEFT", self.controls.specSelect, "TOPLEFT", 0, -38)
|
|
end
|
|
|
|
local bottomDrawerHeight = self.controls.powerReportList.shown and 194 or 0
|
|
self.controls.specSelect.y = -bottomDrawerHeight - linesHeight
|
|
|
|
local treeViewPort = { x = viewPort.x, y = viewPort.y, width = viewPort.width, height = viewPort.height - (self.showConvert and 64 + bottomDrawerHeight + linesHeight or 32 + bottomDrawerHeight + linesHeight)}
|
|
if self.jumpToNode then
|
|
self.viewer:Focus(self.jumpToX, self.jumpToY, treeViewPort, self.build)
|
|
self.jumpToNode = false
|
|
end
|
|
self.viewer.compareSpec = self.isComparing and self.specList[self.activeCompareSpec] or nil
|
|
self.viewer:Draw(self.build, treeViewPort, inputEvents)
|
|
|
|
local newSpecList = self:GetSpecList()
|
|
self.controls.compareSelect.selIndex = self.activeCompareSpec
|
|
self.controls.compareSelect:SetList(newSpecList)
|
|
t_insert(newSpecList, "Manage trees... (ctrl-m)")
|
|
self.controls.specSelect.selIndex = self.activeSpec
|
|
self.controls.specSelect:SetList(newSpecList)
|
|
|
|
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.shown = self.viewer.showHeatMap
|
|
self.controls.treeHeatMapStatSelect.list = self.powerStatList
|
|
self.controls.treeHeatMapStatSelect.selIndex = 1
|
|
self.controls.treeHeatMapStatSelect:CheckDroppedWidth(true)
|
|
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 + bottomDrawerHeight + linesHeight), viewPort.width, 28 + bottomDrawerHeight + linesHeight)
|
|
if self.showConvert then
|
|
local height = viewPort.width < convertMaxWidth and (bottomDrawerHeight + linesHeight) or 0
|
|
SetDrawColor(0.05, 0.05, 0.05)
|
|
DrawImage(nil, viewPort.x, viewPort.y + viewPort.height - (60 + bottomDrawerHeight + linesHeight + convertTwoLineHeight), viewPort.width, 28 + height)
|
|
SetDrawColor(0.85, 0.85, 0.85)
|
|
DrawImage(nil, viewPort.x, viewPort.y + viewPort.height - (64 + bottomDrawerHeight + linesHeight + convertTwoLineHeight), viewPort.width, 4)
|
|
end
|
|
-- let white lines overwrite the black sections, regardless of showConvert
|
|
SetDrawColor(0.85, 0.85, 0.85)
|
|
DrawImage(nil, viewPort.x, viewPort.y + viewPort.height - (32 + bottomDrawerHeight + linesHeight), viewPort.width, 4)
|
|
|
|
self:DrawControls(viewPort)
|
|
end
|
|
|
|
function TreeTabClass:GetSpecList()
|
|
local newSpecList = { }
|
|
for _, spec in ipairs(self.specList) do
|
|
t_insert(newSpecList, (spec.treeVersion ~= latestTreeVersion and ("["..treeVersions[spec.treeVersion].display.."] ") or "")..(spec.title or "Default"))
|
|
end
|
|
return newSpecList
|
|
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, 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 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, 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
|
|
local child = {
|
|
elem = "Spec"
|
|
}
|
|
spec:Save(child)
|
|
t_insert(xml, child)
|
|
end
|
|
end
|
|
|
|
function TreeTabClass:SetActiveSpec(specId)
|
|
local prevSpec = self.build.spec
|
|
self.activeSpec = m_min(specId, #self.specList)
|
|
local curSpec = self.specList[self.activeSpec]
|
|
data.setJewelRadiiGlobally(curSpec.treeVersion)
|
|
self.build.spec = curSpec
|
|
self.build.buildFlag = true
|
|
self.build.spec:SetWindowTitleWithBuildClass()
|
|
self.build:UpdateClassDropdowns(curSpec.treeVersion)
|
|
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]
|
|
else
|
|
-- Unsocket the old jewel from the previous spec
|
|
slot.selItemId = 0
|
|
end
|
|
end
|
|
end
|
|
self.showConvert = not curSpec.treeVersion:match("^" .. latestTreeVersion)
|
|
if self.build.itemsTab.itemOrderList[1] then
|
|
-- Update item slots if items have been loaded already
|
|
self.build.itemsTab:PopulateSlots()
|
|
end
|
|
-- Update the passive tree dropdown control in itemsTab
|
|
self.build.itemsTab.controls.specSelect.selIndex = specId
|
|
-- Update Version dropdown to active spec's
|
|
if self.controls.versionSelect then
|
|
self.controls.versionSelect:SelByValue(curSpec.treeVersion, 'value')
|
|
end
|
|
self.build:SyncLoadouts()
|
|
end
|
|
|
|
function TreeTabClass:SetCompareSpec(specId)
|
|
self.activeCompareSpec = m_min(specId, #self.specList)
|
|
local curSpec = self.specList[self.activeCompareSpec]
|
|
|
|
self.compareSpec = curSpec
|
|
end
|
|
|
|
function TreeTabClass:ConvertToVersion(version, remove, success, ignoreTreeSubType)
|
|
local treeSubTypeCapture = self.build.spec.treeVersion:match("(_%l+_?%l*)")
|
|
if not ignoreTreeSubType and treeSubTypeCapture and not version:match(treeSubTypeCapture) then
|
|
if isValueInTable(treeVersionList, version..treeSubTypeCapture) then
|
|
version = version..treeSubTypeCapture
|
|
end
|
|
end
|
|
local newSpec = new("PassiveSpec", self.build, version)
|
|
newSpec.title = self.build.spec.title
|
|
newSpec.jewels = copyTable(self.build.spec.jewels)
|
|
newSpec:RestoreUndoState(self.build.spec:CreateUndoState(), version)
|
|
newSpec:BuildClusterJewelGraphs()
|
|
t_insert(self.specList, self.activeSpec + 1, newSpec)
|
|
if remove then
|
|
t_remove(self.specList, self.activeSpec)
|
|
-- activeSpec + 1 is shifted down one on remove, otherwise we would set the spec below it if it exists
|
|
self:SetActiveSpec(self.activeSpec)
|
|
else
|
|
self:SetActiveSpec(self.activeSpec + 1)
|
|
end
|
|
self.modFlag = true
|
|
if success then
|
|
main:OpenMessagePopup("Tree Converted", "The tree has been converted to "..treeVersions[version].display..".\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
|
|
end
|
|
|
|
function TreeTabClass:ConvertAllToVersion(version)
|
|
local currActiveSpec = self.activeSpec
|
|
local specVersionList = { }
|
|
for _, spec in ipairs(self.specList) do
|
|
t_insert(specVersionList, spec.treeVersion)
|
|
end
|
|
for index, specVersion in ipairs(specVersionList) do
|
|
if specVersion ~= version then
|
|
self:SetActiveSpec(index)
|
|
self:ConvertToVersion(version, true, false)
|
|
end
|
|
end
|
|
self:SetActiveSpec(currActiveSpec)
|
|
end
|
|
|
|
function TreeTabClass:OpenSpecManagePopup()
|
|
local importTree =
|
|
new("ButtonControl", nil, {-99, 259, 90, 20}, "Import Tree", function()
|
|
self:OpenImportPopup()
|
|
end)
|
|
local exportTree =
|
|
new("ButtonControl", {"LEFT", importTree, "RIGHT"}, {8, 0, 90, 20}, "Export Tree", function()
|
|
self:OpenExportPopup()
|
|
end)
|
|
|
|
main:OpenPopup(370, 290, "Manage Passive Trees", {
|
|
new("PassiveSpecListControl", nil, {0, 50, 350, 200}, self),
|
|
importTree,
|
|
exportTree,
|
|
new("ButtonControl", {"LEFT", exportTree, "RIGHT"}, {8, 0, 90, 20}, "Done", function()
|
|
main:ClosePopup()
|
|
end),
|
|
})
|
|
end
|
|
|
|
function TreeTabClass:OpenVersionConvertPopup(version, ignoreTreeSubType)
|
|
local controls = { }
|
|
controls.warningLabel = new("LabelControl", nil, {0, 20, 0, 16}, "^7Warning: some or all of the passives may be de-allocated due to changes in the tree.\n\n" ..
|
|
"Convert will replace your current tree.\nCopy + Convert will backup your current tree.\n")
|
|
controls.convert = new("ButtonControl", nil, {-125, 105, 100, 20}, "Convert", function()
|
|
self:ConvertToVersion(version, true, false, ignoreTreeSubType)
|
|
main:ClosePopup()
|
|
end)
|
|
controls.convertCopy = new("ButtonControl", nil, {0, 105, 125, 20}, "Copy + Convert", function()
|
|
self:ConvertToVersion(version, false, false, ignoreTreeSubType)
|
|
main:ClosePopup()
|
|
end)
|
|
controls.cancel = new("ButtonControl", nil, {125, 105, 100, 20}, "Cancel", function()
|
|
self.controls.versionSelect:SelByValue(self.build.spec.treeVersion, 'value')
|
|
main:ClosePopup()
|
|
end)
|
|
main:OpenPopup(570, 140, "Convert to Version "..treeVersions[version].display, controls, "convert", "edit")
|
|
end
|
|
|
|
function TreeTabClass:OpenVersionConvertAllPopup(version)
|
|
local controls = { }
|
|
controls.warningLabel = new("LabelControl", nil, {0, 20, 0, 16}, "^7Warning: some or all of the passives may be de-allocated due to changes in the tree.\n\n" ..
|
|
"Convert will replace all trees that are not Version "..treeVersions[version].display..".\nThis action cannot be undone.\n")
|
|
controls.convert = new("ButtonControl", nil, {-58, 105, 100, 20}, "Convert", function()
|
|
self:ConvertAllToVersion(version)
|
|
main:ClosePopup()
|
|
end)
|
|
controls.cancel = new("ButtonControl", nil, {58, 105, 100, 20}, "Cancel", function()
|
|
main:ClosePopup()
|
|
end)
|
|
main:OpenPopup(570, 140, "Convert all to Version "..treeVersions[version].display, controls, "convert", "edit")
|
|
end
|
|
|
|
function TreeTabClass:OpenImportPopup()
|
|
local versionLookup = "tree/([0-9]+)%.([0-9]+)%.([0-9]+)/"
|
|
local controls = { }
|
|
local function decodePoePlannerTreeLink(treeLink)
|
|
-- treeVersion is not known at this point. We need to decode the URL to get it.
|
|
local tmpSpec = new("PassiveSpec", self.build, latestTreeVersion)
|
|
local newTreeVersion_or_errMsg = tmpSpec:DecodePoePlannerURL(treeLink, true)
|
|
-- Check for an error message
|
|
if string.find(newTreeVersion_or_errMsg, "Invalid") then
|
|
controls.msg.label = "^1"..newTreeVersion_or_errMsg
|
|
return
|
|
end
|
|
|
|
-- 20230908. We always create a new Spec()
|
|
local newSpec = new("PassiveSpec", self.build, newTreeVersion_or_errMsg)
|
|
newSpec.title = controls.name.buf
|
|
newSpec:DecodePoePlannerURL(treeLink, false) --DecodePoePlannerURL was used above and URL proven correct.
|
|
t_insert(self.specList, newSpec)
|
|
-- trigger all the things that go with changing a spec
|
|
self:SetActiveSpec(#self.specList)
|
|
self.modFlag = true
|
|
self.build.spec:AddUndoState()
|
|
self.build.buildFlag = true
|
|
main:ClosePopup()
|
|
end
|
|
|
|
local function decodeTreeLink(treeLink, newTreeVersion)
|
|
-- newTreeVersion is passed in as an output of validateTreeVersion(). It will always be a valid tree version text string
|
|
-- 20230908. We always create a new Spec()
|
|
ConPrintf("Tree version: " .. newTreeVersion)
|
|
local newSpec = new("PassiveSpec", self.build, newTreeVersion)
|
|
newSpec.title = controls.name.buf
|
|
local errMsg = newSpec:DecodeURL(treeLink)
|
|
if errMsg then
|
|
controls.msg.label = "^1"..errMsg.."^7"
|
|
else
|
|
t_insert(self.specList, newSpec)
|
|
-- trigger all the things that go with changing a spec
|
|
self:SetActiveSpec(#self.specList)
|
|
self.modFlag = true
|
|
self.build.spec:AddUndoState()
|
|
self.build.buildFlag = true
|
|
main:ClosePopup()
|
|
end
|
|
end
|
|
local function validateTreeVersion(alternateType, major, minor)
|
|
-- Take the Major and Minor version numbers and confirm it is a valid tree version. The point release is also passed in but it is not used
|
|
-- Return: the passed in tree version as text or latestTreeVersion
|
|
if major and minor then
|
|
--need leading 0 here
|
|
local newTreeVersionNum = tonumber(string.format("%d.%02d", major, minor))
|
|
if newTreeVersionNum >= treeVersions[defaultTreeVersion].num and newTreeVersionNum <= treeVersions[latestTreeVersion].num then
|
|
-- no leading 0 here
|
|
return string.format("%s_%s", major, minor) .. (alternateType and ("_" .. alternateType:gsub("-", "_")) or "")
|
|
else
|
|
print(string.format("Version '%d_%02d' is out of bounds", major, minor))
|
|
end
|
|
end
|
|
return latestTreeVersion .. (alternateType and ("_" .. alternateType:gsub("-", "_")) or "")
|
|
end
|
|
|
|
controls.nameLabel = new("LabelControl", nil, {-180, 20, 0, 16}, "Enter name for this passive tree:")
|
|
controls.name = new("EditControl", nil, {100, 20, 350, 18}, "", nil, nil, nil, function(buf)
|
|
controls.msg.label = ""
|
|
controls.import.enabled = buf:match("%S") and controls.edit.buf:match("%S")
|
|
end)
|
|
controls.editLabel = new("LabelControl", nil, {-150, 45, 0, 16}, "Enter passive tree link:")
|
|
controls.edit = new("EditControl", nil, {100, 45, 350, 18}, "", nil, nil, nil, function(buf)
|
|
controls.msg.label = ""
|
|
controls.import.enabled = buf:match("%S") and controls.name.buf:match("%S")
|
|
end)
|
|
controls.msg = new("LabelControl", nil, {0, 65, 0, 16}, "")
|
|
controls.import = new("ButtonControl", nil, {-45, 85, 80, 20}, "Import", function()
|
|
local treeLink = controls.edit.buf
|
|
if #treeLink == 0 then
|
|
return
|
|
end
|
|
-- EG: http://poeurl.com/dABz
|
|
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.."^7"
|
|
controls.import.enabled = true
|
|
return
|
|
else
|
|
decodeTreeLink(treeLink, validateTreeVersion(treeLink:match("tree/(%l+%-?%l*)"), treeLink:match(versionLookup)))
|
|
end
|
|
end)
|
|
end
|
|
elseif treeLink:match("poeplanner.com/") then
|
|
decodePoePlannerTreeLink(treeLink:gsub("/%?v=.+#","/"))
|
|
elseif treeLink:match("poeskilltree.com/") then
|
|
local oldStyleVersionLookup = "/%?v=([0-9]+)%.([0-9]+)%.([0-9]+)%-?%w?%-?%w?#"
|
|
-- Strip the version from the tree : https://poeskilltree.com/?v=3.6.0#AAAABAMAABEtfIOFMo6-ksHfsOvu -> https://poeskilltree.com/AAAABAMAABEtfIOFMo6-ksHfsOvu
|
|
decodeTreeLink(treeLink:gsub("/%?v=.+#","/"), validateTreeVersion(treeLink:match("%-(%l+%-?%l*)#"), treeLink:match(oldStyleVersionLookup)))
|
|
else
|
|
-- EG: https://www.pathofexile.com/passive-skill-tree/3.15.0/AAAABgMADI6-HwKSwQQHLJwtH9-wTLNfKoP3ES3r5AAA
|
|
-- EG: https://www.pathofexile.com/fullscreen-passive-skill-tree/3.15.0/AAAABgMADAQHES0fAiycLR9Ms18qg_eOvpLB37Dr5AAA
|
|
-- EG: https://www.pathofexile.com/passive-skill-tree/ruthless/AAAABgAAAAAA (Ruthless doesn't have versions)
|
|
-- EG: https://www.pathofexile.com/passive-skill-tree/ruthless-alternate/AAAABgAAAAAA
|
|
-- EG: https://www.pathofexile.com/passive-skill-tree/alternate/AAAABgAAAAAA
|
|
decodeTreeLink(treeLink, validateTreeVersion(treeLink:match("tree/(%l+%-?%l*)"), treeLink:match(versionLookup)))
|
|
end
|
|
end)
|
|
controls.import.enabled = false
|
|
controls.cancel = new("ButtonControl", nil, {45, 85, 80, 20}, "Cancel", function()
|
|
main:ClosePopup()
|
|
end)
|
|
main:OpenPopup(580, 115, "Import Tree", controls, "import", "name")
|
|
end
|
|
|
|
function TreeTabClass:OpenExportPopup()
|
|
local treeLink = self.build.spec:EncodeURL(treeVersions[self.build.spec.treeVersion].url)
|
|
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(response, errMsg)
|
|
controls.shrink.label = "Done"
|
|
if errMsg or not response.body:match("%S") then
|
|
main:OpenMessagePopup("PoEURL Shortener", "Failed to get PoEURL link. Try again later.")
|
|
else
|
|
treeLink = "http://poeurl.com/"..response.body
|
|
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 treeNodes = self.build.spec.tree.nodes
|
|
local nodeName = treeNodes[selectedNode.id].dn
|
|
local function buildMods(selectedNode)
|
|
wipeTable(modGroups)
|
|
local numLinkedNodes = selectedNode.linkedId and #selectedNode.linkedId or 0
|
|
local nodeValue = treeNodes[selectedNode.id].sd[1] or ""
|
|
for id, node in pairs(self.build.spec.tree.tattoo.nodes) do
|
|
if (nodeName:match(node.targetType:gsub("^Small ", "")) or (node.targetValue ~= "" and nodeValue:match(node.targetValue)) or
|
|
(node.targetType == "Small Attribute" and (nodeName == "Intelligence" or nodeName == "Strength" or nodeName == "Dexterity"))
|
|
or (node.targetType == "Keystone" and treeNodes[selectedNode.id].type == node.targetType))
|
|
and node.MinimumConnected <= numLinkedNodes and ((node.legacy == nil or node.legacy == false) or node.legacy == self.showLegacyTattoo) then
|
|
local combine = false
|
|
for id, desc in pairs(node.stats) do
|
|
combine = (id:match("^local_display.*") and #node.stats == (#node.sd - 1)) or combine
|
|
if combine then break end
|
|
end
|
|
local descriptionsAndReminders = copyTable(node.sd)
|
|
if combine then
|
|
t_remove(descriptionsAndReminders, 1)
|
|
t_remove(descriptionsAndReminders, 1)
|
|
t_insert(descriptionsAndReminders, 1, node.sd[1] .. " " .. node.sd[2])
|
|
end
|
|
local descriptionsAndReminders = combine and { [1] = table.concat(node.sd, " ") } or copyTable(node.sd)
|
|
if node.reminderText then
|
|
t_insert(descriptionsAndReminders, node.reminderText[1])
|
|
end
|
|
t_insert(modGroups, {
|
|
label = node.dn .. " " .. table.concat(node.sd, ","),
|
|
descriptions = descriptionsAndReminders,
|
|
id = id,
|
|
})
|
|
end
|
|
end
|
|
table.sort(modGroups, function(a, b) return a.label < b.label end)
|
|
end
|
|
local function addModifier(selectedNode)
|
|
local newTattooNode = self.build.spec.tree.tattoo.nodes[modGroups[controls.modSelect.selIndex].id]
|
|
newTattooNode.id = selectedNode.id
|
|
self.build.spec.hashOverrides[selectedNode.id] = newTattooNode
|
|
self.build.spec:ReplaceNode(selectedNode, newTattooNode)
|
|
self.build.spec:BuildAllDependsAndPaths()
|
|
end
|
|
|
|
local function constructUI(modGroup)
|
|
local totalHeight = 43
|
|
local maxWidth = 375
|
|
local i = 1
|
|
while controls[i] do
|
|
controls[i] = nil
|
|
i = i + 1
|
|
end
|
|
|
|
local wrapTable = {}
|
|
for idx, desc in ipairs(modGroup.descriptions) do
|
|
for _, wrappedDesc in ipairs(main:WrapString(desc, 16, maxWidth)) do
|
|
t_insert(wrapTable, wrappedDesc)
|
|
end
|
|
end
|
|
for idx, desc in ipairs(wrapTable) do
|
|
controls[idx] = new("LabelControl", {"TOPLEFT", controls[idx-1] or controls.modSelect,"TOPLEFT"}, {0, 20, 600, 16}, "^7"..desc)
|
|
totalHeight = totalHeight + 20
|
|
end
|
|
main.popups[1].height = totalHeight + 75
|
|
local buttonHeight = totalHeight + 15
|
|
controls.save.y = buttonHeight
|
|
controls.reset.y = buttonHeight
|
|
controls.close.y = buttonHeight
|
|
controls.totalTattoos.y = buttonHeight + 30
|
|
end
|
|
|
|
buildMods(selectedNode)
|
|
controls.modSelectLabel = new("LabelControl", {"TOPRIGHT",nil,"TOPLEFT"}, {150, 25, 0, 16}, "^7Modifier:")
|
|
controls.modSelect = new("DropDownControl", {"TOPLEFT",nil,"TOPLEFT"}, {155, 25, 250, 18}, modGroups, function(idx) constructUI(modGroups[idx]) end)
|
|
controls.modSelect.selIndex = self.defaultTattoo[nodeName] or 1
|
|
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)
|
|
self.modFlag = true
|
|
self.build.buildFlag = true
|
|
self.defaultTattoo[nodeName] = controls.modSelect.selIndex
|
|
main:ClosePopup()
|
|
end)
|
|
controls.reset = new("ButtonControl", nil, {0, 75, 80, 20}, "Reset Node", function()
|
|
self:RemoveTattooFromNode(selectedNode)
|
|
self.modFlag = true
|
|
self.build.buildFlag = true
|
|
self.defaultTattoo[nodeName] = nil
|
|
main:ClosePopup()
|
|
end)
|
|
controls.close = new("ButtonControl", nil, {90, 75, 80, 20}, "Cancel", function()
|
|
main:ClosePopup()
|
|
end)
|
|
|
|
local function getTattooCount()
|
|
local count = 0
|
|
for _, node in pairs(self.build.spec.hashOverrides) do
|
|
if node.isTattoo and not node.dn:find("Runegraft") then
|
|
count = count + 1
|
|
end
|
|
end
|
|
if count > 50 then
|
|
count = colorCodes.NEGATIVE..count
|
|
end
|
|
return count
|
|
end
|
|
controls.totalTattoos = new("LabelControl", nil, { 0, 95, 0, 16 }, "^7Tattoo Count: ".. getTattooCount() .."/50" )
|
|
main:OpenPopup(600, 105, "Replace Modifier of Node", controls, "save")
|
|
constructUI(modGroups[self.defaultTattoo[nodeName] or 1])
|
|
|
|
-- Show Legacy Tattoos
|
|
controls.showLegacyTattoo = new("CheckBoxControl", { "LEFT", controls.totalTattoos, "RIGHT" }, { 205, 0, 20 }, "Show Legacy Tattoos:", function(state)
|
|
self.showLegacyTattoo = state
|
|
buildMods(selectedNode)
|
|
end)
|
|
controls.showLegacyTattoo.state = self.showLegacyTattoo
|
|
end
|
|
|
|
function TreeTabClass:SaveMasteryPopup(node, listControl)
|
|
if listControl.selValue == nil then
|
|
return
|
|
end
|
|
local effect = self.build.spec.tree.masteryEffects[listControl.selValue.id]
|
|
node.sd = effect.sd
|
|
node.allMasteryOptions = false
|
|
node.reminderText = { "Tip: Right click to select a different effect" }
|
|
self.build.spec.tree:ProcessStats(node)
|
|
self.build.spec.masterySelections[node.id] = effect.id
|
|
if not node.alloc then
|
|
self.build.spec:AllocNode(node, self.viewer.tracePath and node == self.viewer.tracePath[#self.viewer.tracePath] and self.viewer.tracePath)
|
|
end
|
|
self.build.spec:AddUndoState()
|
|
self.modFlag = true
|
|
self.build.buildFlag = true
|
|
main:ClosePopup()
|
|
end
|
|
|
|
function TreeTabClass:OpenMasteryPopup(node, viewPort)
|
|
local controls = { }
|
|
local effects = { }
|
|
local cachedSd = node.sd
|
|
local cachedAllMasteryOption = node.allMasteryOptions
|
|
|
|
wipeTable(effects)
|
|
for _, effect in pairs(node.masteryEffects) do
|
|
local assignedNodeId = isValueInTable(self.build.spec.masterySelections, effect.effect)
|
|
if not assignedNodeId or assignedNodeId == node.id then
|
|
t_insert(effects, {label = t_concat(effect.stats, " / "), id = effect.effect})
|
|
end
|
|
end
|
|
--Check to make sure that the effects list has a potential mod to apply to a mastery
|
|
if not (next(effects) == nil) then
|
|
local passiveMasteryControlHeight = (#effects + 1) * 14 + 2
|
|
controls.close = new("ButtonControl", nil, {0, 30 + passiveMasteryControlHeight, 90, 20}, "Cancel", function()
|
|
node.sd = cachedSd
|
|
node.allMasteryOptions = cachedAllMasteryOption
|
|
self.build.spec.tree:ProcessStats(node)
|
|
main:ClosePopup()
|
|
end)
|
|
controls.effect = new("PassiveMasteryControl", {"TOPLEFT",nil,"TOPLEFT"}, {6, 25, 0, passiveMasteryControlHeight}, effects, self, node, controls.save)
|
|
main:OpenPopup(controls.effect.width + 12, controls.effect.height + 60, node.name, controls)
|
|
end
|
|
end
|
|
|
|
function TreeTabClass:SetPowerCalc(powerStat)
|
|
self.viewer.showHeatMap = true
|
|
self.build.buildFlag = true
|
|
self.build.calcsTab.powerBuildFlag = true
|
|
self.build.calcsTab.powerStat = powerStat
|
|
self.controls.powerReportList:SetReport(powerStat, nil)
|
|
end
|
|
|
|
function TreeTabClass:BuildPowerReportList(currentStat)
|
|
local report = {}
|
|
|
|
if not (currentStat and currentStat.stat) then
|
|
return report
|
|
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 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 ascendancies, sockets, etc.
|
|
for nodeId, node in pairs(self.build.spec.nodes) do
|
|
local isAlloc = node.alloc or self.build.calcsTab.mainEnv.grantedPassives[nodeId]
|
|
if (node.type == "Normal" or node.type == "Keystone" or node.type == "Notable") and not node.ascendancyName then
|
|
local pathDist
|
|
if isAlloc then
|
|
pathDist = #(node.depends or { }) == 0 and 1 or #node.depends
|
|
else
|
|
pathDist = #(node.path or { }) == 0 and 1 or #node.path
|
|
end
|
|
local nodePower = (node.power.singleStat or 0) * ((displayStat.pc or displayStat.mod) and 100 or 1)
|
|
local pathPower = (node.power.pathPower or 0) / pathDist * ((displayStat.pc or displayStat.mod) and 100 or 1)
|
|
local nodePowerStr = s_format("%"..displayStat.fmt, nodePower)
|
|
local pathPowerStr = s_format("%"..displayStat.fmt, pathPower)
|
|
|
|
nodePowerStr = formatNumSep(nodePowerStr)
|
|
pathPowerStr = formatNumSep(pathPowerStr)
|
|
|
|
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
|
|
if (pathPower > 0 and not displayStat.lowerIsBetter) or (pathPower < 0 and displayStat.lowerIsBetter) then
|
|
pathPowerStr = colorCodes.POSITIVE .. pathPowerStr
|
|
elseif (pathPower < 0 and not displayStat.lowerIsBetter) or (pathPower > 0 and displayStat.lowerIsBetter) then
|
|
pathPowerStr = colorCodes.NEGATIVE .. pathPowerStr
|
|
end
|
|
|
|
t_insert(report, {
|
|
name = node.dn,
|
|
power = nodePower,
|
|
powerStr = nodePowerStr,
|
|
pathPower = pathPower,
|
|
pathPowerStr = pathPowerStr,
|
|
allocated = isAlloc,
|
|
id = node.id,
|
|
x = node.x,
|
|
y = node.y,
|
|
type = node.type,
|
|
pathDist = 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 nodePower = (node.power and node.power.singleStat or 0) * ((displayStat.pc or displayStat.mod) and 100 or 1)
|
|
local nodePowerStr = s_format("%"..displayStat.fmt, nodePower)
|
|
|
|
nodePowerStr = formatNumSep(nodePowerStr)
|
|
|
|
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,
|
|
pathPower = 0,
|
|
pathPowerStr = "--",
|
|
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
|
|
|
|
return report
|
|
end
|
|
|
|
function TreeTabClass:FindTimelessJewel()
|
|
local socketViewer = new("PassiveTreeView")
|
|
local treeData = self.build.spec.tree
|
|
local legionNodes = treeData.legion.nodes
|
|
local legionAdditions = treeData.legion.additions
|
|
local timelessData = self.build.timelessData
|
|
local controls = { }
|
|
local modData = { }
|
|
local ignoredMods = { "Might of the Vaal", "Legacy of the Vaal", "Strength", "Add Strength", "Dex", "Add Dexterity", "Devotion", "Price of Glory" }
|
|
local totalMods = { [2] = "Strength", [3] = "Dexterity", [4] = "Devotion" }
|
|
local totalModIDs = {
|
|
["total_strength"] = { ["karui_notable_add_strength"] = true, ["karui_attribute_strength"] = true, ["karui_small_strength"] = true },
|
|
["total_dexterity"] = { ["maraketh_notable_add_dexterity"] = true, ["maraketh_attribute_dex"] = true, ["maraketh_small_dex"] = true },
|
|
["total_devotion"] = { ["templar_notable_devotion"] = true, ["templar_devotion_node"] = true, ["templar_small_devotion"] = true }
|
|
}
|
|
local reverseTotalModIDs = {
|
|
["karui_notable_add_strength"] = true,
|
|
["karui_attribute_strength"] = true,
|
|
["karui_small_strength"] = true,
|
|
["maraketh_notable_add_dexterity"] = true,
|
|
["maraketh_attribute_dex"] = true,
|
|
["maraketh_small_dex"] = true,
|
|
["templar_notable_devotion"] = true,
|
|
["templar_devotion_node"] = true,
|
|
["templar_small_devotion"] = true
|
|
}
|
|
local jewelTypes = {
|
|
{ label = "Glorious Vanity", name = "vaal", id = 1 },
|
|
{ label = "Lethal Pride", name = "karui", id = 2 },
|
|
{ label = "Brutal Restraint", name = "maraketh", id = 3 },
|
|
{ label = "Militant Faith", name = "templar", id = 4 },
|
|
{ label = "Elegant Hubris", name = "eternal", id = 5 }
|
|
}
|
|
-- rebuild `timelessData.jewelType` as we only store the minimum amount of `jewelType` data in build XML
|
|
if next(timelessData.jewelType) then
|
|
for idx, jewelType in ipairs(jewelTypes) do
|
|
if jewelType.id == timelessData.jewelType.id then
|
|
timelessData.jewelType = jewelType
|
|
break
|
|
end
|
|
end
|
|
else
|
|
timelessData.jewelType = jewelTypes[1]
|
|
end
|
|
local conquerorTypes = {
|
|
[1] = {
|
|
{ label = "Any", id = 1 },
|
|
{ label = "Doryani (Corrupted Soul)", id = 2 },
|
|
{ label = "Xibaqua (Divine Flesh)", id = 3 },
|
|
{ label = "Ahuana (Immortal Ambition)", id = 4 }
|
|
},
|
|
[2] = {
|
|
{ label = "Any", id = 1 },
|
|
{ label = "Kaom (Strength of Blood)", id = 2 },
|
|
{ label = "Rakiata (Tempered by War)", id = 3 },
|
|
{ label = "Akoya (Chainbreaker)", id = 4 }
|
|
},
|
|
[3] = {
|
|
{ label = "Any", id = 1 },
|
|
{ label = "Asenath (Dance with Death)", id = 2 },
|
|
{ label = "Nasima (Second Sight)", id = 3 },
|
|
{ label = "Balbala (The Traitor)", id = 4 }
|
|
},
|
|
[4] = {
|
|
{ label = "Any", id = 1 },
|
|
{ label = "Avarius (Power of Purpose)", id = 2 },
|
|
{ label = "Dominus (Inner Conviction)", id = 3 },
|
|
{ label = "Maxarius (Transcendence)", id = 4 }
|
|
},
|
|
[5] = {
|
|
{ label = "Any", id = 1 },
|
|
{ label = "Cadiro (Supreme Decadence)", id = 2 },
|
|
{ label = "Victario (Supreme Grandstanding)", id = 3 },
|
|
{ label = "Caspiro (Supreme Ostentation)", id = 4 }
|
|
}
|
|
}
|
|
-- rebuild `timelessData.conquerorType` as we only store the minimum amount of `conquerorType` data in build XML
|
|
if next(timelessData.conquerorType) then
|
|
for idx, conquerorType in ipairs(conquerorTypes[timelessData.jewelType.id]) do
|
|
if conquerorType.id == timelessData.conquerorType.id then
|
|
timelessData.conquerorType = conquerorType
|
|
break
|
|
end
|
|
end
|
|
else
|
|
timelessData.conquerorType = conquerorTypes[timelessData.jewelType.id][1]
|
|
end
|
|
local devotionVariants = {
|
|
{ id = 1 , label = "Any" },
|
|
{ id = 2 , label = "Totem Damage" },
|
|
{ id = 3 , label = "Brand Damage" },
|
|
{ id = 4 , label = "Channelling Damage" },
|
|
{ id = 5 , label = "Area Damage" },
|
|
{ id = 6 , label = "Elemental Damage" },
|
|
{ id = 7 , label = "Elemental Resistances" },
|
|
{ id = 8 , label = "Effect of non-Damaging Ailments" },
|
|
{ id = 9 , label = "Elemental Ailment Duration" },
|
|
{ id = 10, label = "Duration of Curses" },
|
|
{ id = 11, label = "Minion Attack and Cast Speed" },
|
|
{ id = 12, label = "Minions Accuracy Rating" },
|
|
{ id = 13, label = "Mana Regen" },
|
|
{ id = 14, label = "Skill Cost" },
|
|
{ id = 15, label = "Non-Curse Aura Effect" },
|
|
{ id = 16, label = "Defences from Shield" }
|
|
}
|
|
local jewelSockets = { }
|
|
for socketId, socketData in pairs(self.build.spec.nodes) do
|
|
if socketData.isJewelSocket and socketData.name ~= "Charm Socket"then
|
|
local keystone = "Unknown"
|
|
if socketId == 26725 then
|
|
keystone = "Marauder"
|
|
elseif socketId == 54127 then
|
|
keystone = "Duelist"
|
|
elseif socketId == 7960 then
|
|
keystone = "Templar/Witch"
|
|
else
|
|
local minDistance = math.huge
|
|
for _, nodeInRadius in pairs(treeData.nodes[socketId].nodesInRadius[3]) do
|
|
if nodeInRadius.isKeystone then
|
|
local distance = math.sqrt((nodeInRadius.x - socketData.x) ^ 2 + (nodeInRadius.y - socketData.y) ^ 2)
|
|
if distance < minDistance then
|
|
keystone = nodeInRadius.name
|
|
minDistance = distance
|
|
end
|
|
end
|
|
end
|
|
end
|
|
local label = keystone .. ": " .. socketId
|
|
if self.build.spec.allocNodes[socketId] then
|
|
label = "# " .. label
|
|
end
|
|
t_insert(jewelSockets, {
|
|
label = label,
|
|
keystone = keystone,
|
|
id = socketId
|
|
})
|
|
end
|
|
end
|
|
t_sort(jewelSockets, function(a, b) return a.label < b.label end)
|
|
-- rebuild `timelessData.jewelSocket` as we only store the minimum amount of `jewelSocket` data in build XML
|
|
if next(timelessData.jewelSocket) then
|
|
for idx, jewelSocket in ipairs(jewelSockets) do
|
|
if jewelSocket.id == timelessData.jewelSocket.id then
|
|
timelessData.jewelSocket = jewelSocket
|
|
break
|
|
end
|
|
end
|
|
else
|
|
timelessData.jewelSocket = jewelSockets[1]
|
|
end
|
|
|
|
local function buildMods()
|
|
wipeTable(modData)
|
|
local smallModData = { }
|
|
for _, node in pairs(legionNodes) do
|
|
if node.id:match("^" .. timelessData.jewelType.name .. "_.+") and not isValueInArray(ignoredMods, node.dn) and not node.ks then
|
|
if node["not"] then
|
|
t_insert(modData, {
|
|
label = node.dn .. " " .. node.sd[1],
|
|
descriptions = copyTable(node.sd),
|
|
type = timelessData.jewelType.name,
|
|
id = node.id
|
|
})
|
|
if node.sd[2] then
|
|
modData[#modData].label = modData[#modData].label .. " " .. node.sd[2]
|
|
end
|
|
else
|
|
t_insert(smallModData, {
|
|
label = node.dn,
|
|
descriptions = copyTable(node.sd),
|
|
type = timelessData.jewelType.name,
|
|
id = node.id
|
|
})
|
|
end
|
|
end
|
|
end
|
|
for _, addition in pairs(legionAdditions) do
|
|
-- exclude passives that are already added (vaal, attributes, devotion)
|
|
if addition.id:match("^" .. timelessData.jewelType.name .. "_.+") and not isValueInArray(ignoredMods, addition.dn) and timelessData.jewelType.name ~= "vaal" then
|
|
t_insert(modData, {
|
|
label = addition.dn,
|
|
descriptions = copyTable(addition.sd),
|
|
type = timelessData.jewelType.name,
|
|
id = addition.id
|
|
})
|
|
end
|
|
end
|
|
t_sort(modData, function(a, b) return a.label < b.label end)
|
|
t_sort(smallModData, function (a, b) return a.label < b.label end)
|
|
if totalMods[timelessData.jewelType.id] then
|
|
t_insert(modData, 1, {
|
|
label = "Total " .. totalMods[timelessData.jewelType.id],
|
|
descriptions = { "This is a hybrid node containing all additions to " .. totalMods[timelessData.jewelType.id] },
|
|
type = timelessData.jewelType.name,
|
|
id = "total_" .. totalMods[timelessData.jewelType.id]:lower(),
|
|
totalMod = true
|
|
})
|
|
end
|
|
t_insert(modData, 1, { label = "..." })
|
|
for i = 1, #smallModData do
|
|
modData[#modData + 1] = smallModData[i]
|
|
end
|
|
end
|
|
|
|
local function getNodeWeights()
|
|
local nodeWeights = {
|
|
[1] = controls.nodeSliderValue.label:sub(3):lower(),
|
|
[2] = controls.nodeSlider2Value.label:sub(3):lower(),
|
|
[3] = controls.nodeSlider3Value.label:sub(3):lower()
|
|
}
|
|
for i, nodeWeight in ipairs(nodeWeights) do
|
|
if tonumber(nodeWeight) ~= nil then
|
|
nodeWeights[i] = round(tonumber(nodeWeight), 3)
|
|
end
|
|
end
|
|
return nodeWeights
|
|
end
|
|
|
|
local searchListTbl = { }
|
|
local searchListFallbackTbl = { }
|
|
local function parseSearchList(mode, fallback)
|
|
if mode == 0 then
|
|
if fallback then
|
|
-- timelessData.searchListFallback => searchListFallbackTbl
|
|
if timelessData.searchListFallback then
|
|
searchListFallbackTbl = { }
|
|
for inputLine in timelessData.searchListFallback:gmatch("[^\r\n]+") do
|
|
searchListFallbackTbl[#searchListFallbackTbl + 1] = { }
|
|
for splitLine in inputLine:gmatch("([^,%s]+)") do
|
|
searchListFallbackTbl[#searchListFallbackTbl][#searchListFallbackTbl[#searchListFallbackTbl] + 1] = splitLine
|
|
end
|
|
end
|
|
end
|
|
else
|
|
-- timelessData.searchList => searchListTbl
|
|
if timelessData.searchList then
|
|
searchListTbl = { }
|
|
for inputLine in timelessData.searchList:gmatch("[^\r\n]+") do
|
|
searchListTbl[#searchListTbl + 1] = { }
|
|
for splitLine in inputLine:gmatch("([^,%s]+)") do
|
|
searchListTbl[#searchListTbl][#searchListTbl[#searchListTbl] + 1] = splitLine
|
|
end
|
|
end
|
|
end
|
|
end
|
|
else
|
|
if fallback then
|
|
-- searchListFallbackTbl => controls.searchListFallback
|
|
if controls.searchListFallback and controls.nodeSelect then
|
|
local searchText = ""
|
|
for _, curRow in ipairs(searchListFallbackTbl) do
|
|
if curRow[1] == controls.nodeSelect.list[controls.nodeSelect.selIndex].id then
|
|
local nodeWeights = getNodeWeights()
|
|
curRow[2] = nodeWeights[1]
|
|
curRow[3] = nodeWeights[2]
|
|
curRow[4] = nodeWeights[3]
|
|
end
|
|
if #searchText > 0 then
|
|
searchText = searchText .. "\n"
|
|
end
|
|
searchText = searchText .. t_concat(curRow, ", ")
|
|
end
|
|
if timelessData.searchListFallback ~= searchText then
|
|
timelessData.searchListFallback = searchText
|
|
controls.searchListFallback:SetText(searchText)
|
|
self.build.modFlag = true
|
|
end
|
|
end
|
|
else
|
|
-- searchListTbl => controls.searchList
|
|
if controls.searchList and controls.nodeSelect then
|
|
local searchText = ""
|
|
for _, curRow in ipairs(searchListTbl) do
|
|
if curRow[1] == controls.nodeSelect.list[controls.nodeSelect.selIndex].id then
|
|
local nodeWeights = getNodeWeights()
|
|
curRow[2] = nodeWeights[1]
|
|
curRow[3] = nodeWeights[2]
|
|
curRow[4] = nodeWeights[3]
|
|
end
|
|
if #searchText > 0 then
|
|
searchText = searchText .. "\n"
|
|
end
|
|
searchText = searchText .. t_concat(curRow, ", ")
|
|
end
|
|
if timelessData.searchList ~= searchText then
|
|
timelessData.searchList = searchText
|
|
controls.searchList:SetText(searchText)
|
|
self.build.modFlag = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
parseSearchList(0, false) -- initial load: [timelessData.searchList => searchListTbl]
|
|
parseSearchList(0, true) -- initial load: [timelessData.searchListFallback => searchListFallbackTbl]
|
|
local function updateSearchList(text, fallback)
|
|
if fallback then
|
|
timelessData.searchListFallback = text
|
|
controls.searchListFallback:SetText(text)
|
|
else
|
|
timelessData.searchList = text
|
|
controls.searchList:SetText(text)
|
|
end
|
|
parseSearchList(0, fallback)
|
|
self.build.modFlag = true
|
|
end
|
|
|
|
controls.devotionSelectLabel = new("LabelControl", {"TOPRIGHT", nil, "TOPLEFT"}, {820, 25, 0, 16}, "^7Devotion modifiers:")
|
|
controls.devotionSelectLabel.shown = timelessData.jewelType.id == 4
|
|
controls.devotionSelect1 = new("DropDownControl", {"TOP", controls.devotionSelectLabel, "BOTTOM"}, {0, 8, 200, 18}, devotionVariants, function(index, value)
|
|
timelessData.devotionVariant1 = index
|
|
end)
|
|
controls.devotionSelect1.selIndex = timelessData.devotionVariant1
|
|
controls.devotionSelect2 = new("DropDownControl", {"TOP", controls.devotionSelect1, "BOTTOM"}, {0, 7, 200, 18}, devotionVariants, function(index, value)
|
|
timelessData.devotionVariant2 = index
|
|
end)
|
|
controls.devotionSelect2.selIndex = timelessData.devotionVariant2
|
|
|
|
controls.jewelSelectLabel = new("LabelControl", {"TOPRIGHT", nil, "TOPLEFT"}, {405, 25, 0, 16}, "^7Jewel Type:")
|
|
controls.jewelSelect = new("DropDownControl", {"LEFT", controls.jewelSelectLabel, "RIGHT"}, {10, 0, 200, 18}, jewelTypes, function(index, value)
|
|
timelessData.jewelType = value
|
|
controls.devotionSelectLabel.shown = value.id == 4 -- Militant Faith
|
|
controls.protectAllocatedLabel.shown = (value.id == 4 and controls.socketFilter.state)
|
|
controls.conquerorSelect.list = conquerorTypes[timelessData.jewelType.id]
|
|
controls.conquerorSelect.selIndex = 1
|
|
timelessData.conquerorType = conquerorTypes[timelessData.jewelType.id][1]
|
|
controls.nodeSelect.selIndex = 1
|
|
buildMods()
|
|
updateSearchList("", false)
|
|
updateSearchList("", true)
|
|
end)
|
|
controls.jewelSelect.selIndex = timelessData.jewelType.id
|
|
|
|
controls.conquerorSelectLabel = new("LabelControl", {"TOPRIGHT", nil, "TOPLEFT"}, {405, 50, 0, 16}, "^7Conqueror:")
|
|
controls.conquerorSelect = new("DropDownControl", {"LEFT", controls.conquerorSelectLabel, "RIGHT"}, {10, 0, 200, 18}, conquerorTypes[timelessData.jewelType.id], function(index, value)
|
|
timelessData.conquerorType = value
|
|
self.build.modFlag = true
|
|
end)
|
|
controls.conquerorSelect.selIndex = timelessData.conquerorType.id
|
|
|
|
local allocatedNodes = { }
|
|
local protectedNodes = { }
|
|
local protectedNodesCount = 0
|
|
self.allocatedNodesInRadiusCount = 0
|
|
local function setAllocatedNodes() -- find allocated nodes in radius for Militant Faith filtering / protected nodes dropdown
|
|
local nodeNames = { }
|
|
local radiusNodes = treeData.nodes[timelessData.jewelSocket.id].nodesInRadius[3] -- large radius around timelessData.jewelSocket.id
|
|
for nodeId in pairs(radiusNodes) do
|
|
if self.build.calcsTab.mainEnv.grantedPassives[nodeId] ~= nil or self.build.spec.allocNodes[nodeId] ~= nil then
|
|
allocatedNodes[nodeId] = true
|
|
if treeData.nodes[nodeId] and treeData.nodes[nodeId].isNotable then
|
|
t_insert(nodeNames, treeData.nodes[nodeId].dn)
|
|
end
|
|
end
|
|
end
|
|
controls.protectAllocatedSelect:SetList(nodeNames)
|
|
self.allocatedNodesInRadiusCount = #nodeNames
|
|
end
|
|
|
|
controls.socketSelectLabel = new("LabelControl", {"TOPRIGHT", nil, "TOPLEFT"}, {405, 75, 0, 16}, "^7Jewel Socket:")
|
|
controls.socketSelect = new("TimelessJewelSocketControl", {"LEFT", controls.socketSelectLabel, "RIGHT"}, {10, 0, 200, 18}, jewelSockets, function(index, value)
|
|
timelessData.jewelSocket = value
|
|
setAllocatedNodes() -- reset list when changing sockets
|
|
self.build.modFlag = true
|
|
end, self.build, socketViewer)
|
|
-- we need to search through `jewelSockets` for the correct `id` as the `idx` can become stale due to dynamic sorting
|
|
for idx, jewelSocket in ipairs(jewelSockets) do
|
|
if jewelSocket.id == timelessData.jewelSocket.id then
|
|
controls.socketSelect.selIndex = idx
|
|
break
|
|
end
|
|
end
|
|
|
|
local function clearProtected() -- clear all controls, nodes related to Militant Faith filtering
|
|
protectedNodesCount = 0
|
|
protectedNodes = { }
|
|
for index, _ in pairs(controls) do
|
|
if index:find("protected:") then
|
|
controls[index] = nil
|
|
end
|
|
end
|
|
end
|
|
controls.socketFilterLabel = new("LabelControl", { "TOPRIGHT", nil, "TOPLEFT" }, { 405, 100, 0, 16 }, "^7Filter Nodes:")
|
|
controls.socketFilter = new("CheckBoxControl", { "LEFT", controls.socketFilterLabel, "RIGHT" }, { 10, 0, 18 }, nil, function(value)
|
|
timelessData.socketFilter = value
|
|
self.build.modFlag = true
|
|
controls.socketFilterAdditionalDistanceLabel.shown = value
|
|
controls.socketFilterAdditionalDistance.shown = value
|
|
controls.socketFilterAdditionalDistanceValue.shown = value
|
|
controls.protectAllocatedLabel.shown = (value and timelessData.jewelType.label == "Militant Faith")
|
|
|
|
if value then
|
|
setAllocatedNodes()
|
|
else
|
|
clearProtected()
|
|
end
|
|
end)
|
|
controls.socketFilter.tooltipFunc = function(tooltip, mode, index, value)
|
|
tooltip:Clear()
|
|
tooltip:AddLine(16, "^7Enable this option to exclude nodes that you do not have allocated on your active passive skill tree.")
|
|
tooltip:AddLine(16, "^7This can be useful if you're never going to path towards those excluded nodes and don't care what happens to them.")
|
|
end
|
|
controls.socketFilter.state = timelessData.socketFilter
|
|
|
|
-- Militant Faith protect notables controls
|
|
controls.protectAllocatedLabel = new("LabelControl", { "TOPLEFT", nil, "TOPLEFT" }, { 15, 25, 0, 16 }, "^7Protect allocated nodes from changing:")
|
|
controls.protectAllocatedSelect = new("DropDownControl", { "TOPLEFT", controls.protectAllocatedLabel, "BOTTOMLEFT" }, { 0, 8, 200, 18 }, nil, nil)
|
|
controls.protectAllocatedButtonAdd = new("ButtonControl", { "LEFT", controls.protectAllocatedSelect, "RIGHT" }, { 5, 0, 44, 18 }, "Add", function()
|
|
local selValue = controls.protectAllocatedSelect:GetSelValue()
|
|
if selValue and not controls["protected:"..selValue] then
|
|
protectedNodesCount = protectedNodesCount + 1
|
|
t_insert(protectedNodes, selValue)
|
|
controls["protected:"..selValue] = new("LabelControl", { "TOPLEFT", controls.protectAllocatedSelect, "BOTTOMLEFT" }, { 0, 16 * protectedNodesCount - 10, 0, 16 }, "^7"..selValue)
|
|
end
|
|
end)
|
|
controls.protectAllocatedButtonClear = new("ButtonControl", { "LEFT", controls.protectAllocatedButtonAdd, "RIGHT" }, { 5, 0, 44, 18 }, "Clear", function()
|
|
clearProtected()
|
|
end)
|
|
-- set shown and list on load
|
|
if controls.socketFilter.state then
|
|
setAllocatedNodes()
|
|
end
|
|
controls.protectAllocatedLabel.shown = controls.jewelSelect.selIndex == 4 and controls.socketFilter.state
|
|
|
|
controls.protectAllocatedButtonAdd.tooltipFunc = function(tooltip, mode, index, value)
|
|
tooltip:Clear()
|
|
tooltip:AddLine(16, "^7Protect allocated nodes during search.")
|
|
tooltip:AddLine(16, "^7This can be useful if transforming certain notables would break your build.")
|
|
end
|
|
|
|
local socketFilterAdditionalDistanceMAX = 10
|
|
controls.socketFilterAdditionalDistanceLabel = new("LabelControl", {"LEFT", controls.socketFilter, "RIGHT"}, {10, 0, 0, 16}, "^7Node Distance:")
|
|
controls.socketFilterAdditionalDistance = new("SliderControl", {"LEFT", controls.socketFilterAdditionalDistanceLabel, "RIGHT"}, {10, 0, 66, 18}, function(value)
|
|
timelessData.socketFilterDistance = m_floor(value * socketFilterAdditionalDistanceMAX + 0.01)
|
|
controls.socketFilterAdditionalDistanceValue.label = s_format("^7%d", timelessData.socketFilterDistance)
|
|
end, { ["SHIFT"] = 1, ["CTRL"] = 1 / (socketFilterAdditionalDistanceMAX * 2), ["DEFAULT"] = 1 / socketFilterAdditionalDistanceMAX })
|
|
controls.socketFilterAdditionalDistance.tooltipFunc = function(tooltip, mode, index, value)
|
|
tooltip:Clear()
|
|
if not controls.socketFilterAdditionalDistance.dragging then
|
|
tooltip:AddLine(16, "^7This controls the maximum amount of points that need to be spent to grab a node before its filtered out")
|
|
end
|
|
end
|
|
controls.socketFilterAdditionalDistance.tooltip.realDraw = controls.socketFilterAdditionalDistance.tooltip.Draw
|
|
controls.socketFilterAdditionalDistance.tooltip.Draw = function(self, x, y, width, height, viewPort)
|
|
local sliderOffsetX = round(184 * (1 - controls.socketFilterAdditionalDistance.val))
|
|
local tooltipWidth, tooltipHeight = self:GetSize()
|
|
if main.screenW >= 1384 - sliderOffsetX then
|
|
return controls.socketFilterAdditionalDistance.tooltip.realDraw(self, x - 8 - sliderOffsetX, y - 4 - tooltipHeight, width, height, viewPort)
|
|
end
|
|
return controls.socketFilterAdditionalDistance.tooltip.realDraw(self, x, y, width, height, viewPort)
|
|
end
|
|
controls.socketFilterAdditionalDistanceValue = new("LabelControl", {"LEFT", controls.socketFilterAdditionalDistance, "RIGHT"}, {5, 0, 0, 16}, "^70")
|
|
controls.socketFilterAdditionalDistance:SetVal((timelessData.socketFilterDistance or 0) / socketFilterAdditionalDistanceMAX)
|
|
controls.socketFilterAdditionalDistanceLabel.shown = timelessData.socketFilter
|
|
controls.socketFilterAdditionalDistance.shown = timelessData.socketFilter
|
|
controls.socketFilterAdditionalDistanceValue.shown = timelessData.socketFilter
|
|
|
|
local scrollWheelSpeedTbl = { ["SHIFT"] = 0.01, ["CTRL"] = 0.0001, ["DEFAULT"] = 0.001 }
|
|
local scrollWheelSpeedTbl2 = { ["SHIFT"] = 0.2, ["CTRL"] = 0.002, ["DEFAULT"] = 0.02 }
|
|
|
|
local nodeSliderStatLabel = "None"
|
|
controls.nodeSliderLabel = new("LabelControl", {"TOPRIGHT", nil, "TOPLEFT"}, {405, 125, 0, 16}, "^7Primary Node Weight:")
|
|
controls.nodeSlider = new("SliderControl", {"LEFT", controls.nodeSliderLabel, "RIGHT"}, {10, 0, 200, 16}, function(value)
|
|
controls.nodeSliderValue.label = s_format("^7%.3f", value * 10)
|
|
parseSearchList(1, controls.searchListFallback and controls.searchListFallback.shown or false)
|
|
end, scrollWheelSpeedTbl)
|
|
controls.nodeSlider.tooltipFunc = function(tooltip, mode, index, value)
|
|
tooltip:Clear()
|
|
if not controls.nodeSlider.dragging then
|
|
if nodeSliderStatLabel == "None" then
|
|
tooltip:AddLine(16, "^7For nodes with multiple stats this slider controls the weight of the first stat listed.")
|
|
else
|
|
tooltip:AddLine(16, "^7This slider controls the weight of the following stat:")
|
|
tooltip:AddLine(16, "^7 " .. nodeSliderStatLabel)
|
|
end
|
|
end
|
|
end
|
|
controls.nodeSliderValue = new("LabelControl", {"LEFT", controls.nodeSlider, "RIGHT"}, {5, 0, 0, 16}, "^71.000")
|
|
controls.nodeSlider.tooltip.realDraw = controls.nodeSlider.tooltip.Draw
|
|
controls.nodeSlider.tooltip.Draw = function(self, x, y, width, height, viewPort)
|
|
local sliderOffsetX = round(184 * (1 - controls.nodeSlider.val))
|
|
local tooltipWidth, tooltipHeight = self:GetSize()
|
|
if main.screenW >= 1338 - sliderOffsetX then
|
|
return controls.nodeSlider.tooltip.realDraw(self, x - 8 - sliderOffsetX, y - 4 - tooltipHeight, width, height, viewPort)
|
|
end
|
|
return controls.nodeSlider.tooltip.realDraw(self, x, y, width, height, viewPort)
|
|
end
|
|
controls.nodeSlider:SetVal(0.1)
|
|
|
|
local nodeSlider2StatLabel = "None"
|
|
controls.nodeSlider2Label = new("LabelControl", {"TOPRIGHT", nil, "TOPLEFT"}, {405, 150, 0, 16}, "^7Secondary Node Weight:")
|
|
controls.nodeSlider2 = new("SliderControl", {"LEFT", controls.nodeSlider2Label, "RIGHT"}, {10, 0, 200, 16}, function(value)
|
|
controls.nodeSlider2Value.label = s_format("^7%.3f", value * 10)
|
|
parseSearchList(1, controls.searchListFallback and controls.searchListFallback.shown or false)
|
|
end, scrollWheelSpeedTbl)
|
|
controls.nodeSlider2.tooltipFunc = function(tooltip, mode, index, value)
|
|
tooltip:Clear()
|
|
if not controls.nodeSlider2.dragging then
|
|
if nodeSlider2StatLabel == "None" then
|
|
tooltip:AddLine(16, "^7For nodes with multiple stats this slider controls the weight of the second stat listed.")
|
|
else
|
|
tooltip:AddLine(16, "^7This slider controls the weight of the following stat:")
|
|
tooltip:AddLine(16, "^7 " .. nodeSlider2StatLabel)
|
|
end
|
|
end
|
|
end
|
|
controls.nodeSlider2Value = new("LabelControl", {"LEFT", controls.nodeSlider2, "RIGHT"}, {5, 0, 0, 16}, "^71.000")
|
|
controls.nodeSlider2.tooltip.realDraw = controls.nodeSlider2.tooltip.Draw
|
|
controls.nodeSlider2.tooltip.Draw = function(self, x, y, width, height, viewPort)
|
|
local sliderOffsetX = round(184 * (1 - controls.nodeSlider2.val))
|
|
local tooltipWidth, tooltipHeight = self:GetSize()
|
|
if main.screenW >= 1384 - sliderOffsetX then
|
|
return controls.nodeSlider2.tooltip.realDraw(self, x - 8 - sliderOffsetX, y - 4 - tooltipHeight, width, height, viewPort)
|
|
end
|
|
return controls.nodeSlider2.tooltip.realDraw(self, x, y, width, height, viewPort)
|
|
end
|
|
controls.nodeSlider2:SetVal(0.1)
|
|
|
|
controls.nodeSlider3Label = new("LabelControl", {"TOPRIGHT", nil, "TOPLEFT"}, {405, 175, 0, 16}, "^7Minimum Node Weight:")
|
|
controls.nodeSlider3 = new("SliderControl", {"LEFT", controls.nodeSlider3Label, "RIGHT"}, {10, 0, 200, 16}, function(value)
|
|
if value == 1 then
|
|
controls.nodeSlider3Value.label = "^7Required"
|
|
else
|
|
controls.nodeSlider3Value.label = s_format("^7%.f", value * 500)
|
|
end
|
|
parseSearchList(1, controls.searchListFallback and controls.searchListFallback.shown or false)
|
|
end, scrollWheelSpeedTbl2)
|
|
controls.nodeSlider3.tooltipFunc = function(tooltip, mode, index, value)
|
|
tooltip:Clear()
|
|
if not controls.nodeSlider3.dragging then
|
|
tooltip:AddLine(16, "^7Seeds that do not meet the minimum weight threshold for a desired node are excluded from the search results.")
|
|
end
|
|
end
|
|
controls.nodeSlider3Value = new("LabelControl", {"LEFT", controls.nodeSlider3, "RIGHT"}, {5, 0, 0, 16}, "^70")
|
|
controls.nodeSlider3.tooltip.realDraw = controls.nodeSlider3.tooltip.Draw
|
|
controls.nodeSlider3.tooltip.Draw = function(self, x, y, width, height, viewPort)
|
|
local sliderOffsetX = round(184 * (1 - controls.nodeSlider3.val))
|
|
local tooltipWidth, tooltipHeight = self:GetSize()
|
|
if main.screenW >= 1728 - sliderOffsetX then
|
|
return controls.nodeSlider3.tooltip.realDraw(self, x - 8 - sliderOffsetX, y - 4 - tooltipHeight, width, height, viewPort)
|
|
end
|
|
return controls.nodeSlider3.tooltip.realDraw(self, x, y, width, height, viewPort)
|
|
end
|
|
controls.nodeSlider3:SetVal(0)
|
|
|
|
local function updateSliders(sliderData)
|
|
if sliderData[2] == "required" then
|
|
controls.nodeSlider.val = 1
|
|
controls.nodeSliderValue.label = s_format("^7%.3f", 10)
|
|
else
|
|
controls.nodeSlider.val = m_min(m_max((tonumber(sliderData[2]) or 0) / 10, 0), 1)
|
|
controls.nodeSliderValue.label = s_format("^7%.3f", controls.nodeSlider.val * 10)
|
|
end
|
|
if controls.nodeSlider2.enabled then
|
|
if sliderData[3] == "required" then
|
|
controls.nodeSlider2.val = 1
|
|
controls.nodeSlider2Value.label = s_format("^7%.3f", 10)
|
|
else
|
|
controls.nodeSlider2.val = m_min(m_max((tonumber(sliderData[3]) or 0) / 10, 0), 1)
|
|
controls.nodeSlider2Value.label = s_format("^7%.3f", controls.nodeSlider2.val * 10)
|
|
end
|
|
end
|
|
if sliderData[4] == "required" then
|
|
controls.nodeSlider3.val = 1
|
|
controls.nodeSlider3Value.label = "^7Required"
|
|
else
|
|
controls.nodeSlider3.val = m_min(m_max((tonumber(sliderData[4]) or 0) / 500, 0), 1)
|
|
controls.nodeSlider3Value.label = s_format("^7%.f", controls.nodeSlider3.val * 500)
|
|
end
|
|
end
|
|
|
|
buildMods()
|
|
controls.nodeSelectLabel = new("LabelControl", {"TOPRIGHT", nil, "TOPLEFT"}, {405, 200, 0, 16}, "^7Search for Node:")
|
|
controls.nodeSelect = new("DropDownControl", {"LEFT", controls.nodeSelectLabel, "RIGHT"}, {10, 0, 200, 18}, modData, function(index, value)
|
|
nodeSliderStatLabel = "None"
|
|
nodeSlider2StatLabel = "None"
|
|
if value.id then
|
|
local statCount = 0
|
|
for _, legionNode in ipairs(legionNodes) do
|
|
if legionNode.id == value.id then
|
|
statCount = #legionNode.sd
|
|
nodeSliderStatLabel = legionNode.sd[1] or "None"
|
|
nodeSlider2StatLabel = legionNode.sd[2] or "None"
|
|
break
|
|
end
|
|
end
|
|
if statCount == 0 then
|
|
for _, legionAddition in ipairs(legionAdditions) do
|
|
if legionAddition.id == value.id then
|
|
statCount = #legionAddition.sd
|
|
nodeSliderStatLabel = legionAddition.sd[1] or "None"
|
|
nodeSlider2StatLabel = legionAddition.sd[2] or "None"
|
|
break
|
|
end
|
|
end
|
|
end
|
|
if statCount <= 1 then
|
|
controls.nodeSlider2Label.label = "^9Secondary Node Weight:"
|
|
controls.nodeSlider2.val = 0
|
|
controls.nodeSlider2Value.label = s_format("^9%.3f", 0)
|
|
else
|
|
controls.nodeSlider2Label.label = "^7Secondary Node Weight:"
|
|
controls.nodeSlider2Value.label = s_format("^7%.3f", controls.nodeSlider2.val * 10)
|
|
end
|
|
controls.nodeSlider2.enabled = statCount > 1
|
|
|
|
local nodeWeights = getNodeWeights()
|
|
local newNode = value.id .. ", " .. nodeWeights[1] .. ", " .. nodeWeights[2] .. ", " .. nodeWeights[3]
|
|
if controls.searchListFallback and controls.searchListFallback.shown then
|
|
for _, searchRow in ipairs(searchListFallbackTbl) do
|
|
-- update nodeSlider values and prevent duplicate searchList entries
|
|
if searchRow[1] == value.id then
|
|
updateSliders(searchRow)
|
|
return
|
|
end
|
|
end
|
|
controls.searchListFallback.caret = #controls.searchListFallback.buf + 1
|
|
controls.searchListFallback:Insert((#controls.searchListFallback.buf > 0 and "\n" or "") .. newNode)
|
|
else
|
|
for _, searchRow in ipairs(searchListTbl) do
|
|
-- update nodeSlider values and prevent duplicate searchList entries
|
|
if searchRow[1] == value.id then
|
|
updateSliders(searchRow)
|
|
return
|
|
end
|
|
end
|
|
controls.searchList.caret = #controls.searchList.buf + 1
|
|
controls.searchList:Insert((#controls.searchList.buf > 0 and "\n" or "") .. newNode)
|
|
end
|
|
self.build.modFlag = true
|
|
end
|
|
end)
|
|
controls.nodeSelect.tooltipFunc = function(tooltip, mode, index, value)
|
|
tooltip:Clear()
|
|
if mode ~= "OUT" and value.descriptions then
|
|
for _, line in ipairs(value.descriptions) do
|
|
tooltip:AddLine(16, "^7" .. line)
|
|
end
|
|
end
|
|
end
|
|
|
|
local function generateFallbackWeights(nodes, selection)
|
|
local calcFunc, calcBase = self.build.calcsTab:GetMiscCalculator(self.build)
|
|
local newList = { }
|
|
local baseOutput = calcFunc()
|
|
if baseOutput.Minion then
|
|
baseOutput = baseOutput.Minion
|
|
end
|
|
local baseValue = baseOutput[selection.stat] or 1
|
|
if selection.transform then
|
|
baseValue = selection.transform(baseValue)
|
|
end
|
|
for _, newNode in ipairs(nodes) do
|
|
local output = nil
|
|
if newNode.calcMultiple then
|
|
output = calcFunc({ addNodes = { [newNode.node[1]] = true } })
|
|
else
|
|
output = calcFunc({ addNodes = { [newNode] = true } })
|
|
end
|
|
if output.Minion then
|
|
output = output.Minion
|
|
end
|
|
local outputValue = output[selection.stat] or 0
|
|
if selection.transform then
|
|
outputValue = selection.transform(outputValue)
|
|
end
|
|
outputValue = outputValue / baseValue
|
|
if outputValue ~= outputValue then
|
|
outputValue = 1
|
|
end
|
|
t_insert(newList, {
|
|
id = newNode.id,
|
|
weight1 = (outputValue - 1) / (newNode.divisor or 1)
|
|
})
|
|
if newNode.calcMultiple then
|
|
output = calcFunc({ addNodes = { [newNode.node[2]] = true } })
|
|
if output.Minion then
|
|
output = output.Minion
|
|
end
|
|
outputValue = output[selection.stat] or 0
|
|
if selection.transform then
|
|
outputValue = selection.transform(outputValue)
|
|
end
|
|
outputValue = outputValue / baseValue
|
|
if outputValue ~= outputValue then
|
|
outputValue = 1
|
|
end
|
|
newList[#newList].weight2 = (outputValue - 1) / (newNode.divisor or 1)
|
|
end
|
|
end
|
|
return newList
|
|
end
|
|
|
|
local function setupFallbackWeights()
|
|
-- replaceHelperFunc is duplicated from PassiveSpec.lua
|
|
local replaceHelperFunc = function(statToFix, statKey, statMod, value)
|
|
if statMod.fmt == "g" then -- note the only one we actually care about is "Ritual of Flesh" life regen
|
|
if statKey:find("per_minute") then
|
|
value = round(value / 60, 1)
|
|
elseif statKey:find("permyriad") then
|
|
value = value / 100
|
|
elseif statKey:find("_ms") then
|
|
value = value / 1000
|
|
end
|
|
end
|
|
--if statMod.fmt == "d" then -- only ever d or g, and we want both past here
|
|
if statMod.min ~= statMod.max then
|
|
return statToFix:gsub("%(" .. statMod.min .. "%-" .. statMod.max .. "%)", value)
|
|
elseif statMod.min ~= value then -- only true for might/legacy of the vaal which can combine stats
|
|
return statToFix:gsub(statMod.min, value)
|
|
end
|
|
return statToFix -- if it doesn't need to be changed
|
|
end
|
|
|
|
local nodes = { }
|
|
for _, modNode in ipairs(modData) do
|
|
if modNode.id then
|
|
local newNode = nil
|
|
for _, legionNode in ipairs(legionNodes) do
|
|
if legionNode.id == modNode.id or (totalModIDs[modNode.id] and totalModIDs[modNode.id][legionNode.id]) then
|
|
newNode = { }
|
|
newNode.id = modNode.id
|
|
if modNode.type == "vaal" then
|
|
if #legionNode.sd == 2 then
|
|
newNode.calcMultiple = true
|
|
if legionNode.modListGenerated then
|
|
newNode.node = copyTable(legionNode.modListGenerated)
|
|
else
|
|
-- generate modList
|
|
local modList1, extra1 = modLib.parseMod(replaceHelperFunc(legionNode.sd[1], legionNode.sortedStats[1], legionNode.stats[legionNode.sortedStats[1]], 100))
|
|
local modList2, extra2 = modLib.parseMod(replaceHelperFunc(legionNode.sd[2], legionNode.sortedStats[2], legionNode.stats[legionNode.sortedStats[2]], 100))
|
|
local modLists = { { modList = modList1 }, { modList = modList2 } }
|
|
legionNode.modListGenerated = copyTable(modLists)
|
|
newNode.node = copyTable(modLists)
|
|
end
|
|
newNode.node[1].id = legionNode.id
|
|
newNode.node[2].id = legionNode.id
|
|
else
|
|
if legionNode.modListGenerated then
|
|
newNode.modList = copyTable(legionNode.modListGenerated)
|
|
else
|
|
-- generate modList
|
|
local modList, extra = modLib.parseMod(replaceHelperFunc(legionNode.sd[1], legionNode.sortedStats[1], legionNode.stats[legionNode.sortedStats[1]], 100))
|
|
legionNode.modListGenerated = modList
|
|
newNode.modList = modList
|
|
end
|
|
end
|
|
newNode.divisor = 100
|
|
else
|
|
newNode.modList = legionNode.modList
|
|
if modNode.totalMod then
|
|
newNode.divisor = legionNode.modList[1].value
|
|
end
|
|
end
|
|
break
|
|
end
|
|
end
|
|
if not newNode then
|
|
for _, legionAddition in ipairs(legionAdditions) do
|
|
if legionAddition.id == modNode.id or (totalModIDs[modNode.id] and totalModIDs[modNode.id][legionAddition.id]) then
|
|
newNode = { }
|
|
newNode.id = modNode.id
|
|
if legionAddition.modList then
|
|
newNode.modList = legionAddition.modList
|
|
elseif legionAddition.modListGenerated then
|
|
newNode.modList = legionAddition.modListGenerated
|
|
else
|
|
-- generate modList
|
|
local line = legionAddition.sd[1]
|
|
if modNode.type == "vaal" then
|
|
for key, stat in legionAddition.stats do -- should only be length 1
|
|
line = replaceHelperFunc(line, key, stat, 100)
|
|
end
|
|
end
|
|
local modList, extra = modLib.parseMod(line)
|
|
legionAddition.modListGenerated = modList
|
|
newNode.modList = modList
|
|
end
|
|
if modNode.type == "vaal" then
|
|
newNode.divisor = 100
|
|
elseif modNode.totalMod then
|
|
newNode.divisor = newNode.modList[1].value
|
|
end
|
|
break
|
|
end
|
|
end
|
|
end
|
|
if newNode then
|
|
t_insert(nodes, newNode)
|
|
end
|
|
end
|
|
end
|
|
local output = generateFallbackWeights(nodes, controls.fallbackWeightsList.list[controls.fallbackWeightsList.selIndex])
|
|
local newList = ""
|
|
local weightScalar = 100
|
|
for _, legionNode in ipairs(output) do
|
|
if legionNode.weight1 ~= 0 or (legionNode.weight2 and legionNode.weight2 ~= 0) then
|
|
if #newList > 0 then
|
|
newList = newList .. "\n"
|
|
end
|
|
newList = newList .. legionNode.id .. ", " .. round(legionNode.weight1 * weightScalar, 3) .. ", " .. round((legionNode.weight2 or 0) * weightScalar, 3) .. ", 0"
|
|
end
|
|
end
|
|
updateSearchList(newList, true)
|
|
end
|
|
|
|
controls.fallbackWeightsLabel = new("LabelControl", {"TOPRIGHT", nil, "TOPLEFT"}, {405, 225, 0, 16}, "^7Fallback Weight Mode:")
|
|
local fallbackWeightsList = { }
|
|
for id, stat in pairs(data.powerStatList) do
|
|
if not stat.ignoreForItems and stat.label ~= "Name" then
|
|
t_insert(fallbackWeightsList, {
|
|
label = "Sort by " .. stat.label,
|
|
stat = stat.stat,
|
|
transform = stat.transform,
|
|
})
|
|
end
|
|
end
|
|
controls.fallbackWeightsList = new("DropDownControl", {"LEFT", controls.fallbackWeightsLabel, "RIGHT"}, {10, 0, 200, 18}, fallbackWeightsList, function(index)
|
|
timelessData.fallbackWeightMode.idx = index
|
|
end)
|
|
controls.fallbackWeightsList.selIndex = timelessData.fallbackWeightMode.idx or 1
|
|
controls.fallbackWeightsButton = new("ButtonControl", {"LEFT", controls.fallbackWeightsList, "RIGHT"}, {5, 0, 66, 18}, "Generate", function()
|
|
setupFallbackWeights()
|
|
controls.searchListFallbackButton.label = "^4Fallback Nodes"
|
|
end)
|
|
controls.fallbackWeightsButton.tooltipFunc = function(tooltip, mode, index, value)
|
|
tooltip:Clear()
|
|
tooltip:AddLine(16, "^7Click this button to generate new fallback node weights, replacing your old ones.")
|
|
end
|
|
controls.totalMinimumWeightLabel = new("LabelControl", {"TOPRIGHT", nil, "TOPLEFT"}, {405, 250, 0, 16}, "^7Total Minimum Weight:")
|
|
controls.totalMinimumWeight = new("EditControl", {"LEFT", controls.totalMinimumWeightLabel, "RIGHT"}, {10, 0, 60, 18}, "", nil, "%D", nil, function(val)
|
|
local num = tonumber(val)
|
|
timelessData.totalMinimumWeight = num or nil
|
|
self.build.modFlag = true
|
|
end)
|
|
controls.totalMinimumWeight.tooltipFunc = function(tooltip, mode, index, value)
|
|
tooltip:Clear()
|
|
tooltip:AddLine(16, "^7Optional: Only show results where total weight meets or exceeds this value.")
|
|
end
|
|
|
|
|
|
controls.searchListButton = new("ButtonControl", {"TOPLEFT", nil, "TOPLEFT"}, {12, 250, 106, 20}, "^7Desired Nodes", function()
|
|
if controls.searchListFallback.shown then
|
|
controls.searchListFallback.shown = false
|
|
controls.searchListFallback.enabled = false
|
|
controls.searchList.shown = true
|
|
controls.searchList.enabled = true
|
|
end
|
|
end)
|
|
controls.searchListButton.tooltipFunc = function(tooltip, mode, index, value)
|
|
tooltip:Clear()
|
|
tooltip:AddLine(16, "^7This contains a list of your desired nodes along with their primary, secondary, and minimum weights.")
|
|
tooltip:AddLine(16, "^7This list can be updated manually or by selecting the node you want to update via the search dropdown list and then moving the node weight sliders.")
|
|
end
|
|
controls.searchListButton.locked = function() return controls.searchList.shown end
|
|
controls.searchListFallbackButton = new("ButtonControl", {"LEFT", controls.searchListButton, "RIGHT"}, {5, 0, 110, 20}, "^7Fallback Nodes", function()
|
|
controls.searchList.shown = false
|
|
controls.searchList.enabled = false
|
|
controls.searchListFallback.shown = true
|
|
controls.searchListFallback.enabled = true
|
|
controls.searchListFallbackButton.label = "^7Fallback Nodes"
|
|
end)
|
|
controls.searchListFallbackButton.tooltipFunc = function(tooltip, mode, index, value)
|
|
tooltip:Clear()
|
|
tooltip:AddLine(16, "^7This contains a list of your fallback nodes along with their primary, secondary, and minimum weights.")
|
|
tooltip:AddLine(16, "^7This list can be updated manually or by selecting the node you want to update via the search dropdown list and then moving the node weight sliders.")
|
|
tooltip:AddLine(16, "^7Fallback node weights are only used when no matching entry exists in the desired nodes list, allowing you to override or disable specific automatic weights.")
|
|
tooltip:AddLine(16, "^7Fallback node weights typically contain automatically generated stat weights based on your current build.")
|
|
tooltip:AddLine(16, "^7Any manual changes made to your fallback nodes are lost when you click the generate button, as it completely replaces them.")
|
|
end
|
|
controls.searchListFallbackButton.locked = function() return controls.searchListFallback.shown end
|
|
controls.searchList = new("EditControl", {"TOPLEFT", nil, "TOPLEFT"}, {12, 275, 438, 200}, timelessData.searchList, nil, "^%C\t\n", nil, function(value)
|
|
timelessData.searchList = value
|
|
parseSearchList(0, false)
|
|
self.build.modFlag = true
|
|
end, 16, true)
|
|
controls.searchList.shown = true
|
|
controls.searchList.enabled = true
|
|
controls.searchList:SetText(timelessData.searchList and timelessData.searchList or "")
|
|
controls.searchListFallback = new("EditControl", {"TOPLEFT", nil, "TOPLEFT"}, {12, 275, 438, 200}, timelessData.searchListFallback, nil, "^%C\t\n", nil, function(value)
|
|
timelessData.searchListFallback = value
|
|
parseSearchList(0, true)
|
|
self.build.modFlag = true
|
|
end, 16, true)
|
|
controls.searchListFallback.shown = false
|
|
controls.searchListFallback.enabled = false
|
|
controls.searchListFallback:SetText(timelessData.searchListFallback and timelessData.searchListFallback or "")
|
|
|
|
controls.searchResultsLabel = new("LabelControl", { "TOPLEFT", nil, "TOPRIGHT" }, { -390, 250, 0, 16 }, "^7Results:")
|
|
controls.searchResults = new("TimelessJewelListControl", { "TOPLEFT", nil, "TOPRIGHT" }, { -450, 275, 438, 200 }, self.build)
|
|
controls.searchTradeLeagueSelect = new("DropDownControl", { "BOTTOMRIGHT", controls.searchResults, "TOPRIGHT" }, { -175, -5, 140, 20 }, nil, function(_, value)
|
|
self.timelessJewelLeagueSelect = value
|
|
end)
|
|
self.tradeQueryRequests = new("TradeQueryRequests")
|
|
controls.msg = new("LabelControl", nil, { -280, 5, 0, 16 }, "")
|
|
if #self.tradeLeaguesList > 0 then
|
|
controls.searchTradeLeagueSelect:SetList(self.tradeLeaguesList)
|
|
-- restore the last league selected
|
|
for i, league in ipairs(self.tradeLeaguesList) do
|
|
if league == self.timelessJewelLeagueSelect then
|
|
controls.searchTradeLeagueSelect:SetSel(i)
|
|
break
|
|
end
|
|
end
|
|
else
|
|
self.tradeQueryRequests:FetchLeagues("pc", function(leagues, errMsg)
|
|
if errMsg then
|
|
controls.msg.label = "^1Error fetching league list, default league will be used\n"..errMsg.."^7"
|
|
return
|
|
end
|
|
local tempLeagueTable = { }
|
|
for _, league in ipairs(leagues) do
|
|
if league ~= "Standard" and league ~= "Ruthless" and league ~= "Hardcore" and league ~= "Hardcore Ruthless" then
|
|
if not (league:find("Hardcore") or league:find("Ruthless")) then
|
|
-- set the dynamic, base league name to index 1 to sync league shown in dropdown on load with default/old behavior of copy trade url
|
|
t_insert(tempLeagueTable, league)
|
|
for _, val in ipairs(self.tradeLeaguesList) do
|
|
t_insert(tempLeagueTable, val)
|
|
end
|
|
self.tradeLeaguesList = copyTable(tempLeagueTable)
|
|
else
|
|
t_insert(self.tradeLeaguesList, league)
|
|
end
|
|
end
|
|
end
|
|
t_insert(self.tradeLeaguesList, "Standard")
|
|
t_insert(self.tradeLeaguesList, "Hardcore")
|
|
t_insert(self.tradeLeaguesList, "Ruthless")
|
|
t_insert(self.tradeLeaguesList, "Hardcore Ruthless")
|
|
controls.searchTradeLeagueSelect:SetList(self.tradeLeaguesList)
|
|
end)
|
|
end
|
|
controls.searchTradeButton = new("ButtonControl", { "BOTTOMRIGHT", controls.searchResults, "TOPRIGHT" }, { 0, -5, 170, 20 }, "Copy Trade URL", function()
|
|
local seedTrades = {}
|
|
local startRow = controls.searchResults.selIndex or 1
|
|
local endRow = startRow + m_floor(10 / ((timelessData.sharedResults.conqueror.id == 1) and 3 or 1))
|
|
if controls.searchResults.highlightIndex then
|
|
startRow = m_min(controls.searchResults.selIndex, controls.searchResults.highlightIndex)
|
|
endRow = m_max(controls.searchResults.selIndex, controls.searchResults.highlightIndex)
|
|
end
|
|
|
|
local seedCount = m_min(#timelessData.searchResults - startRow, endRow - startRow) + 1
|
|
-- update if not highlighted already
|
|
|
|
local prevSearch = controls.searchTradeButton.lastSearch
|
|
if prevSearch and prevSearch[1] == startRow and prevSearch[2] == seedCount then
|
|
startRow = endRow + 1
|
|
if (startRow > #timelessData.searchResults) then
|
|
return
|
|
end
|
|
seedCount = m_min(#timelessData.searchResults - startRow + 1, seedCount)
|
|
endRow = startRow + seedCount - 1
|
|
end
|
|
controls.searchResults.selIndex = startRow
|
|
controls.searchResults.highlightIndex = endRow
|
|
|
|
controls.searchTradeButton.lastSearch = {startRow, seedCount}
|
|
|
|
for i = startRow, startRow + seedCount - 1 do
|
|
local result = timelessData.searchResults[i]
|
|
|
|
local conquerorKeystoneTradeIds = data.timelessJewelTradeIDs[timelessData.jewelType.id].keystone
|
|
local conquerorTradeIds = { conquerorKeystoneTradeIds[1], conquerorKeystoneTradeIds[2], conquerorKeystoneTradeIds[3] }
|
|
if timelessData.sharedResults.conqueror.id > 1 then
|
|
conquerorTradeIds = { conquerorKeystoneTradeIds[timelessData.sharedResults.conqueror.id - 1] }
|
|
end
|
|
|
|
for _, tradeId in ipairs(conquerorTradeIds) do
|
|
t_insert(seedTrades, {
|
|
id = tradeId,
|
|
value = {
|
|
min = result.seed,
|
|
max = result.seed
|
|
}
|
|
})
|
|
end
|
|
end
|
|
|
|
local search = {
|
|
query = {
|
|
status = {
|
|
option = "online"
|
|
},
|
|
stats = {
|
|
{
|
|
filters = seedTrades,
|
|
type = "count",
|
|
value = {
|
|
min = 1
|
|
}
|
|
}
|
|
}
|
|
},
|
|
sort = {
|
|
price = "asc"
|
|
}
|
|
}
|
|
|
|
if data.timelessJewelTradeIDs[timelessData.jewelType.id].devotion ~= nil then
|
|
local devotionFilters = {}
|
|
if timelessData.sharedResults.devotionVariant1.id > 1 then
|
|
t_insert(devotionFilters, { id = data.timelessJewelTradeIDs[timelessData.jewelType.id].devotion[timelessData.sharedResults.devotionVariant1.id - 1] })
|
|
end
|
|
if timelessData.sharedResults.devotionVariant2.id > 1 then
|
|
t_insert(devotionFilters, { id = data.timelessJewelTradeIDs[timelessData.jewelType.id].devotion[timelessData.sharedResults.devotionVariant2.id - 1] })
|
|
end
|
|
if next(devotionFilters) then
|
|
t_insert(search.query.stats, {
|
|
filters = devotionFilters,
|
|
type = "and"
|
|
})
|
|
end
|
|
end
|
|
|
|
-- if the league was not selected via dropdown, then default to the first league in the dropdown or "" if the leagues could not be read
|
|
self.timelessJewelLeagueSelect = self.timelessJewelLeagueSelect or (self.tradeLeaguesList and #self.tradeLeaguesList > 0 and self.tradeLeaguesList[1]) or ""
|
|
|
|
Copy("https://www.pathofexile.com/trade/search/"..(self.timelessJewelLeagueSelect).."/?q=" .. (s_gsub(dkjson.encode(search), "[^a-zA-Z0-9]", function(a)
|
|
return s_format("%%%02X", s_byte(a))
|
|
end)))
|
|
|
|
controls.searchTradeButton.label = "Copy Next Trade URL"
|
|
end)
|
|
controls.searchTradeButton.enabled = timelessData.searchResults and #timelessData.searchResults > 0
|
|
controls.searchTradeButton.tooltipFunc = function(tooltip, mode, index, value)
|
|
tooltip:Clear()
|
|
tooltip:AddLine(16, "^7Click to generate and copy a trade URL for searching for jewels in this list.")
|
|
tooltip:AddLine(16, "^7Paste the URL in a web browser to search.")
|
|
tooltip:AddLine(16, "")
|
|
tooltip:AddLine(16, "^7You can click to select a row so that search begins from there.")
|
|
tooltip:AddLine(16, "^7After selecting a row You can also shift+click on another row to select a range of rows to search.")
|
|
end
|
|
|
|
local width = 80
|
|
local divider = 10
|
|
local buttons = 3
|
|
local totalWidth = m_floor(width * buttons + divider * (buttons - 1))
|
|
local buttonX = -totalWidth / 2 + width / 2
|
|
|
|
controls.searchButton = new("ButtonControl", nil, {buttonX, 485, width, 20}, "Search", function()
|
|
if treeData.nodes[timelessData.jewelSocket.id] and treeData.nodes[timelessData.jewelSocket.id].isJewelSocket then
|
|
local radiusNodes = treeData.nodes[timelessData.jewelSocket.id].nodesInRadius[3] -- large radius around timelessData.jewelSocket.id
|
|
local allocatedNodes = { }
|
|
local unAllocatedNodesDistance = { }
|
|
local targetNodes = { }
|
|
local targetSmallNodes = { ["attributeSmalls"] = 0, ["otherSmalls"] = 0 }
|
|
local desiredNodes = { }
|
|
local minimumWeights = { }
|
|
local resultNodes = { }
|
|
local rootNodes = { }
|
|
local desiredIdx = 0
|
|
local searchListCombinedTbl = { }
|
|
local searchListNodeFound = { }
|
|
for _, curRow in ipairs(searchListTbl) do
|
|
searchListNodeFound[curRow[1]] = true
|
|
searchListCombinedTbl[#searchListCombinedTbl + 1] = copyTable(curRow)
|
|
end
|
|
for _, curRow in ipairs(searchListFallbackTbl) do
|
|
if not searchListNodeFound[curRow[1]] then
|
|
searchListCombinedTbl[#searchListCombinedTbl + 1] = copyTable(curRow)
|
|
end
|
|
end
|
|
for _, desiredNode in ipairs(searchListCombinedTbl) do
|
|
if #desiredNode > 1 then
|
|
local displayName = nil
|
|
local singleStat = false
|
|
if totalMods[timelessData.jewelType.id] and desiredNode[1] == "total_" .. totalMods[timelessData.jewelType.id]:lower() then
|
|
desiredNode[1] = "totalStat"
|
|
displayName = totalMods[timelessData.jewelType.id]
|
|
end
|
|
if displayName == nil then
|
|
for _, legionNode in ipairs(legionNodes) do
|
|
if legionNode.id == desiredNode[1] then
|
|
-- non-vaal replacements only support one nodeWeight
|
|
if timelessData.jewelType.id > 1 then
|
|
singleStat = true
|
|
end
|
|
displayName = t_concat(legionNode.sd, " + ")
|
|
break
|
|
end
|
|
end
|
|
end
|
|
if displayName == nil then
|
|
for _, legionAddition in ipairs(legionAdditions) do
|
|
if legionAddition.id == desiredNode[1] then
|
|
-- additions only support one nodeWeight
|
|
singleStat = true
|
|
displayName = t_concat(legionAddition.sd, " + ")
|
|
break
|
|
end
|
|
end
|
|
end
|
|
if displayName ~= nil then
|
|
for i, val in ipairs(desiredNode) do
|
|
if singleStat and i == 2 then
|
|
desiredNode[2] = tonumber(desiredNode[2]) or tonumber(desiredNode[3]) or 1
|
|
end
|
|
if val == "required" then
|
|
desiredNode[i] = (singleStat and i == 2) and desiredNode[2] or 0
|
|
if desiredNode[4] == nil or desiredNode[4] < 0.001 then
|
|
desiredNode[4] = 0.001
|
|
end
|
|
end
|
|
end
|
|
if desiredNode[4] ~= nil and tonumber(desiredNode[4]) > 0 then
|
|
t_insert(minimumWeights, { reqNode = desiredNode[1], weight = tonumber(desiredNode[4]) })
|
|
end
|
|
-- if we're protecting a node and the number of protected nodes is less than the total allocated in radius and the total desired nodes is less than the total allocated in radius
|
|
-- these constraints avoid a blank result in the case where you set a min weight of 1 onto a non devotion stat with zero unprotected nodes
|
|
if protectedNodesCount > 0 and protectedNodesCount < self.allocatedNodesInRadiusCount and (#searchListCombinedTbl < self.allocatedNodesInRadiusCount) then
|
|
t_insert(minimumWeights, { reqNode = desiredNode[1], weight = 1 })
|
|
end
|
|
if desiredNodes[desiredNode[1]] then
|
|
desiredNodes[desiredNode[1]] = {
|
|
nodeWeight = tonumber(desiredNode[2]) or 0.001,
|
|
nodeWeight2 = tonumber(desiredNode[3]) or 0.001,
|
|
displayName = displayName or desiredNode[1],
|
|
desiredIdx = desiredNodes[desiredNode[1]].desiredIdx
|
|
}
|
|
else
|
|
desiredIdx = desiredIdx + 1
|
|
desiredNodes[desiredNode[1]] = {
|
|
nodeWeight = tonumber(desiredNode[2]) or 0.001,
|
|
nodeWeight2 = tonumber(desiredNode[3]) or 0.001,
|
|
displayName = displayName or desiredNode[1],
|
|
desiredIdx = desiredIdx
|
|
}
|
|
end
|
|
end
|
|
end
|
|
end
|
|
wipeTable(searchListCombinedTbl)
|
|
for _, class in pairs(treeData.classes) do
|
|
rootNodes[class.startNodeId] = true
|
|
end
|
|
if controls.socketFilter.state then
|
|
timelessData.socketFilterDistance = timelessData.socketFilterDistance or 0
|
|
for nodeId in pairs(radiusNodes) do
|
|
allocatedNodes[nodeId] = self.build.calcsTab.mainEnv.grantedPassives[nodeId] ~= nil or self.build.spec.allocNodes[nodeId] ~= nil
|
|
if timelessData.socketFilterDistance > 0 then
|
|
unAllocatedNodesDistance[nodeId] = self.build.spec.nodes[nodeId].pathDist or 1000
|
|
end
|
|
end
|
|
end
|
|
for nodeId in pairs(radiusNodes) do
|
|
if not rootNodes[nodeId]
|
|
and not treeData.nodes[nodeId].isJewelSocket
|
|
and not treeData.nodes[nodeId].isKeystone
|
|
and (not controls.socketFilter.state or allocatedNodes[nodeId] or (timelessData.socketFilterDistance > 0 and unAllocatedNodesDistance[nodeId] <= timelessData.socketFilterDistance)) then
|
|
if (treeData.nodes[nodeId].isNotable or timelessData.jewelType.id == 1) then
|
|
targetNodes[nodeId] = true
|
|
elseif desiredNodes["totalStat"] and not treeData.nodes[nodeId].isNotable then
|
|
if isValueInArray({ "Strength", "Intelligence", "Dexterity" }, treeData.nodes[nodeId].dn) then
|
|
targetSmallNodes.attributeSmalls = targetSmallNodes.attributeSmalls + 1
|
|
else
|
|
targetSmallNodes.otherSmalls = targetSmallNodes.otherSmalls + 1
|
|
end
|
|
end
|
|
end
|
|
end
|
|
local seedWeights = { }
|
|
local seedMultiplier = timelessData.jewelType.id == 5 and 20 or 1 -- Elegant Hubris
|
|
for curSeed = data.timelessJewelSeedMin[timelessData.jewelType.id] * seedMultiplier, data.timelessJewelSeedMax[timelessData.jewelType.id] * seedMultiplier, seedMultiplier do
|
|
seedWeights[curSeed] = 0
|
|
resultNodes[curSeed] = { }
|
|
for targetNode in pairs(targetNodes) do
|
|
local jewelDataTbl = data.readLUT(curSeed, targetNode, timelessData.jewelType.id)
|
|
if not next(jewelDataTbl) then
|
|
ConPrintf("Missing LUT: " .. timelessData.jewelType.label)
|
|
else
|
|
local curNode = nil
|
|
local curNodeId = nil
|
|
if (timelessData.jewelType.id == 4 and isValueInTable(protectedNodes, treeData.nodes[targetNode].dn)) then
|
|
if not desiredNodes["totalStat"] then -- only add if user has not entered their own Devotion to the table
|
|
desiredNodes["totalStat"] = {
|
|
nodeWeight = 0.1, -- keeps total score low to let desired stats decide sort
|
|
nodeWeight2 = 0,
|
|
displayName = "Devotion",
|
|
desiredIdx = desiredIdx + 1
|
|
}
|
|
end
|
|
curNodeId = "totalStat"
|
|
end
|
|
if jewelDataTbl[1] >= data.timelessJewelAdditions and not isValueInTable(protectedNodes, treeData.nodes[targetNode].dn) then -- replace
|
|
curNode = legionNodes[jewelDataTbl[1] + 1 - data.timelessJewelAdditions]
|
|
curNodeId = curNode and legionNodes[jewelDataTbl[1] + 1 - data.timelessJewelAdditions].id or nil
|
|
else -- add
|
|
curNode = legionAdditions[jewelDataTbl[1] + 1]
|
|
curNodeId = curNode and legionAdditions[jewelDataTbl[1] + 1].id or nil
|
|
end
|
|
if desiredNodes["totalStat"] and reverseTotalModIDs[curNodeId] then
|
|
curNodeId = "totalStat"
|
|
end
|
|
if timelessData.jewelType.id == 1 then
|
|
local headerSize = #jewelDataTbl
|
|
if headerSize == 2 or headerSize == 3 then
|
|
if desiredNodes[curNodeId] then
|
|
resultNodes[curSeed][curNodeId] = resultNodes[curSeed][curNodeId] or { targetNodeNames = { }, totalWeight = 0 }
|
|
local statMod1 = curNode.stats[curNode.sortedStats[1]]
|
|
local weight = desiredNodes[curNodeId].nodeWeight * jewelDataTbl[statMod1.index + 1]
|
|
local statMod2 = curNode.stats[curNode.sortedStats[2]]
|
|
if statMod2 then
|
|
weight = weight + desiredNodes[curNodeId].nodeWeight2 * jewelDataTbl[statMod2.index + 1]
|
|
end
|
|
t_insert(resultNodes[curSeed][curNodeId], targetNode)
|
|
t_insert(resultNodes[curSeed][curNodeId].targetNodeNames, treeData.nodes[targetNode].name)
|
|
resultNodes[curSeed][curNodeId].totalWeight = resultNodes[curSeed][curNodeId].totalWeight + weight
|
|
seedWeights[curSeed] = seedWeights[curSeed] + weight
|
|
end
|
|
elseif headerSize == 6 or headerSize == 8 then
|
|
for i, jewelData in ipairs(jewelDataTbl) do
|
|
curNode = legionAdditions[jewelDataTbl[i] + 1]
|
|
curNodeId = curNode and legionAdditions[jewelDataTbl[i] + 1].id or nil
|
|
if i <= (headerSize / 2) then
|
|
if desiredNodes[curNodeId] then
|
|
resultNodes[curSeed][curNodeId] = resultNodes[curSeed][curNodeId] or { targetNodeNames = { }, totalWeight = 0 }
|
|
local weight = desiredNodes[curNodeId].nodeWeight * jewelDataTbl[i + (headerSize / 2)]
|
|
resultNodes[curSeed][curNodeId].totalWeight = resultNodes[curSeed][curNodeId].totalWeight + weight
|
|
t_insert(resultNodes[curSeed][curNodeId], targetNode)
|
|
t_insert(resultNodes[curSeed][curNodeId].targetNodeNames, treeData.nodes[targetNode].name)
|
|
seedWeights[curSeed] = seedWeights[curSeed] + weight
|
|
end
|
|
else
|
|
break
|
|
end
|
|
end
|
|
end
|
|
elseif desiredNodes[curNodeId] then
|
|
resultNodes[curSeed][curNodeId] = resultNodes[curSeed][curNodeId] or { targetNodeNames = { }, totalWeight = 0 }
|
|
resultNodes[curSeed][curNodeId].totalWeight = resultNodes[curSeed][curNodeId].totalWeight + desiredNodes[curNodeId].nodeWeight
|
|
t_insert(resultNodes[curSeed][curNodeId], targetNode)
|
|
t_insert(resultNodes[curSeed][curNodeId].targetNodeNames, treeData.nodes[targetNode].name)
|
|
seedWeights[curSeed] = seedWeights[curSeed] + desiredNodes[curNodeId].nodeWeight
|
|
end
|
|
end
|
|
end
|
|
if desiredNodes["totalStat"] then
|
|
resultNodes[curSeed]["totalStat"] = resultNodes[curSeed]["totalStat"] or { targetNodeNames = { }, totalWeight = 0 }
|
|
if timelessData.jewelType.id == 4 then -- Militant Faith
|
|
local addedWeight = desiredNodes["totalStat"].nodeWeight * (5 * targetSmallNodes.otherSmalls + 10 * targetSmallNodes.attributeSmalls)
|
|
addedWeight = addedWeight + resultNodes[curSeed]["totalStat"].totalWeight * 4
|
|
resultNodes[curSeed]["totalStat"].totalWeight = resultNodes[curSeed]["totalStat"].totalWeight + addedWeight
|
|
seedWeights[curSeed] = seedWeights[curSeed] + addedWeight
|
|
else
|
|
local addedWeight = desiredNodes["totalStat"].nodeWeight * (4 * targetSmallNodes.otherSmalls + 2 * targetSmallNodes.attributeSmalls)
|
|
addedWeight = addedWeight + resultNodes[curSeed]["totalStat"].totalWeight * 19
|
|
resultNodes[curSeed]["totalStat"].totalWeight = resultNodes[curSeed]["totalStat"].totalWeight + addedWeight
|
|
seedWeights[curSeed] = seedWeights[curSeed] + addedWeight
|
|
end
|
|
end
|
|
-- check minimum weights
|
|
for _, val in ipairs(minimumWeights) do
|
|
if (resultNodes[curSeed][val.reqNode] and resultNodes[curSeed][val.reqNode].totalWeight or 0) < val.weight then
|
|
resultNodes[curSeed] = nil
|
|
break
|
|
end
|
|
end
|
|
end
|
|
wipeTable(timelessData.searchResults)
|
|
wipeTable(timelessData.sharedResults)
|
|
timelessData.sharedResults.type = timelessData.jewelType
|
|
timelessData.sharedResults.conqueror = timelessData.conquerorType
|
|
timelessData.sharedResults.devotionVariant1 = devotionVariants[timelessData.devotionVariant1]
|
|
timelessData.sharedResults.devotionVariant2 = devotionVariants[timelessData.devotionVariant2]
|
|
timelessData.sharedResults.socket = timelessData.jewelSocket
|
|
timelessData.sharedResults.desiredNodes = desiredNodes
|
|
local function formatSearchValue(input)
|
|
local matchPattern1 = " 0"
|
|
local replacePattern1 = " "
|
|
local matchPattern2 = ".0 "
|
|
local replacePattern2 = " "
|
|
local matchPattern3 = " %."
|
|
local replacePattern3 = "0."
|
|
local matchPattern4 = "%.([0-9])0"
|
|
local replacePattern4 = ".%1 "
|
|
return (" " .. s_format("%006.2f", input))
|
|
:gsub(matchPattern1, replacePattern1):gsub(matchPattern1, replacePattern1)
|
|
:gsub(matchPattern2, replacePattern2):gsub(matchPattern2, replacePattern2)
|
|
:gsub(matchPattern3, replacePattern3)
|
|
:gsub(matchPattern4, replacePattern4)
|
|
end
|
|
local searchResultsIdx = 1
|
|
for seedMatch, seedData in pairs(resultNodes) do
|
|
-- filter out the results so that only the ones that beat the total minimum weight parameter remain in search results
|
|
local passesMin = (not timelessData.totalMinimumWeight) or (seedWeights[seedMatch] >= timelessData.totalMinimumWeight)
|
|
if seedWeights[seedMatch] > 0 and passesMin then
|
|
timelessData.searchResults[searchResultsIdx] = { label = seedMatch .. ":" }
|
|
if timelessData.jewelType.id == 1 or timelessData.jewelType.id == 3 then
|
|
-- Glorious Vanity [100-8000], Brutal Restraint [500-8000]
|
|
if seedMatch < 1000 then
|
|
timelessData.searchResults[searchResultsIdx].label = " " .. timelessData.searchResults[searchResultsIdx].label
|
|
end
|
|
elseif timelessData.jewelType.id == 4 then
|
|
-- Militant Faith [2000-10000]
|
|
if seedMatch < 10000 then
|
|
timelessData.searchResults[searchResultsIdx].label = " " .. timelessData.searchResults[searchResultsIdx].label
|
|
end
|
|
else
|
|
-- Elegant Hubris [2000-160000]
|
|
if seedMatch < 10000 then
|
|
timelessData.searchResults[searchResultsIdx].label = " " .. timelessData.searchResults[searchResultsIdx].label
|
|
elseif seedMatch < 100000 then
|
|
timelessData.searchResults[searchResultsIdx].label = " " .. timelessData.searchResults[searchResultsIdx].label
|
|
end
|
|
end
|
|
local sortedNodeArray = { }
|
|
for legionId, desiredNode in pairs(desiredNodes) do
|
|
if seedData[legionId] then
|
|
if desiredNode.desiredIdx == 8 then
|
|
sortedNodeArray[8] = " ..."
|
|
elseif desiredNode.desiredIdx < 8 then
|
|
sortedNodeArray[desiredNode.desiredIdx] = formatSearchValue(seedData[legionId].totalWeight)
|
|
end
|
|
timelessData.searchResults[searchResultsIdx][legionId] = timelessData.searchResults[searchResultsIdx][legionId] or { }
|
|
timelessData.searchResults[searchResultsIdx][legionId].targetNodeNames = seedData[legionId].targetNodeNames
|
|
elseif desiredNode.desiredIdx < 8 then
|
|
sortedNodeArray[desiredNode.desiredIdx] = " 0 "
|
|
end
|
|
end
|
|
timelessData.searchResults[searchResultsIdx].label = timelessData.searchResults[searchResultsIdx].label .. t_concat(sortedNodeArray)
|
|
timelessData.searchResults[searchResultsIdx].seed = seedMatch
|
|
timelessData.searchResults[searchResultsIdx].total = seedWeights[seedMatch]
|
|
searchResultsIdx = searchResultsIdx + 1
|
|
end
|
|
end
|
|
t_sort(timelessData.searchResults, function(a, b) return a.total > b.total end)
|
|
controls.searchTradeButton.enabled = timelessData.searchResults and #timelessData.searchResults > 0
|
|
controls.searchTradeButton.lastSearch = nil
|
|
controls.searchTradeButton.label = "Copy Trade URL"
|
|
controls.searchResults.highlightIndex = nil
|
|
controls.searchResults.selIndex = 1
|
|
end
|
|
end)
|
|
controls.resetButton = new("ButtonControl", nil, {buttonX + (width + divider), 485, width, 20}, "Reset", function()
|
|
updateSearchList("", true)
|
|
updateSearchList("", false)
|
|
wipeTable(timelessData.searchResults)
|
|
controls.searchTradeButton.enabled = false
|
|
clearProtected()
|
|
end)
|
|
controls.closeButton = new("ButtonControl", nil, {buttonX + (width + divider) * 2, 485, width, 20}, "Cancel", function()
|
|
main:ClosePopup()
|
|
end)
|
|
|
|
main:OpenPopup(910, 517, "Find a Timeless Jewel", controls)
|
|
end
|