From e6e88f59c469add6458533f45c800dce12169a9f Mon Sep 17 00:00:00 2001 From: Riley Maersch Date: Sun, 15 Mar 2026 18:30:21 -0500 Subject: [PATCH 1/3] [ISSUE-9451] Loadout Management * Adds Loadout management replacing the single New Loadout option in the loadout dropdown. This new Manage option opens a ListControl menu for creating new, copying, renaming and deleting whole loadouts. This does not change the rules around managing the names for each set that's part of a loadout. * Multiple functions that existed in the list controls were refactored to be functions that were part of the respective tab class. ie: Renaming is now a function in ItemsTab as a class function instead of that logic living in the list control * Several refactors around builds were pulled out of creating the list into functions so they could be reused or called elsewhere. --- src/Classes/BuildSetListControl.lua | 144 +++++++++++++ src/Classes/ConfigSetListControl.lua | 8 +- src/Classes/ConfigTab.lua | 21 +- src/Classes/ItemSetListControl.lua | 7 +- src/Classes/ItemsTab.lua | 14 +- src/Classes/PassiveSpecListControl.lua | 10 +- src/Classes/SkillSetListControl.lua | 18 +- src/Classes/SkillsTab.lua | 29 ++- src/Classes/TreeTab.lua | 18 +- src/Modules/Build.lua | 268 ++++++++++++++++--------- 10 files changed, 389 insertions(+), 148 deletions(-) create mode 100644 src/Classes/BuildSetListControl.lua diff --git a/src/Classes/BuildSetListControl.lua b/src/Classes/BuildSetListControl.lua new file mode 100644 index 0000000000..ac12422e02 --- /dev/null +++ b/src/Classes/BuildSetListControl.lua @@ -0,0 +1,144 @@ +-- Path of Building +-- +-- Class: Build Set List +-- Build set list control. +-- +local t_insert = table.insert +local t_remove = table.remove +local m_max = math.max +local s_format = string.format + +local BuildSetListClass = newClass("BuildSetListControl", "ListControl", function(self, anchor, rect, buildMode, treeTab) + self.ListControl(anchor, rect, 16, "VERTICAL", true, buildMode.treeTab.specList) + self.buildMode = buildMode + self.controls.copy = new("ButtonControl", {"BOTTOMLEFT",self,"TOP"}, {2, -4, 60, 18}, "Copy", function() + local build = buildMode:GetLoadoutByName(self.selValue.title) + self:CopyLoadoutClick(build) + end) + self.controls.copy.enabled = function() + return self.selValue ~= nil + end + self.controls.delete = new("ButtonControl", {"LEFT",self.controls.copy,"RIGHT"}, {4, 0, 60, 18}, "Delete", function() + self:OnSelDelete(self.selIndex, self.selValue) + end) + self.controls.delete.enabled = function() + return self.selValue ~= nil and #self.list > 1 + end + self.controls.rename = new("ButtonControl", {"BOTTOMRIGHT",self,"TOP"}, {-2, -4, 60, 18}, "Rename", function() + self:RenameLoadout(self.selValue) + end) + self.controls.rename.enabled = function() + return self.selValue ~= nil + end + self.controls.new = new("ButtonControl", {"RIGHT",self.controls.rename,"LEFT"}, {-4, 0, 60, 18}, "New", function() + self:NewLoadout() + end) +end) + +function BuildSetListClass:RenameLoadout(spec, addOnName) + local controls = { } + local specName = spec.title or "Default" + controls.label = new("LabelControl", nil, {0, 20, 0, 16}, "^7Enter name for this loadout:") + controls.edit = new("EditControl", nil, {0, 40, 350, 20}, specName, nil, nil, 100, function(buf) + controls.save.enabled = buf:match("%S") + end) + controls.save = new("ButtonControl", nil, {-45, 70, 80, 20}, "Save", function() + local newTitle = controls.edit.buf + self.buildMode:RenameLoadout(specName, newTitle, function() + self.buildMode:SyncLoadouts() + self.buildMode.modFlag = true + end) + if addOnName then + self.selIndex = self.buildMode.treeTab.specListIdToOrderId[spec.id] + self.selValue = spec + end + main:ClosePopup() + end) + controls.save.enabled = false + controls.cancel = new("ButtonControl", nil, {45, 70, 80, 20}, "Cancel", function() + main:ClosePopup() + end) + main:OpenPopup(370, 100, specName and "Rename Loadout" or "Set Name", controls, "save", "edit", "cancel") +end + +function BuildSetListClass:CopyLoadoutClick(build) + local controls = { } + local buildName = self.buildMode.treeTab.specList[build.specId].title + controls.label = new("LabelControl", nil, {0, 20, 0, 16}, "^7Enter name for this loadout:") + controls.edit = new("EditControl", nil, {0, 40, 350, 20}, buildName, nil, nil, 100, function(buf) + controls.save.enabled = buf:match("%S") + end) + controls.save = new("ButtonControl", nil, {-45, 70, 80, 20}, "Save", function() + local newBuildName = controls.edit.buf + local newSpec = self.buildMode:CopyLoadout(build.specId, build.itemSetId, build.skillSetId, build.configSetId, newBuildName) + self.buildMode:SyncLoadouts() + self.buildMode.modFlag = true + self.buildMode.controls.buildLoadouts:SetSel(newSpec.id + 1) + main:ClosePopup() + end) + controls.save.enabled = false + controls.cancel = new("ButtonControl", nil, {45, 70, 80, 20}, "Cancel", function() + main:ClosePopup() + end) + main:OpenPopup(370, 100, buildName and "Rename" or "Set Name", controls, "save", "edit", "cancel") +end + +function BuildSetListClass:NewLoadout() + local controls = { } + controls.label = new("LabelControl", nil, {0, 20, 0, 16}, "^7Enter name for this loadout:") + controls.edit = new("EditControl", nil, {0, 40, 350, 20}, "New Loadout", nil, nil, 100, function(buf) + controls.save.enabled = buf:match("%S") + end) + controls.save = new("ButtonControl", nil, {-45, 70, 80, 20}, "Save", function() + self.buildMode:NewLoadout(controls.edit.buf, function() + self.buildMode:SyncLoadouts() + self.buildMode.modFlag = true + self.buildMode.controls.buildLoadouts:SetSel(1) + main:ClosePopup() + end) + end) + controls.save.enabled = false + controls.cancel = new("ButtonControl", nil, {45, 70, 80, 20}, "Cancel", function() + main:ClosePopup() + end) + main:OpenPopup(370, 100, "Set Name", controls, "save", "edit", "cancel") +end + +function BuildSetListClass:GetRowValue(column, index, spec) + if column == 1 then + local used = spec:CountAllocNodes() + return (spec.treeVersion ~= latestTreeVersion and ("["..treeVersions[spec.treeVersion].display.."] ") or "") + .. (spec.title or "Default") + .. " (" .. (spec.curAscendClassName ~= "None" and spec.curAscendClassName or spec.curClassName) .. ", " .. used .. " points)" + .. (index == self.buildMode.treeTab.activeSpec and " ^9(Current)" or "") + end +end + +function BuildSetListClass:OnOrderChange() + self.buildMode.modFlag = true +end + +function BuildSetListClass:OnSelClick(index, spec, doubleClick) + if doubleClick and index ~= self.buildMode.treeTab.activeSpec then + self.buildMode.controls.buildLoadouts:SetSel(index + 1) + end +end + +function BuildSetListClass:OnSelDelete(index, spec) + if #self.list > 1 then + main:OpenConfirmPopup("Delete Loadout", "Are you sure you want to delete '"..(spec.title or "Default").."'?", "Delete", function() + t_remove(self.list, index) + local nextLoadoutIndex = index == self.buildMode.treeTab.activeSpec and m_max(1, index - 1) or index + local nextLoadout = self.list[nextLoadoutIndex] + self.buildMode:DeleteLoadout(spec.title or "Default", nextLoadout.title or "Default") + self.selIndex = nil + self.selValue = nil + end) + end +end + +function BuildSetListClass:OnSelKeyDown(index, spec, key) + if key == "F2" then + self:RenameLoadout(spec) + end +end diff --git a/src/Classes/ConfigSetListControl.lua b/src/Classes/ConfigSetListControl.lua index c38e809d76..ef2695db59 100644 --- a/src/Classes/ConfigSetListControl.lua +++ b/src/Classes/ConfigSetListControl.lua @@ -11,13 +11,7 @@ local ConfigSetListClass = newClass("ConfigSetListControl", "ListControl", funct self.ListControl(anchor, rect, 16, "VERTICAL", true, configTab.configSetOrderList) self.configTab = configTab self.controls.copy = new("ButtonControl", {"BOTTOMLEFT",self,"TOP"}, {2, -4, 60, 18}, "Copy", function() - local configSet = configTab.configSets[self.selValue] - local newConfigSet = copyTable(configSet) - newConfigSet.id = 1 - while configTab.configSets[newConfigSet.id] do - newConfigSet.id = newConfigSet.id + 1 - end - configTab.configSets[newConfigSet.id] = newConfigSet + local newConfigSet = configTab:CopyConfigSet(self.selValue) self:RenameSet(newConfigSet, true) end) self.controls.copy.enabled = function() diff --git a/src/Classes/ConfigTab.lua b/src/Classes/ConfigTab.lua index 54192fd4c8..8cc1a12dc3 100644 --- a/src/Classes/ConfigTab.lua +++ b/src/Classes/ConfigTab.lua @@ -4,6 +4,7 @@ -- Configuration tab for the current build. -- local t_insert = table.insert +local t_maxn = table.maxn local m_min = math.min local m_max = math.max local m_floor = math.floor @@ -958,10 +959,7 @@ end function ConfigTabClass:NewConfigSet(configSetId, title) local configSet = { id = configSetId, title = title, input = { }, placeholder = { } } if not configSetId then - configSet.id = 1 - while self.configSets[configSet.id] do - configSet.id = configSet.id + 1 - end + configSet.id = #self.configSets + 1 end -- there are default values for input and placeholder that every new config set needs to have for _, varData in ipairs(varList) do @@ -977,8 +975,17 @@ function ConfigTabClass:NewConfigSet(configSetId, title) return configSet end +function ConfigTabClass:CopyConfigSet(configSetId, newConfigSetName) + local configSet = self.configSets[configSetId] + local newConfigSet = copyTable(configSet) + newConfigSet.id = #self.configSets + 1 + newConfigSet.title = newConfigSetName or configSet.title .. " (Copy)" + t_insert(self.configSets, newConfigSet) + return newConfigSet +end + -- Changes the active config set -function ConfigTabClass:SetActiveConfigSet(configSetId, init) +function ConfigTabClass:SetActiveConfigSet(configSetId, init, deferSync) -- Initialize config sets if needed if not self.configSetOrderList[1] then self.configSetOrderList[1] = 1 @@ -1002,5 +1009,7 @@ function ConfigTabClass:SetActiveConfigSet(configSetId, init) self:BuildModList() end self.build.buildFlag = true - self.build:SyncLoadouts() + if not deferSync then + self.build:SyncLoadouts() + end end diff --git a/src/Classes/ItemSetListControl.lua b/src/Classes/ItemSetListControl.lua index 04d87e8537..c2be437b4b 100644 --- a/src/Classes/ItemSetListControl.lua +++ b/src/Classes/ItemSetListControl.lua @@ -12,12 +12,7 @@ local ItemSetListClass = newClass("ItemSetListControl", "ListControl", function( self.ListControl(anchor, rect, 16, "VERTICAL", true, itemsTab.itemSetOrderList) self.itemsTab = itemsTab self.controls.copy = new("ButtonControl", {"BOTTOMLEFT",self,"TOP"}, {2, -4, 60, 18}, "Copy", function() - local newSet = copyTable(itemsTab.itemSets[self.selValue]) - newSet.id = 1 - while itemsTab.itemSets[newSet.id] do - newSet.id = newSet.id + 1 - end - itemsTab.itemSets[newSet.id] = newSet + local newSet = itemsTab:CopyItemSet(self.selValue) self:RenameSet(newSet, true) end) self.controls.copy.enabled = function() diff --git a/src/Classes/ItemsTab.lua b/src/Classes/ItemsTab.lua index aaa060c8c7..fd4884a97f 100644 --- a/src/Classes/ItemsTab.lua +++ b/src/Classes/ItemsTab.lua @@ -1351,8 +1351,16 @@ function ItemsTabClass:NewItemSet(itemSetId) return itemSet end +function ItemsTabClass:CopyItemSet(sourceItemSetId, newItemSetName) + local newSet = copyTable(self.itemSets[sourceItemSetId]) + newSet.id = #self.itemSets + 1 + newSet.title = newItemSetName or newSet.title .. " (Copy)" + self.itemSets[newSet.id] = newSet + return newSet +end + -- Changes the active item set -function ItemsTabClass:SetActiveItemSet(itemSetId) +function ItemsTabClass:SetActiveItemSet(itemSetId, deferSync) local prevSet = self.activeItemSet if not self.itemSets[itemSetId] then itemSetId = self.itemSetOrderList[1] @@ -1377,7 +1385,9 @@ function ItemsTabClass:SetActiveItemSet(itemSetId) end self.build.buildFlag = true self:PopulateSlots() - self.build:SyncLoadouts() + if not deferSync then + self.build:SyncLoadouts() + end end -- Equips the given item in the given item set diff --git a/src/Classes/PassiveSpecListControl.lua b/src/Classes/PassiveSpecListControl.lua index f227764d8f..3f0424dccf 100644 --- a/src/Classes/PassiveSpecListControl.lua +++ b/src/Classes/PassiveSpecListControl.lua @@ -11,11 +11,7 @@ local PassiveSpecListClass = newClass("PassiveSpecListControl", "ListControl", f self.ListControl(anchor, rect, 16, "VERTICAL", true, treeTab.specList) self.treeTab = treeTab self.controls.copy = new("ButtonControl", {"BOTTOMLEFT",self,"TOP"}, {2, -4, 60, 18}, "Copy", function() - local newSpec = new("PassiveSpec", treeTab.build, self.selValue.treeVersion) - newSpec.title = self.selValue.title - newSpec.jewels = copyTable(self.selValue.jewels) - newSpec:RestoreUndoState(self.selValue:CreateUndoState()) - newSpec:BuildClusterJewelGraphs() + local newSpec = treeTab:CopyTree(self.selValue) self:RenameSpec(newSpec, "Copy Tree", true) end) self.controls.copy.enabled = function() @@ -43,7 +39,7 @@ local PassiveSpecListClass = newClass("PassiveSpecListControl", "ListControl", f self:UpdateItemsTabPassiveTreeDropdown() end) -function PassiveSpecListClass:RenameSpec(spec, title, addOnName) +function PassiveSpecListClass:RenameSpec(spec, popupTitle, addOnName) local controls = { } controls.label = new("LabelControl", nil, {0, 20, 0, 16}, "^7Enter name for this passive tree:") controls.edit = new("EditControl", nil, {0, 40, 350, 20}, spec.title, nil, nil, 100, function(buf) @@ -66,7 +62,7 @@ function PassiveSpecListClass:RenameSpec(spec, title, addOnName) main:ClosePopup() end) -- main:OpenPopup(370, 100, spec.title and "Rename" or "Set Name", controls, "save", "edit") - main:OpenPopup(370, 100, title, controls, "save", "edit") + main:OpenPopup(370, 100, popupTitle, controls, "save", "edit") end function PassiveSpecListClass:GetRowValue(column, index, spec) diff --git a/src/Classes/SkillSetListControl.lua b/src/Classes/SkillSetListControl.lua index c8208c476d..af00152d27 100644 --- a/src/Classes/SkillSetListControl.lua +++ b/src/Classes/SkillSetListControl.lua @@ -5,6 +5,7 @@ -- local t_insert = table.insert local t_remove = table.remove +local t_maxn = table.maxn local m_max = math.max local s_format = string.format @@ -12,22 +13,7 @@ local SkillSetListClass = newClass("SkillSetListControl", "ListControl", functio self.ListControl(anchor, rect, 16, "VERTICAL", true, skillsTab.skillSetOrderList) self.skillsTab = skillsTab self.controls.copy = new("ButtonControl", {"BOTTOMLEFT",self,"TOP"}, {2, -4, 60, 18}, "Copy", function() - local skillSet = skillsTab.skillSets[self.selValue] - local newSkillSet = copyTable(skillSet, true) - newSkillSet.socketGroupList = { } - for socketGroupIndex, socketGroup in pairs(skillSet.socketGroupList) do - local newGroup = copyTable(socketGroup, true) - newGroup.gemList = { } - for gemIndex, gem in pairs(socketGroup.gemList) do - newGroup.gemList[gemIndex] = copyTable(gem, true) - end - t_insert(newSkillSet.socketGroupList, newGroup) - end - newSkillSet.id = 1 - while skillsTab.skillSets[newSkillSet.id] do - newSkillSet.id = newSkillSet.id + 1 - end - skillsTab.skillSets[newSkillSet.id] = newSkillSet + local newSkillSet = skillsTab:CopySkillSet(self.selValue) self:RenameSet(newSkillSet, true) end) self.controls.copy.enabled = function() diff --git a/src/Classes/SkillsTab.lua b/src/Classes/SkillsTab.lua index a7dfedcc09..a0b31e4650 100644 --- a/src/Classes/SkillsTab.lua +++ b/src/Classes/SkillsTab.lua @@ -1325,17 +1325,32 @@ end function SkillsTabClass:NewSkillSet(skillSetId) local skillSet = { id = skillSetId, socketGroupList = {} } if not skillSetId then - skillSet.id = 1 - while self.skillSets[skillSet.id] do - skillSet.id = skillSet.id + 1 - end + skillSet.id = t_maxn(self.skillSets) + 1 end self.skillSets[skillSet.id] = skillSet return skillSet end +function SkillsTabClass:CopySkillSet(sourceSkillSetId, newSkillSetName) + local skillSet = self.skillSets[sourceSkillSetId] + local newSkillSet = copyTable(skillSet, true) + newSkillSet.title = newSkillSetName or skillSet.title .. " (Copy)" + newSkillSet.socketGroupList = { } + for socketGroupIndex, socketGroup in pairs(skillSet.socketGroupList) do + local newGroup = copyTable(socketGroup, true) + newGroup.gemList = { } + for gemIndex, gem in pairs(socketGroup.gemList) do + newGroup.gemList[gemIndex] = copyTable(gem, true) + end + t_insert(newSkillSet.socketGroupList, newGroup) + end + newSkillSet.id = #self.skillSets + 1 + self.skillSets[newSkillSet.id] = newSkillSet + return newSkillSet +end + -- Changes the active skill set -function SkillsTabClass:SetActiveSkillSet(skillSetId) +function SkillsTabClass:SetActiveSkillSet(skillSetId, deferSync) -- Initialize skill sets if needed if not self.skillSetOrderList[1] then self.skillSetOrderList[1] = 1 @@ -1357,5 +1372,7 @@ function SkillsTabClass:SetActiveSkillSet(skillSetId) -- set the loadout option to the dummy option since it is now dirty self:SetDisplayGroup(self.socketGroupList[1]) - self.build:SyncLoadouts() + if not deferSync then + self.build:SyncLoadouts() + end end diff --git a/src/Classes/TreeTab.lua b/src/Classes/TreeTab.lua index f596fb4daf..55dd4f8c29 100644 --- a/src/Classes/TreeTab.lua +++ b/src/Classes/TreeTab.lua @@ -10,6 +10,7 @@ local t_insert = table.insert local t_remove = table.remove local t_sort = table.sort local t_concat = table.concat +local t_maxn = table.maxn local m_max = math.max local m_min = math.min local m_floor = math.floor @@ -535,7 +536,7 @@ function TreeTabClass:Save(xml) end end -function TreeTabClass:SetActiveSpec(specId) +function TreeTabClass:SetActiveSpec(specId, deferSync) local prevSpec = self.build.spec self.activeSpec = m_min(specId, #self.specList) local curSpec = self.specList[self.activeSpec] @@ -570,7 +571,9 @@ function TreeTabClass:SetActiveSpec(specId) if self.controls.versionSelect then self.controls.versionSelect:SelByValue(curSpec.treeVersion, 'value') end - self.build:SyncLoadouts() + if not deferSync then + self.build:SyncLoadouts() + end end function TreeTabClass:SetCompareSpec(specId) @@ -641,6 +644,17 @@ function TreeTabClass:OpenSpecManagePopup() }) end +function TreeTabClass:CopyTree(sourceSpecId, newSpecName) + local newSpec = new("PassiveSpec", self.build, self.specList[sourceSpecId].treeVersion) + newSpec.title = newSpecName or self.specList[sourceSpecId].title .. " (Copy)" + newSpec.jewels = copyTable(self.specList[sourceSpecId].jewels) + newSpec:RestoreUndoState(self.specList[sourceSpecId]:CreateUndoState()) + newSpec:BuildClusterJewelGraphs() + newSpec.id = #self.specList + 1 + t_insert(self.specList, newSpec) + return newSpec +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" .. diff --git a/src/Modules/Build.lua b/src/Modules/Build.lua index 7173c2fb4b..60c9b2858e 100644 --- a/src/Modules/Build.lua +++ b/src/Modules/Build.lua @@ -7,6 +7,7 @@ local pairs = pairs local ipairs = ipairs local next = next local t_insert = table.insert +local t_remove = table.remove local t_sort = table.sort local m_min = math.min local m_max = math.max @@ -299,103 +300,12 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin self.controls.buildLoadouts:SetSel(1) return end - if value == "^7^7New Loadout" then - local controls = { } - controls.label = new("LabelControl", nil, {0, 20, 0, 16}, "^7Enter name for this loadout:") - controls.edit = new("EditControl", nil, {0, 40, 350, 20}, "New Loadout", nil, nil, 100, function(buf) - controls.save.enabled = buf:match("%S") - end) - controls.save = new("ButtonControl", nil, {-45, 70, 80, 20}, "Save", function() - local loadout = controls.edit.buf - - local newSpec = new("PassiveSpec", self, latestTreeVersion) - newSpec.title = loadout - t_insert(self.treeTab.specList, newSpec) - - local itemSet = self.itemsTab:NewItemSet(#self.itemsTab.itemSets + 1) - t_insert(self.itemsTab.itemSetOrderList, itemSet.id) - itemSet.title = loadout - - local skillSet = self.skillsTab:NewSkillSet(#self.skillsTab.skillSets + 1) - t_insert(self.skillsTab.skillSetOrderList, skillSet.id) - skillSet.title = loadout - - local configSet = self.configTab:NewConfigSet(#self.configTab.configSets + 1) - t_insert(self.configTab.configSetOrderList, configSet.id) - configSet.title = loadout - - self:SyncLoadouts() - self.modFlag = true - main:ClosePopup() - end) - controls.save.enabled = false - controls.cancel = new("ButtonControl", nil, {45, 70, 80, 20}, "Cancel", function() - main:ClosePopup() - end) - main:OpenPopup(370, 100, "Set Name", controls, "save", "edit", "cancel") - - self.controls.buildLoadouts:SetSel(1) - return - end - - -- item, skill, and config sets have identical structure - -- return id as soon as it's found - local function findSetId(setOrderList, value, sets, setSpecialLinks) - for _, setOrder in ipairs(setOrderList) do - if value == (sets[setOrder].title or "Default") then - return setOrder - else - local linkMatch = string.match(value, "%{(%w+)%}") - if linkMatch then - return setSpecialLinks[linkMatch]["setId"] - end - end - end - return nil + if value == "^7^7Manage" then + self:OpenBuildSetManagePopup() end - -- trees have a different structure with id/name pairs - -- return id as soon as it's found - local function findNamedSetId(treeList, value, setSpecialLinks) - for id, spec in ipairs(treeList) do - if value == spec then - return id - else - local linkMatch = string.match(value, "%{(%w+)%}") - if linkMatch then - return setSpecialLinks[linkMatch]["setId"] - end - end - end - return nil - end - - local oneSkill = self.skillsTab and #self.skillsTab.skillSetOrderList == 1 - local oneItem = self.itemsTab and #self.itemsTab.itemSetOrderList == 1 - local oneConfig = self.configTab and #self.configTab.configSetOrderList == 1 - - local newSpecId = findNamedSetId(self.treeTab:GetSpecList(), value, self.treeListSpecialLinks) - local newItemId = oneItem and 1 or findSetId(self.itemsTab.itemSetOrderList, value, self.itemsTab.itemSets, self.itemListSpecialLinks) - local newSkillId = oneSkill and 1 or findSetId(self.skillsTab.skillSetOrderList, value, self.skillsTab.skillSets, self.skillListSpecialLinks) - local newConfigId = oneConfig and 1 or findSetId(self.configTab.configSetOrderList, value, self.configTab.configSets, self.configListSpecialLinks) - - -- if exact match nor special grouping cannot find setIds, bail - if newSpecId == nil or newItemId == nil or newSkillId == nil or newConfigId == nil then - return - end - - if newSpecId ~= self.treeTab.activeSpec then - self.treeTab:SetActiveSpec(newSpecId) - end - if newItemId ~= self.itemsTab.activeItemSetId then - self.itemsTab:SetActiveItemSet(newItemId) - end - if newSkillId ~= self.skillsTab.activeSkillSetId then - self.skillsTab:SetActiveSkillSet(newSkillId) - end - if newConfigId ~= self.configTab.activeConfigSetId then - self.configTab:SetActiveConfigSet(newConfigId) - end + local loadout = self:GetLoadoutByName(value) + self:SetActiveLoadout(loadout) self.controls.buildLoadouts:SelByValue(value) end) @@ -821,7 +731,7 @@ function buildMode:SyncLoadouts() -- giving the options unique formatting so it can not match with user-created sets t_insert(filteredList, "^7^7-----") - t_insert(filteredList, "^7^7New Loadout") + t_insert(filteredList, "^7^7Manage") t_insert(filteredList, "^7^7Sync") t_insert(filteredList, "^7^7Help >>") @@ -857,6 +767,162 @@ function buildMode:SyncLoadouts() return treeList, itemList, skillList, configList end +function buildMode:NewLoadout(loadoutName, callback --[[function]]) + local newSpec = new("PassiveSpec", self, latestTreeVersion) + local newItemSet = self.itemsTab:NewItemSet(#self.itemsTab.itemSets + 1) + local newSkillSet = self.skillsTab:NewSkillSet(#self.skillsTab.skillSets + 1) + local newConfigSet = self.configTab:NewConfigSet(#self.configTab.configSets + 1) + self:AddLoadout(loadoutName, newSpec, newItemSet, newSkillSet, newConfigSet, callback) +end + +function buildMode:CopyLoadout(copyTreeId, copyItemSetId, copySkillSetId, copyConfigSetId, loadoutName) + local newSpec = self.treeTab:CopyTree(copyTreeId, loadoutName) + local newItemSet = self.itemsTab:CopyItemSet(copyItemSetId, loadoutName) + local newSkillSet = self.skillsTab:CopySkillSet(copySkillSetId, loadoutName) + local newConfigSet = self.configTab:CopyConfigSet(copyConfigSetId, loadoutName) + + t_insert(self.itemsTab.itemSetOrderList, newItemSet.id) + t_insert(self.skillsTab.skillSetOrderList, newSkillSet.id) + t_insert(self.configTab.configSetOrderList, newConfigSet.id) + return newSpec, newItemSet, newSkillSet, newConfigSet +end + +function buildMode:AddLoadout(loadoutName, newSpec, newItemSet, newSkillSet, newConfigSet, callback --[[function]]) + t_insert(self.treeTab.specList, newSpec) + newSpec.title = loadoutName + + t_insert(self.itemsTab.itemSetOrderList, newItemSet.id) + newItemSet.title = loadoutName + + t_insert(self.skillsTab.skillSetOrderList, newSkillSet.id) + newSkillSet.title = loadoutName + + t_insert(self.configTab.configSetOrderList, newConfigSet.id) + newConfigSet.title = loadoutName + + callback() +end + +function buildMode:DeleteLoadout(loadoutName, nextLoadoutName) + local loadout = self:GetLoadoutByName(loadoutName) + local nextLoadout = self:GetLoadoutByName(nextLoadoutName) + if loadout.specId then + self.treeTab.specList[loadout.specId] = nil + self.treeTab:SetActiveSpec(1) + end + if loadout.itemSetId then + self.itemsTab.itemSets[loadout.itemSetId] = nil + t_remove(self.itemsTab.itemSetOrderList, loadout.itemSetId) + end + if loadout.skillSetId then + self.skillsTab.skillSets[loadout.skillSetId] = nil + t_remove(self.skillsTab.skillSetOrderList, loadout.skillSetId) + end + if loadout.configSetId then + self.configTab.configSets[loadout.configSetId] = nil + t_remove(self.configTab.configSetOrderList, loadout.configSetId) + end + + self:SetActiveLoadout(nextLoadout) +end + +function buildMode:RenameLoadout(oldName, newName, callback) + local loadout = self:GetLoadoutByName(oldName) + if loadout.specId then + self.treeTab.specList[loadout.specId].title = newName + self.treeTab.modFlag = true + end + if loadout.itemSetId then + self.itemsTab.itemSets[loadout.itemSetId].title = newName + self.itemsTab.modFlag = true + end + if loadout.skillSetId then + self.skillsTab.skillSets[loadout.skillSetId].title = newName + self.skillsTab.modFlag = true + end + if loadout.configSetId then + self.configTab.configSets[loadout.configSetId].title = newName + self.configTab.modFlag = true + end + callback() +end + +function buildMode:GetLoadoutByName(loadoutName) + -- item, skill, and config sets have identical structure + -- return id as soon as it's found + local function findSetId(setOrderList, value, sets, setSpecialLinks) + for _, setOrder in ipairs(setOrderList) do + if value == (sets[setOrder].title or "Default") then + return setOrder + else + local linkMatch = string.match(value, "%{(%w+)%}") + if linkMatch then + return setSpecialLinks[linkMatch]["setId"] + end + end + end + return nil + end + + -- trees have a different structure with id/name pairs + -- return id as soon as it's found + local function findNamedSetId(treeList, value, setSpecialLinks) + for id, spec in ipairs(treeList) do + if value == spec then + return id + else + local linkMatch = string.match(value, "%{(%w+)%}") + if linkMatch then + return setSpecialLinks[linkMatch]["setId"] + end + end + end + return nil + end + + local oneSkill = self.skillsTab and #self.skillsTab.skillSetOrderList == 1 + local oneItem = self.itemsTab and #self.itemsTab.itemSetOrderList == 1 + local oneConfig = self.configTab and #self.configTab.configSetOrderList == 1 + + local specId = findNamedSetId(self.treeTab:GetSpecList(), loadoutName, self.treeListSpecialLinks) + local itemId = oneItem and 1 or findSetId(self.itemsTab.itemSetOrderList, loadoutName, self.itemsTab.itemSets, self.itemListSpecialLinks) + local skillId = oneSkill and 1 or findSetId(self.skillsTab.skillSetOrderList, loadoutName, self.skillsTab.skillSets, self.skillListSpecialLinks) + local configId = oneConfig and 1 or findSetId(self.configTab.configSetOrderList, loadoutName, self.configTab.configSets, self.configListSpecialLinks) + + + return { + specId = specId, + itemSetId = itemId, + skillSetId = skillId, + configSetId = configId + } +end + +function buildMode:SetActiveLoadout(loadout) + if not loadout then + return + end + + local newSpecId, newItemId, newSkillId, newConfigId = loadout.specId, loadout.itemSetId, loadout.skillSetId, loadout.configSetId + if newSpecId == nil or newItemId == nil or newSkillId == nil or newConfigId == nil then + return + end + + if newSpecId ~= self.treeTab.activeSpec then + self.treeTab:SetActiveSpec(newSpecId, true) + end + if newItemId ~= self.itemsTab.activeItemSetId then + self.itemsTab:SetActiveItemSet(newItemId, true) + end + if newSkillId ~= self.skillsTab.activeSkillSetId then + self.skillsTab:SetActiveSkillSet(newSkillId, true) + end + if newConfigId ~= self.configTab.activeConfigSetId then + self.configTab:SetActiveConfigSet(newConfigId, false, true) + end + self:SyncLoadouts() +end + function buildMode:EstimatePlayerProgress() local PointsUsed, AscUsed, SecondaryAscUsed = self.spec:CountAllocNodes() local extra = self.calcsTab.mainOutput and self.calcsTab.mainOutput.ExtraPoints or 0 @@ -1960,4 +2026,14 @@ function buildMode:SaveDBFile() end end +-- Opens the build set manager +function buildMode:OpenBuildSetManagePopup() + main:OpenPopup(370, 290, "Manage Build Sets", { + new("BuildSetListControl", nil, {0, 50, 350, 200}, self, self.treeTab), + new("ButtonControl", nil, {0, 260, 90, 20}, "Done", function() + main:ClosePopup() + end), + }) +end + return buildMode From 829d956839404ae315210f7841f65ab52d52ac72 Mon Sep 17 00:00:00 2001 From: Riley Maersch Date: Mon, 16 Mar 2026 08:27:19 -0500 Subject: [PATCH 2/3] [ISSUE-9451] Loadout Management * Formatted changed code --- src/Classes/BuildSetListControl.lua | 109 ++++++----- src/Classes/ConfigTab.lua | 12 +- src/Classes/ItemsTab.lua | 16 +- src/Classes/SkillsTab.lua | 30 +-- src/Classes/TreeTab.lua | 16 +- src/Modules/Build.lua | 278 ++++++++++++++-------------- 6 files changed, 235 insertions(+), 226 deletions(-) diff --git a/src/Classes/BuildSetListControl.lua b/src/Classes/BuildSetListControl.lua index ac12422e02..dc450d2a67 100644 --- a/src/Classes/BuildSetListControl.lua +++ b/src/Classes/BuildSetListControl.lua @@ -11,38 +11,40 @@ local s_format = string.format local BuildSetListClass = newClass("BuildSetListControl", "ListControl", function(self, anchor, rect, buildMode, treeTab) self.ListControl(anchor, rect, 16, "VERTICAL", true, buildMode.treeTab.specList) self.buildMode = buildMode - self.controls.copy = new("ButtonControl", {"BOTTOMLEFT",self,"TOP"}, {2, -4, 60, 18}, "Copy", function() + self.controls.copy = new("ButtonControl", { "BOTTOMLEFT", self, "TOP" }, { 2, -4, 60, 18 }, "Copy", function() local build = buildMode:GetLoadoutByName(self.selValue.title) self:CopyLoadoutClick(build) end) self.controls.copy.enabled = function() return self.selValue ~= nil end - self.controls.delete = new("ButtonControl", {"LEFT",self.controls.copy,"RIGHT"}, {4, 0, 60, 18}, "Delete", function() - self:OnSelDelete(self.selIndex, self.selValue) - end) + self.controls.delete = new("ButtonControl", { "LEFT", self.controls.copy, "RIGHT" }, { 4, 0, 60, 18 }, "Delete", + function() + self:OnSelDelete(self.selIndex, self.selValue) + end) self.controls.delete.enabled = function() return self.selValue ~= nil and #self.list > 1 end - self.controls.rename = new("ButtonControl", {"BOTTOMRIGHT",self,"TOP"}, {-2, -4, 60, 18}, "Rename", function() + self.controls.rename = new("ButtonControl", { "BOTTOMRIGHT", self, "TOP" }, { -2, -4, 60, 18 }, "Rename", function() self:RenameLoadout(self.selValue) end) self.controls.rename.enabled = function() return self.selValue ~= nil end - self.controls.new = new("ButtonControl", {"RIGHT",self.controls.rename,"LEFT"}, {-4, 0, 60, 18}, "New", function() - self:NewLoadout() - end) + self.controls.new = new("ButtonControl", { "RIGHT", self.controls.rename, "LEFT" }, { -4, 0, 60, 18 }, "New", + function() + self:NewLoadout() + end) end) function BuildSetListClass:RenameLoadout(spec, addOnName) - local controls = { } + local controls = {} local specName = spec.title or "Default" - controls.label = new("LabelControl", nil, {0, 20, 0, 16}, "^7Enter name for this loadout:") - controls.edit = new("EditControl", nil, {0, 40, 350, 20}, specName, nil, nil, 100, function(buf) + controls.label = new("LabelControl", nil, { 0, 20, 0, 16 }, "^7Enter name for this loadout:") + controls.edit = new("EditControl", nil, { 0, 40, 350, 20 }, specName, nil, nil, 100, function(buf) controls.save.enabled = buf:match("%S") end) - controls.save = new("ButtonControl", nil, {-45, 70, 80, 20}, "Save", function() + controls.save = new("ButtonControl", nil, { -45, 70, 80, 20 }, "Save", function() local newTitle = controls.edit.buf self.buildMode:RenameLoadout(specName, newTitle, function() self.buildMode:SyncLoadouts() @@ -55,61 +57,65 @@ function BuildSetListClass:RenameLoadout(spec, addOnName) main:ClosePopup() end) controls.save.enabled = false - controls.cancel = new("ButtonControl", nil, {45, 70, 80, 20}, "Cancel", function() + controls.cancel = new("ButtonControl", nil, { 45, 70, 80, 20 }, "Cancel", function() main:ClosePopup() end) main:OpenPopup(370, 100, specName and "Rename Loadout" or "Set Name", controls, "save", "edit", "cancel") end function BuildSetListClass:CopyLoadoutClick(build) - local controls = { } - local buildName = self.buildMode.treeTab.specList[build.specId].title - controls.label = new("LabelControl", nil, {0, 20, 0, 16}, "^7Enter name for this loadout:") - controls.edit = new("EditControl", nil, {0, 40, 350, 20}, buildName, nil, nil, 100, function(buf) + local controls = {} + local buildName = self.buildMode.treeTab.specList[build.specId].title + controls.label = new("LabelControl", nil, { 0, 20, 0, 16 }, "^7Enter name for this loadout:") + controls.edit = new("EditControl", nil, { 0, 40, 350, 20 }, buildName, nil, nil, 100, function(buf) controls.save.enabled = buf:match("%S") end) - controls.save = new("ButtonControl", nil, {-45, 70, 80, 20}, "Save", function() + controls.save = new("ButtonControl", nil, { -45, 70, 80, 20 }, "Save", function() local newBuildName = controls.edit.buf - local newSpec = self.buildMode:CopyLoadout(build.specId, build.itemSetId, build.skillSetId, build.configSetId, newBuildName) + local newSpec = self.buildMode:CopyLoadout(build.specId, build.itemSetId, build.skillSetId, build.configSetId, + newBuildName) self.buildMode:SyncLoadouts() - self.buildMode.modFlag = true - self.buildMode.controls.buildLoadouts:SetSel(newSpec.id + 1) + self.buildMode.modFlag = true + self.buildMode.controls.buildLoadouts:SetSel(newSpec.id + 1) main:ClosePopup() end) controls.save.enabled = false - controls.cancel = new("ButtonControl", nil, {45, 70, 80, 20}, "Cancel", function() + controls.cancel = new("ButtonControl", nil, { 45, 70, 80, 20 }, "Cancel", function() main:ClosePopup() end) main:OpenPopup(370, 100, buildName and "Rename" or "Set Name", controls, "save", "edit", "cancel") end function BuildSetListClass:NewLoadout() - local controls = { } - controls.label = new("LabelControl", nil, {0, 20, 0, 16}, "^7Enter name for this loadout:") - controls.edit = new("EditControl", nil, {0, 40, 350, 20}, "New Loadout", nil, nil, 100, function(buf) - controls.save.enabled = buf:match("%S") - end) - controls.save = new("ButtonControl", nil, {-45, 70, 80, 20}, "Save", function() - self.buildMode:NewLoadout(controls.edit.buf, function() - self.buildMode:SyncLoadouts() - self.buildMode.modFlag = true - self.buildMode.controls.buildLoadouts:SetSel(1) - main:ClosePopup() - end) - end) - controls.save.enabled = false - controls.cancel = new("ButtonControl", nil, {45, 70, 80, 20}, "Cancel", function() - main:ClosePopup() - end) - main:OpenPopup(370, 100, "Set Name", controls, "save", "edit", "cancel") + local controls = {} + controls.label = new("LabelControl", nil, { 0, 20, 0, 16 }, "^7Enter name for this loadout:") + controls.edit = new("EditControl", nil, { 0, 40, 350, 20 }, "New Loadout", nil, nil, 100, function(buf) + controls.save.enabled = buf:match("%S") + end) + controls.save = new("ButtonControl", nil, { -45, 70, 80, 20 }, "Save", function() + self.buildMode:NewLoadout(controls.edit.buf, function() + self.buildMode:SyncLoadouts() + self.buildMode.modFlag = true + self.buildMode.controls.buildLoadouts:SetSel(1) + main:ClosePopup() + end) + end) + controls.save.enabled = false + controls.cancel = new("ButtonControl", nil, { 45, 70, 80, 20 }, "Cancel", function() + main:ClosePopup() + end) + main:OpenPopup(370, 100, "Set Name", controls, "save", "edit", "cancel") end function BuildSetListClass:GetRowValue(column, index, spec) if column == 1 then local used = spec:CountAllocNodes() - return (spec.treeVersion ~= latestTreeVersion and ("["..treeVersions[spec.treeVersion].display.."] ") or "") - .. (spec.title or "Default") - .. " (" .. (spec.curAscendClassName ~= "None" and spec.curAscendClassName or spec.curClassName) .. ", " .. used .. " points)" + return (spec.treeVersion ~= latestTreeVersion and ("[" .. treeVersions[spec.treeVersion].display .. "] ") or "") + .. (spec.title or "Default") + .. + " (" .. + (spec.curAscendClassName ~= "None" and spec.curAscendClassName or spec.curClassName) .. + ", " .. used .. " points)" .. (index == self.buildMode.treeTab.activeSpec and " ^9(Current)" or "") end end @@ -126,14 +132,15 @@ end function BuildSetListClass:OnSelDelete(index, spec) if #self.list > 1 then - main:OpenConfirmPopup("Delete Loadout", "Are you sure you want to delete '"..(spec.title or "Default").."'?", "Delete", function() - t_remove(self.list, index) - local nextLoadoutIndex = index == self.buildMode.treeTab.activeSpec and m_max(1, index - 1) or index - local nextLoadout = self.list[nextLoadoutIndex] - self.buildMode:DeleteLoadout(spec.title or "Default", nextLoadout.title or "Default") - self.selIndex = nil - self.selValue = nil - end) + main:OpenConfirmPopup("Delete Loadout", "Are you sure you want to delete '" .. (spec.title or "Default") .. "'?", + "Delete", function() + t_remove(self.list, index) + local nextLoadoutIndex = index == self.buildMode.treeTab.activeSpec and m_max(1, index - 1) or index + local nextLoadout = self.list[nextLoadoutIndex] + self.buildMode:DeleteLoadout(spec.title or "Default", nextLoadout.title or "Default") + self.selIndex = nil + self.selValue = nil + end) end end diff --git a/src/Classes/ConfigTab.lua b/src/Classes/ConfigTab.lua index 8cc1a12dc3..6f761a31de 100644 --- a/src/Classes/ConfigTab.lua +++ b/src/Classes/ConfigTab.lua @@ -976,12 +976,12 @@ function ConfigTabClass:NewConfigSet(configSetId, title) end function ConfigTabClass:CopyConfigSet(configSetId, newConfigSetName) - local configSet = self.configSets[configSetId] - local newConfigSet = copyTable(configSet) - newConfigSet.id = #self.configSets + 1 - newConfigSet.title = newConfigSetName or configSet.title .. " (Copy)" - t_insert(self.configSets, newConfigSet) - return newConfigSet + local configSet = self.configSets[configSetId] + local newConfigSet = copyTable(configSet) + newConfigSet.id = #self.configSets + 1 + newConfigSet.title = newConfigSetName or configSet.title .. " (Copy)" + t_insert(self.configSets, newConfigSet) + return newConfigSet end -- Changes the active config set diff --git a/src/Classes/ItemsTab.lua b/src/Classes/ItemsTab.lua index fd4884a97f..32dcce9b6f 100644 --- a/src/Classes/ItemsTab.lua +++ b/src/Classes/ItemsTab.lua @@ -1352,11 +1352,11 @@ function ItemsTabClass:NewItemSet(itemSetId) end function ItemsTabClass:CopyItemSet(sourceItemSetId, newItemSetName) - local newSet = copyTable(self.itemSets[sourceItemSetId]) - newSet.id = #self.itemSets + 1 - newSet.title = newItemSetName or newSet.title .. " (Copy)" - self.itemSets[newSet.id] = newSet - return newSet + local newSet = copyTable(self.itemSets[sourceItemSetId]) + newSet.id = #self.itemSets + 1 + newSet.title = newItemSetName or newSet.title .. " (Copy)" + self.itemSets[newSet.id] = newSet + return newSet end -- Changes the active item set @@ -1385,9 +1385,9 @@ function ItemsTabClass:SetActiveItemSet(itemSetId, deferSync) end self.build.buildFlag = true self:PopulateSlots() - if not deferSync then - self.build:SyncLoadouts() - end + if not deferSync then + self.build:SyncLoadouts() + end end -- Equips the given item in the given item set diff --git a/src/Classes/SkillsTab.lua b/src/Classes/SkillsTab.lua index a0b31e4650..79a177ae18 100644 --- a/src/Classes/SkillsTab.lua +++ b/src/Classes/SkillsTab.lua @@ -1332,21 +1332,21 @@ function SkillsTabClass:NewSkillSet(skillSetId) end function SkillsTabClass:CopySkillSet(sourceSkillSetId, newSkillSetName) - local skillSet = self.skillSets[sourceSkillSetId] - local newSkillSet = copyTable(skillSet, true) - newSkillSet.title = newSkillSetName or skillSet.title .. " (Copy)" - newSkillSet.socketGroupList = { } - for socketGroupIndex, socketGroup in pairs(skillSet.socketGroupList) do - local newGroup = copyTable(socketGroup, true) - newGroup.gemList = { } - for gemIndex, gem in pairs(socketGroup.gemList) do - newGroup.gemList[gemIndex] = copyTable(gem, true) - end - t_insert(newSkillSet.socketGroupList, newGroup) - end - newSkillSet.id = #self.skillSets + 1 - self.skillSets[newSkillSet.id] = newSkillSet - return newSkillSet + local skillSet = self.skillSets[sourceSkillSetId] + local newSkillSet = copyTable(skillSet, true) + newSkillSet.title = newSkillSetName or skillSet.title .. " (Copy)" + newSkillSet.socketGroupList = {} + for socketGroupIndex, socketGroup in pairs(skillSet.socketGroupList) do + local newGroup = copyTable(socketGroup, true) + newGroup.gemList = {} + for gemIndex, gem in pairs(socketGroup.gemList) do + newGroup.gemList[gemIndex] = copyTable(gem, true) + end + t_insert(newSkillSet.socketGroupList, newGroup) + end + newSkillSet.id = #self.skillSets + 1 + self.skillSets[newSkillSet.id] = newSkillSet + return newSkillSet end -- Changes the active skill set diff --git a/src/Classes/TreeTab.lua b/src/Classes/TreeTab.lua index 55dd4f8c29..8ee3c9cb4a 100644 --- a/src/Classes/TreeTab.lua +++ b/src/Classes/TreeTab.lua @@ -645,14 +645,14 @@ function TreeTabClass:OpenSpecManagePopup() end function TreeTabClass:CopyTree(sourceSpecId, newSpecName) - local newSpec = new("PassiveSpec", self.build, self.specList[sourceSpecId].treeVersion) - newSpec.title = newSpecName or self.specList[sourceSpecId].title .. " (Copy)" - newSpec.jewels = copyTable(self.specList[sourceSpecId].jewels) - newSpec:RestoreUndoState(self.specList[sourceSpecId]:CreateUndoState()) - newSpec:BuildClusterJewelGraphs() - newSpec.id = #self.specList + 1 - t_insert(self.specList, newSpec) - return newSpec + local newSpec = new("PassiveSpec", self.build, self.specList[sourceSpecId].treeVersion) + newSpec.title = newSpecName or self.specList[sourceSpecId].title .. " (Copy)" + newSpec.jewels = copyTable(self.specList[sourceSpecId].jewels) + newSpec:RestoreUndoState(self.specList[sourceSpecId]:CreateUndoState()) + newSpec:BuildClusterJewelGraphs() + newSpec.id = #self.specList + 1 + t_insert(self.specList, newSpec) + return newSpec end function TreeTabClass:OpenVersionConvertPopup(version, ignoreTreeSubType) diff --git a/src/Modules/Build.lua b/src/Modules/Build.lua index 60c9b2858e..038d35b7a9 100644 --- a/src/Modules/Build.lua +++ b/src/Modules/Build.lua @@ -300,12 +300,12 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin self.controls.buildLoadouts:SetSel(1) return end - if value == "^7^7Manage" then + if value == "^7^7Manage" then self:OpenBuildSetManagePopup() end - local loadout = self:GetLoadoutByName(value) - self:SetActiveLoadout(loadout) + local loadout = self:GetLoadoutByName(value) + self:SetActiveLoadout(loadout) self.controls.buildLoadouts:SelByValue(value) end) @@ -728,10 +728,9 @@ function buildMode:SyncLoadouts() end end end - -- giving the options unique formatting so it can not match with user-created sets t_insert(filteredList, "^7^7-----") - t_insert(filteredList, "^7^7Manage") + t_insert(filteredList, "^7^7Manage") t_insert(filteredList, "^7^7Sync") t_insert(filteredList, "^7^7Help >>") @@ -768,159 +767,162 @@ function buildMode:SyncLoadouts() end function buildMode:NewLoadout(loadoutName, callback --[[function]]) - local newSpec = new("PassiveSpec", self, latestTreeVersion) - local newItemSet = self.itemsTab:NewItemSet(#self.itemsTab.itemSets + 1) - local newSkillSet = self.skillsTab:NewSkillSet(#self.skillsTab.skillSets + 1) - local newConfigSet = self.configTab:NewConfigSet(#self.configTab.configSets + 1) - self:AddLoadout(loadoutName, newSpec, newItemSet, newSkillSet, newConfigSet, callback) + local newSpec = new("PassiveSpec", self, latestTreeVersion) + local newItemSet = self.itemsTab:NewItemSet(#self.itemsTab.itemSets + 1) + local newSkillSet = self.skillsTab:NewSkillSet(#self.skillsTab.skillSets + 1) + local newConfigSet = self.configTab:NewConfigSet(#self.configTab.configSets + 1) + self:AddLoadout(loadoutName, newSpec, newItemSet, newSkillSet, newConfigSet, callback) end function buildMode:CopyLoadout(copyTreeId, copyItemSetId, copySkillSetId, copyConfigSetId, loadoutName) - local newSpec = self.treeTab:CopyTree(copyTreeId, loadoutName) - local newItemSet = self.itemsTab:CopyItemSet(copyItemSetId, loadoutName) - local newSkillSet = self.skillsTab:CopySkillSet(copySkillSetId, loadoutName) - local newConfigSet = self.configTab:CopyConfigSet(copyConfigSetId, loadoutName) - - t_insert(self.itemsTab.itemSetOrderList, newItemSet.id) - t_insert(self.skillsTab.skillSetOrderList, newSkillSet.id) - t_insert(self.configTab.configSetOrderList, newConfigSet.id) - return newSpec, newItemSet, newSkillSet, newConfigSet + local newSpec = self.treeTab:CopyTree(copyTreeId, loadoutName) + local newItemSet = self.itemsTab:CopyItemSet(copyItemSetId, loadoutName) + local newSkillSet = self.skillsTab:CopySkillSet(copySkillSetId, loadoutName) + local newConfigSet = self.configTab:CopyConfigSet(copyConfigSetId, loadoutName) + + t_insert(self.itemsTab.itemSetOrderList, newItemSet.id) + t_insert(self.skillsTab.skillSetOrderList, newSkillSet.id) + t_insert(self.configTab.configSetOrderList, newConfigSet.id) + return newSpec, newItemSet, newSkillSet, newConfigSet end function buildMode:AddLoadout(loadoutName, newSpec, newItemSet, newSkillSet, newConfigSet, callback --[[function]]) - t_insert(self.treeTab.specList, newSpec) - newSpec.title = loadoutName + t_insert(self.treeTab.specList, newSpec) + newSpec.title = loadoutName - t_insert(self.itemsTab.itemSetOrderList, newItemSet.id) - newItemSet.title = loadoutName + t_insert(self.itemsTab.itemSetOrderList, newItemSet.id) + newItemSet.title = loadoutName - t_insert(self.skillsTab.skillSetOrderList, newSkillSet.id) - newSkillSet.title = loadoutName + t_insert(self.skillsTab.skillSetOrderList, newSkillSet.id) + newSkillSet.title = loadoutName - t_insert(self.configTab.configSetOrderList, newConfigSet.id) - newConfigSet.title = loadoutName + t_insert(self.configTab.configSetOrderList, newConfigSet.id) + newConfigSet.title = loadoutName - callback() + callback() end function buildMode:DeleteLoadout(loadoutName, nextLoadoutName) - local loadout = self:GetLoadoutByName(loadoutName) - local nextLoadout = self:GetLoadoutByName(nextLoadoutName) - if loadout.specId then - self.treeTab.specList[loadout.specId] = nil - self.treeTab:SetActiveSpec(1) - end - if loadout.itemSetId then - self.itemsTab.itemSets[loadout.itemSetId] = nil - t_remove(self.itemsTab.itemSetOrderList, loadout.itemSetId) - end - if loadout.skillSetId then - self.skillsTab.skillSets[loadout.skillSetId] = nil - t_remove(self.skillsTab.skillSetOrderList, loadout.skillSetId) - end - if loadout.configSetId then - self.configTab.configSets[loadout.configSetId] = nil - t_remove(self.configTab.configSetOrderList, loadout.configSetId) - end - - self:SetActiveLoadout(nextLoadout) + local loadout = self:GetLoadoutByName(loadoutName) + local nextLoadout = self:GetLoadoutByName(nextLoadoutName) + if loadout.specId then + self.treeTab.specList[loadout.specId] = nil + self.treeTab:SetActiveSpec(1) + end + if loadout.itemSetId then + self.itemsTab.itemSets[loadout.itemSetId] = nil + t_remove(self.itemsTab.itemSetOrderList, loadout.itemSetId) + end + if loadout.skillSetId then + self.skillsTab.skillSets[loadout.skillSetId] = nil + t_remove(self.skillsTab.skillSetOrderList, loadout.skillSetId) + end + if loadout.configSetId then + self.configTab.configSets[loadout.configSetId] = nil + t_remove(self.configTab.configSetOrderList, loadout.configSetId) + end + + self:SetActiveLoadout(nextLoadout) end function buildMode:RenameLoadout(oldName, newName, callback) - local loadout = self:GetLoadoutByName(oldName) - if loadout.specId then - self.treeTab.specList[loadout.specId].title = newName - self.treeTab.modFlag = true - end - if loadout.itemSetId then - self.itemsTab.itemSets[loadout.itemSetId].title = newName - self.itemsTab.modFlag = true - end - if loadout.skillSetId then - self.skillsTab.skillSets[loadout.skillSetId].title = newName - self.skillsTab.modFlag = true - end - if loadout.configSetId then - self.configTab.configSets[loadout.configSetId].title = newName - self.configTab.modFlag = true - end - callback() + local loadout = self:GetLoadoutByName(oldName) + if loadout.specId then + self.treeTab.specList[loadout.specId].title = newName + self.treeTab.modFlag = true + end + if loadout.itemSetId then + self.itemsTab.itemSets[loadout.itemSetId].title = newName + self.itemsTab.modFlag = true + end + if loadout.skillSetId then + self.skillsTab.skillSets[loadout.skillSetId].title = newName + self.skillsTab.modFlag = true + end + if loadout.configSetId then + self.configTab.configSets[loadout.configSetId].title = newName + self.configTab.modFlag = true + end + callback() end function buildMode:GetLoadoutByName(loadoutName) - -- item, skill, and config sets have identical structure - -- return id as soon as it's found - local function findSetId(setOrderList, value, sets, setSpecialLinks) - for _, setOrder in ipairs(setOrderList) do - if value == (sets[setOrder].title or "Default") then - return setOrder - else - local linkMatch = string.match(value, "%{(%w+)%}") - if linkMatch then - return setSpecialLinks[linkMatch]["setId"] - end - end - end - return nil - end - - -- trees have a different structure with id/name pairs - -- return id as soon as it's found - local function findNamedSetId(treeList, value, setSpecialLinks) - for id, spec in ipairs(treeList) do - if value == spec then - return id - else - local linkMatch = string.match(value, "%{(%w+)%}") - if linkMatch then - return setSpecialLinks[linkMatch]["setId"] - end - end - end - return nil - end - - local oneSkill = self.skillsTab and #self.skillsTab.skillSetOrderList == 1 - local oneItem = self.itemsTab and #self.itemsTab.itemSetOrderList == 1 - local oneConfig = self.configTab and #self.configTab.configSetOrderList == 1 - - local specId = findNamedSetId(self.treeTab:GetSpecList(), loadoutName, self.treeListSpecialLinks) - local itemId = oneItem and 1 or findSetId(self.itemsTab.itemSetOrderList, loadoutName, self.itemsTab.itemSets, self.itemListSpecialLinks) - local skillId = oneSkill and 1 or findSetId(self.skillsTab.skillSetOrderList, loadoutName, self.skillsTab.skillSets, self.skillListSpecialLinks) - local configId = oneConfig and 1 or findSetId(self.configTab.configSetOrderList, loadoutName, self.configTab.configSets, self.configListSpecialLinks) - - - return { - specId = specId, - itemSetId = itemId, - skillSetId = skillId, - configSetId = configId - } + -- item, skill, and config sets have identical structure + -- return id as soon as it's found + local function findSetId(setOrderList, value, sets, setSpecialLinks) + for _, setOrder in ipairs(setOrderList) do + if value == (sets[setOrder].title or "Default") then + return setOrder + else + local linkMatch = string.match(value, "%{(%w+)%}") + if linkMatch then + return setSpecialLinks[linkMatch]["setId"] + end + end + end + return nil + end + + -- trees have a different structure with id/name pairs + -- return id as soon as it's found + local function findNamedSetId(treeList, value, setSpecialLinks) + for id, spec in ipairs(treeList) do + if value == spec then + return id + else + local linkMatch = string.match(value, "%{(%w+)%}") + if linkMatch then + return setSpecialLinks[linkMatch]["setId"] + end + end + end + return nil + end + + local oneSkill = self.skillsTab and #self.skillsTab.skillSetOrderList == 1 + local oneItem = self.itemsTab and #self.itemsTab.itemSetOrderList == 1 + local oneConfig = self.configTab and #self.configTab.configSetOrderList == 1 + + local specId = findNamedSetId(self.treeTab:GetSpecList(), loadoutName, self.treeListSpecialLinks) + local itemId = oneItem and 1 or + findSetId(self.itemsTab.itemSetOrderList, loadoutName, self.itemsTab.itemSets, self.itemListSpecialLinks) + local skillId = oneSkill and 1 or + findSetId(self.skillsTab.skillSetOrderList, loadoutName, self.skillsTab.skillSets, self.skillListSpecialLinks) + local configId = oneConfig and 1 or + findSetId(self.configTab.configSetOrderList, loadoutName, self.configTab.configSets, self.configListSpecialLinks) + + return { + specId = specId, + itemSetId = itemId, + skillSetId = skillId, + configSetId = configId + } end function buildMode:SetActiveLoadout(loadout) - if not loadout then - return - end - - local newSpecId, newItemId, newSkillId, newConfigId = loadout.specId, loadout.itemSetId, loadout.skillSetId, loadout.configSetId - if newSpecId == nil or newItemId == nil or newSkillId == nil or newConfigId == nil then - return - end - - if newSpecId ~= self.treeTab.activeSpec then - self.treeTab:SetActiveSpec(newSpecId, true) - end - if newItemId ~= self.itemsTab.activeItemSetId then - self.itemsTab:SetActiveItemSet(newItemId, true) - end - if newSkillId ~= self.skillsTab.activeSkillSetId then - self.skillsTab:SetActiveSkillSet(newSkillId, true) - end - if newConfigId ~= self.configTab.activeConfigSetId then - self.configTab:SetActiveConfigSet(newConfigId, false, true) - end - self:SyncLoadouts() + if not loadout then + return + end + + local newSpecId, newItemId, newSkillId, newConfigId = loadout.specId, loadout.itemSetId, loadout.skillSetId, + loadout.configSetId + if newSpecId == nil or newItemId == nil or newSkillId == nil or newConfigId == nil then + return + end + + if newSpecId ~= self.treeTab.activeSpec then + self.treeTab:SetActiveSpec(newSpecId, true) + end + if newItemId ~= self.itemsTab.activeItemSetId then + self.itemsTab:SetActiveItemSet(newItemId, true) + end + if newSkillId ~= self.skillsTab.activeSkillSetId then + self.skillsTab:SetActiveSkillSet(newSkillId, true) + end + if newConfigId ~= self.configTab.activeConfigSetId then + self.configTab:SetActiveConfigSet(newConfigId, false, true) + end + self:SyncLoadouts() end function buildMode:EstimatePlayerProgress() @@ -2029,8 +2031,8 @@ end -- Opens the build set manager function buildMode:OpenBuildSetManagePopup() main:OpenPopup(370, 290, "Manage Build Sets", { - new("BuildSetListControl", nil, {0, 50, 350, 200}, self, self.treeTab), - new("ButtonControl", nil, {0, 260, 90, 20}, "Done", function() + new("BuildSetListControl", nil, { 0, 50, 350, 200 }, self, self.treeTab), + new("ButtonControl", nil, { 0, 260, 90, 20 }, "Done", function() main:ClosePopup() end), }) From 877ee6bc9f626afea1ef3bba633baebc112ffa77 Mon Sep 17 00:00:00 2001 From: Riley Maersch Date: Tue, 17 Mar 2026 00:46:08 -0500 Subject: [PATCH 3/3] [ISSUE-9451] Loadout Managment * Adds basic tests for NewLoadout, CopyLoadout, DeleteLoadout, and RenameLoadout --- spec/System/TestLoadouts_spec.lua | 138 ++++++++++++++++++++++++++++++ src/Modules/Build.lua | 4 + 2 files changed, 142 insertions(+) create mode 100644 spec/System/TestLoadouts_spec.lua diff --git a/spec/System/TestLoadouts_spec.lua b/spec/System/TestLoadouts_spec.lua new file mode 100644 index 0000000000..2574025317 --- /dev/null +++ b/spec/System/TestLoadouts_spec.lua @@ -0,0 +1,138 @@ +describe("TestLoadouts", function() + before_each(function() + newBuild() + + build.itemsTab:CreateDisplayItemFromRaw([[Dialla's Malefaction + Sage's Robe + Energy Shield: 95 + EnergyShieldBasePercentile: 0 + Variant: Pre 3.19.0 + Variant: Current + Selected Variant: 2 + Sage's Robe + Quality: 20 + Sockets: R-G-B-B-B-B + LevelReq: 37 + Implicits: 0 + Gems can be Socketed in this Item ignoring Socket Colour + {variant:1}Gems Socketed in Red Sockets have +1 to Level + {variant:2}Gems Socketed in Red Sockets have +2 to Level + {variant:1}Gems Socketed in Green Sockets have +10% to Quality + {variant:2}Gems Socketed in Green Sockets have +30% to Quality + {variant:1}Gems Socketed in Blue Sockets gain 25% increased Experience + {variant:2}Gems Socketed in Blue Sockets gain 100% increased Experience + Has no Attribute Requirements]]) + build.itemsTab:AddDisplayItem() + runCallback("OnFrame") + end) + + teardown(function() + -- newBuild() takes care of resetting everything in setup() + end) + + local function getSelectedLoadout(treeId, itemIndex, itemId, skillIndex, skillId, configIndex, configId) + local selectedLoadout = { + treeId = treeId, + itemIndex = itemIndex, + itemId = itemId, + skillIndex = skillIndex, + skillId = skillId, + configIndex = configIndex, + configId = configId, + } + return selectedLoadout + end + + describe("NewLoadout", function() + it("Creates a new loadout with the correct name", function() + build:NewLoadout("Loadout Name", function() end) + build:SyncLoadouts() + -- There are 5 static items in the list + assert.are.equals(7, #build.controls.buildLoadouts.list) + end) + + it("calls the callback function", function() + local callbackCalled = false + build:NewLoadout("Loadout Name", function() callbackCalled = true end) + build:SyncLoadouts() + -- There are 5 static items in the list + assert.are.equals(7, #build.controls.buildLoadouts.list) + assert.are.equals(true, callbackCalled) + end) + end) + + describe("CopyLoadout", function() + it("Copies a loadout with a new name", function() + local loadoutName = "Loadout Name" + local newSpec, newItemSet, newSkillSet, newConfigSet = build:CopyLoadout(1, 1, 1, 1, loadoutName) + build:SyncLoadouts() + -- There are 5 static items in the list + assert.are.equals(7, #build.controls.buildLoadouts.list) + -- First index is the "Loadout: " header, second index is the start of the loadouts + assert.is_not.same(build.controls.buildLoadouts.list[2], build.controls.buildLoadouts.list[3]) + assert.are.equals(loadoutName, build.controls.buildLoadouts.list[3]) + assert.is_not.same(newSpec, build.spec) + assert.is_not.same(newItemSet, build.itemsTab.itemSets[1]) + assert.is_not.same(newSkillSet, build.skillsTab.skillSets[1]) + assert.is_not.same(newConfigSet, build.configTab.configSets[1]) + assert.is_same(loadoutName, newSpec.title) + assert.is_same(loadoutName, newItemSet.title) + assert.is_same(loadoutName, newSkillSet.title) + assert.is_same(loadoutName, newConfigSet.title) + end) + end) + + describe("DeleteLoadout", function() + it("Deletes a loadout and sets the next to the requested loadout by name", function() + local loadoutNameToDelete = "Delete Me" + build:NewLoadout(loadoutNameToDelete, function() end) + build:SyncLoadouts() + -- There are 5 static items in the list + assert.are.equals(7, #build.controls.buildLoadouts.list) + local loadoutToDelete = build:GetLoadoutByName(loadoutNameToDelete) + local nextLoadout = build.controls.buildLoadouts.list[2] -- Default loadout + + build:DeleteLoadout(loadoutNameToDelete, nextLoadout) + build:SyncLoadouts() + + assert.is_nil(build.treeTab.specList[loadoutToDelete.specId]) + assert.is_nil(build.skillsTab.skillSets[loadoutToDelete.skillSetId]) + assert.is_nil(build.itemsTab.itemSets[loadoutToDelete.itemSetId]) + assert.is_nil(build.configTab.configSets[loadoutToDelete.configSetId]) + + assert.is_nil(build.itemsTab.itemSetOrderList[loadoutToDelete.itemSetId]) + assert.is_nil(build.skillsTab.skillSetOrderList[loadoutToDelete.skillSetId]) + assert.is_nil(build.configTab.configSetOrderList[loadoutToDelete.configSetId]) + + assert.is_same(nextLoadout.specId, build.treeTab.displaySpecId) + assert.is_same(nextLoadout.itemSetId, build.itemsTab.displayItemSetId) + assert.is_same(nextLoadout.skillSetId, build.skillsTab.displaySkillSetId) + assert.is_same(nextLoadout.configSetId, build.configTab.displayConfigSetId) + end) + end) + + describe("RenameLoadout", function() + it("renames a loadout and calls the callback", function() + local oldName = "Old Loadout" + local newName = "New Loadout" + local callbackCalled = false + build:NewLoadout(oldName, function() end) + build:SyncLoadouts() + + build:RenameLoadout(oldName, newName, function() callbackCalled = true end) + build:SyncLoadouts() + -- Verify the new name appears in the loadout list + assert.is_same(newName, build.controls.buildLoadouts.list[3]) + -- Verify titles updated on spec, itemSet, skillSet, and configSet + local loadout = build:GetLoadoutByName(newName) + assert.is_same(newName, build.treeTab.specList[loadout.specId].title) + assert.is_same(newName, build.itemsTab.itemSets[loadout.itemSetId].title) + assert.is_same(newName, build.skillsTab.skillSets[loadout.skillSetId].title) + assert.is_same(newName, build.configTab.configSets[loadout.configSetId].title) + -- Ensure callback was called + assert.is_true(callbackCalled) + -- Old name should no longer exist + assert.is_nil(build:GetLoadoutByName(oldName)) + end) + end) +end) diff --git a/src/Modules/Build.lua b/src/Modules/Build.lua index 038d35b7a9..24c2d95dae 100644 --- a/src/Modules/Build.lua +++ b/src/Modules/Build.lua @@ -891,6 +891,10 @@ function buildMode:GetLoadoutByName(loadoutName) local configId = oneConfig and 1 or findSetId(self.configTab.configSetOrderList, loadoutName, self.configTab.configSets, self.configListSpecialLinks) + if not specId and not itemId and not skillId and not configId then + return nil + end + return { specId = specId, itemSetId = itemId,