From 578e423a7e01ffec5d97acdef23007974b1aaa93 Mon Sep 17 00:00:00 2001 From: Can Uysal Date: Fri, 14 Aug 2026 22:56:27 +0300 Subject: [PATCH 1/2] Add loadout manager for copying, renaming and deleting loadouts * Add "Manage Loadouts" option to the loadouts dropdown, replacing "Sync" * Manager supports New, Copy, Rename and Delete across all four associated sets, plus double-click to activate * Copy creates the new loadout automatically with a unique name * Move Sync into the manager popup * Update help.txt and add tests --- help.txt | 8 +- spec/System/TestLoadouts_spec.lua | 70 ++++++++ src/Classes/LoadoutListControl.lua | 278 +++++++++++++++++++++++++++++ src/Modules/Build.lua | 127 ++++++++----- 4 files changed, 439 insertions(+), 44 deletions(-) create mode 100644 spec/System/TestLoadouts_spec.lua create mode 100644 src/Classes/LoadoutListControl.lua diff --git a/help.txt b/help.txt index b728173120d..f05c843fec3 100644 --- a/help.txt +++ b/help.txt @@ -134,7 +134,13 @@ Loadouts can be selected from the dropdown in the top middle of the screen. Sele 3) If there is only one set for a set type (except passive tree), e.g. "Default" config set, it will be assigned to all existing loadouts. The "New Loadout" option allows the user to create all four sets from a single popup for convenience. -The "Sync" option is a backup option to force the UI to update in case the user has changed this data behind the scenes. +The "Manage Loadouts" option opens the loadout manager, which works like the set managers found on the Tree, Skills, Items, and Configuration tabs: + - "New" creates all four sets from a single popup, the same as the "New Loadout" option + - "Copy" duplicates all four sets of the selected loadout into a new loadout automatically + - "Rename" renames all four sets of the selected loadout, preserving any identifiers inside braces { } + - "Delete" deletes all four sets of the selected loadout; sets shared with other loadouts, or the last remaining set of a type, are kept + - Double-clicking a loadout activates it + - "Sync" is a backup option to force the UI to update in case the user has changed this data behind the scenes ---[Party Tab] diff --git a/spec/System/TestLoadouts_spec.lua b/spec/System/TestLoadouts_spec.lua new file mode 100644 index 00000000000..6d877d50482 --- /dev/null +++ b/spec/System/TestLoadouts_spec.lua @@ -0,0 +1,70 @@ +describe("TestLoadouts", function() + before_each(function() + newBuild() + end) + + teardown(function() + -- newBuild() takes care of resetting everything in before_each() + end) + + it("creates a new loadout with all four sets", function() + build:NewLoadout("My Loadout") + assert.are.equals("My Loadout", build.treeTab.specList[#build.treeTab.specList].title) + assert.are.equals("My Loadout", build.itemsTab.itemSets[build.itemsTab.itemSetOrderList[#build.itemsTab.itemSetOrderList]].title) + assert.are.equals("My Loadout", build.skillsTab.skillSets[build.skillsTab.skillSetOrderList[#build.skillsTab.skillSetOrderList]].title) + assert.are.equals("My Loadout", build.configTab.configSets[build.configTab.configSetOrderList[#build.configTab.configSetOrderList]].title) + local found = false + for _, loadout in ipairs(build.loadoutList) do + if loadout.name == "My Loadout" then + found = true + end + end + assert.is_true(found) + end) + + it("copies a loadout automatically", function() + build:NewLoadout("My Loadout") + local listControl = new("LoadoutListControl"):LoadoutListControl(nil, {0, 0, 450, 200}, build) + local loadout + for _, entry in ipairs(build.loadoutList) do + if entry.name == "My Loadout" then + loadout = entry + end + end + assert.is_not_nil(loadout) + listControl:CopyLoadout(loadout) + local copyFound = false + for _, entry in ipairs(build.loadoutList) do + if entry.name == "My Loadout (copy)" then + copyFound = true + end + end + assert.is_true(copyFound) + -- Copying again picks a unique name + listControl:CopyLoadout(loadout) + local copy2Found = false + for _, entry in ipairs(build.loadoutList) do + if entry.name == "My Loadout (copy 2)" then + copy2Found = true + end + end + assert.is_true(copy2Found) + end) + + it("resolves link identifier loadouts", function() + build.treeTab.specList[1].title = "Tree {A}" + local itemSet = build.itemsTab.itemSets[build.itemsTab.itemSetOrderList[1]] + itemSet.title = "Items {A}" + -- With only one skill and config set, they are shared by all loadouts + build:SyncLoadouts() + local loadout + for _, entry in ipairs(build.loadoutList) do + if entry.name == "Tree {A}" then + loadout = entry + end + end + assert.is_not_nil(loadout) + assert.are.equals(1, loadout.specId) + assert.are.equals(build.itemsTab.itemSetOrderList[1], loadout.itemSetId) + end) +end) diff --git a/src/Classes/LoadoutListControl.lua b/src/Classes/LoadoutListControl.lua new file mode 100644 index 00000000000..6716cb132dd --- /dev/null +++ b/src/Classes/LoadoutListControl.lua @@ -0,0 +1,278 @@ +-- Path of Building +-- +-- Class: Loadout List +-- Loadout list control. +-- +local t_insert = table.insert +local t_remove = table.remove +local m_max = math.max + +---@class LoadoutListControl: ListControl +local LoadoutListClass = newClass("LoadoutListControl", "ListControl") + +function LoadoutListClass:LoadoutListControl(anchor, rect, build) + self:ListControl(anchor, rect, 16, "VERTICAL", false, build.loadoutList) + self.build = build + self.controls.copy = new("ButtonControl"):ButtonControl({"BOTTOMLEFT",self,"TOP"}, {2, -4, 60, 18}, "Copy", function() + self:CopyLoadout(self.selValue) + end) + self.controls.copy.enabled = function() + return self.selValue ~= nil + end + self.controls.delete = new("ButtonControl"):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 + end + self.controls.rename = new("ButtonControl"):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"):ButtonControl({"RIGHT",self.controls.rename,"LEFT"}, {-4, 0, 60, 18}, "New", function() + build:OpenLoadoutNamePopup() + end) + return self +end + +-- Returns true if another loadout also uses this set +function LoadoutListClass:IsSetShared(field, setId, loadout) + for _, other in ipairs(self.build.loadoutList) do + if other ~= loadout and other[field] == setId then + return true + end + end + return false +end + +function LoadoutListClass:NameInUse(name) + for _, loadout in ipairs(self.build.loadoutList) do + if loadout.name == name then + return true + end + end + return false +end + +-- Re-selects the loadout owning the given passive tree after the list has been rebuilt +function LoadoutListClass:SelectLoadoutBySpecId(specId) + self.selIndex = nil + self.selValue = nil + for index, loadout in ipairs(self.list) do + if loadout.specId == specId then + self.selIndex = index + self.selValue = loadout + break + end + end +end + +-- Copies all four sets of the given loadout into a new loadout +function LoadoutListClass:CopyLoadout(loadout) + local build = self.build + local baseName = (loadout.name:gsub("%s*%{[%w,]+%}", "")) + if baseName == "" then + baseName = "Default" + end + local newName = baseName .. " (copy)" + local suffix = 1 + while self:NameInUse(newName) do + suffix = suffix + 1 + newName = baseName .. " (copy " .. suffix .. ")" + end + + local spec = build.treeTab.specList[loadout.specId] + local newSpec = new("PassiveSpec"):PassiveSpec(build, spec.treeVersion) + newSpec.title = newName + newSpec.jewels = copyTable(spec.jewels) + newSpec:RestoreUndoState(spec:CreateUndoState()) + newSpec:BuildClusterJewelGraphs() + t_insert(build.treeTab.specList, newSpec) + build.treeTab.modFlag = true + + local itemsTab = build.itemsTab + local newItemSet = copyTable(itemsTab.itemSets[loadout.itemSetId]) + newItemSet.title = newName + newItemSet.id = 1 + while itemsTab.itemSets[newItemSet.id] do + newItemSet.id = newItemSet.id + 1 + end + itemsTab.itemSets[newItemSet.id] = newItemSet + t_insert(itemsTab.itemSetOrderList, newItemSet.id) + itemsTab:AddUndoState() + + local skillsTab = build.skillsTab + local skillSet = skillsTab.skillSets[loadout.skillSetId] + local newSkillSet = copyTable(skillSet, true) + newSkillSet.title = newName + newSkillSet.socketGroupList = { } + for _, 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 + t_insert(skillsTab.skillSetOrderList, newSkillSet.id) + skillsTab:AddUndoState() + + local configTab = build.configTab + local newConfigSet = copyTable(configTab.configSets[loadout.configSetId]) + newConfigSet.title = newName + newConfigSet.id = 1 + while configTab.configSets[newConfigSet.id] do + newConfigSet.id = newConfigSet.id + 1 + end + configTab.configSets[newConfigSet.id] = newConfigSet + t_insert(configTab.configSetOrderList, newConfigSet.id) + configTab:AddUndoState() + + self:UpdateItemsTabPassiveTreeDropdown() + build:SyncLoadouts() + self:SelectLoadoutBySpecId(#build.treeTab.specList) +end + +function LoadoutListClass:RenameLoadout(loadout) + local build = self.build + local spec = build.treeTab.specList[loadout.specId] + local controls = { } + local currentName = ((spec.title or "Default"):gsub("%s*%{[%w,]+%}", "")) + controls.label = new("LabelControl"):LabelControl(nil, {0, 20, 0, 16}, "^7Enter new name for this loadout:") + controls.edit = new("EditControl"):EditControl(nil, {0, 40, 350, 20}, currentName, nil, nil, 100, function(buf) + controls.save.enabled = buf:match("%S") + end) + controls.save = new("ButtonControl"):ButtonControl(nil, {-45, 70, 80, 20}, "Save", function() + local newName = controls.edit.buf + -- Rename each of the associated sets, preserving any {link} identifiers in their titles + local function newTitle(title) + local linkIdentifier = title and title:match("%{[%w,]+%}") + return linkIdentifier and (newName .. " " .. linkIdentifier) or newName + end + spec.title = newTitle(spec.title) + local itemSet = build.itemsTab.itemSets[loadout.itemSetId] + itemSet.title = newTitle(itemSet.title) + local skillSet = build.skillsTab.skillSets[loadout.skillSetId] + skillSet.title = newTitle(skillSet.title) + local configSet = build.configTab.configSets[loadout.configSetId] + configSet.title = newTitle(configSet.title) + build.treeTab.modFlag = true + build.itemsTab:AddUndoState() + build.skillsTab:AddUndoState() + build.configTab:AddUndoState() + self:UpdateItemsTabPassiveTreeDropdown() + build:SyncLoadouts() + self:SelectLoadoutBySpecId(loadout.specId) + main:ClosePopup() + end) + controls.save.enabled = false + controls.cancel = new("ButtonControl"):ButtonControl(nil, {45, 70, 80, 20}, "Cancel", function() + main:ClosePopup() + end) + main:OpenPopup(370, 100, "Rename", controls, "save", "edit", "cancel") +end + +function LoadoutListClass:IsActiveLoadout(loadout) + local build = self.build + return loadout.specId == build.treeTab.activeSpec + and loadout.itemSetId == build.itemsTab.activeItemSetId + and loadout.skillSetId == build.skillsTab.activeSkillSetId + and loadout.configSetId == build.configTab.activeConfigSetId +end + +function LoadoutListClass:GetRowValue(column, index, loadout) + if column == 1 then + return loadout.name .. (self:IsActiveLoadout(loadout) and " ^9(Current)" or "") + end +end + +function LoadoutListClass:OnSelClick(index, loadout, doubleClick) + if doubleClick then + local build = self.build + if loadout.specId ~= build.treeTab.activeSpec then + build.treeTab:SetActiveSpec(loadout.specId) + end + if loadout.itemSetId ~= build.itemsTab.activeItemSetId then + build.itemsTab:SetActiveItemSet(loadout.itemSetId) + end + if loadout.skillSetId ~= build.skillsTab.activeSkillSetId then + build.skillsTab:SetActiveSkillSet(loadout.skillSetId) + end + if loadout.configSetId ~= build.configTab.activeConfigSetId then + build.configTab:SetActiveConfigSet(loadout.configSetId) + end + build:SyncLoadouts() + self:SelectLoadoutBySpecId(loadout.specId) + end +end + +function LoadoutListClass:OnSelDelete(index, loadout) + local build = self.build + main:OpenConfirmPopup("Delete Loadout", "Are you sure you want to delete '"..loadout.name.."'?\nThis will delete the passive tree, item set, skill set and config set associated with it.\nSets shared with other loadouts, or the last remaining set of a type, will be kept.", "Delete", function() + local treeTab, itemsTab, skillsTab, configTab = build.treeTab, build.itemsTab, build.skillsTab, build.configTab + if #treeTab.specList > 1 and not self:IsSetShared("specId", loadout.specId, loadout) then + t_remove(treeTab.specList, loadout.specId) + if loadout.specId == treeTab.activeSpec then + treeTab:SetActiveSpec(m_max(1, loadout.specId - 1)) + else + treeTab.activeSpec = isValueInArray(treeTab.specList, build.spec) + end + treeTab.modFlag = true + end + if #itemsTab.itemSetOrderList > 1 and not self:IsSetShared("itemSetId", loadout.itemSetId, loadout) then + local setIndex = isValueInArray(itemsTab.itemSetOrderList, loadout.itemSetId) + t_remove(itemsTab.itemSetOrderList, setIndex) + itemsTab.itemSets[loadout.itemSetId] = nil + if loadout.itemSetId == itemsTab.activeItemSetId then + itemsTab:SetActiveItemSet(itemsTab.itemSetOrderList[m_max(1, setIndex - 1)]) + end + itemsTab:AddUndoState() + end + if #skillsTab.skillSetOrderList > 1 and not self:IsSetShared("skillSetId", loadout.skillSetId, loadout) then + local setIndex = isValueInArray(skillsTab.skillSetOrderList, loadout.skillSetId) + t_remove(skillsTab.skillSetOrderList, setIndex) + skillsTab.skillSets[loadout.skillSetId] = nil + if loadout.skillSetId == skillsTab.activeSkillSetId then + skillsTab:SetActiveSkillSet(skillsTab.skillSetOrderList[m_max(1, setIndex - 1)]) + end + skillsTab:AddUndoState() + end + if #configTab.configSetOrderList > 1 and not self:IsSetShared("configSetId", loadout.configSetId, loadout) then + local setIndex = isValueInArray(configTab.configSetOrderList, loadout.configSetId) + t_remove(configTab.configSetOrderList, setIndex) + configTab.configSets[loadout.configSetId] = nil + if loadout.configSetId == configTab.activeConfigSetId then + configTab:SetActiveConfigSet(configTab.configSetOrderList[m_max(1, setIndex - 1)]) + end + configTab:AddUndoState() + end + self.selIndex = nil + self.selValue = nil + self:UpdateItemsTabPassiveTreeDropdown() + build:SyncLoadouts() + end) +end + +function LoadoutListClass:OnSelKeyDown(index, loadout, key) + if key == "F2" then + self:RenameLoadout(loadout) + end +end + +-- Update the passive tree dropdown control in itemsTab +function LoadoutListClass:UpdateItemsTabPassiveTreeDropdown() + local build = self.build + local newSpecList = { } + for i, spec in ipairs(build.treeTab.specList) do + newSpecList[i] = spec.title or "Default" + end + build.itemsTab.controls.specSelect:SetList(newSpecList) + build.itemsTab.controls.specSelect.selIndex = build.treeTab.activeSpec +end diff --git a/src/Modules/Build.lua b/src/Modules/Build.lua index dcfbc74e892..96e3c48b5b2 100644 --- a/src/Modules/Build.lua +++ b/src/Modules/Build.lua @@ -306,8 +306,8 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin self.controls.buildLoadouts:SetSel(1) return end - if value == "^7^7Sync" then - self:SyncLoadouts() + if value == "^7^7Manage Loadouts" then + self:OpenLoadoutManagePopup() self.controls.buildLoadouts:SetSel(1) return end @@ -317,40 +317,7 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin return end if value == "^7^7New Loadout" then - local controls = { } - controls.label = new("LabelControl"):LabelControl(nil, {0, 20, 0, 16}, "^7Enter name for this loadout:") - controls.edit = new("EditControl"):EditControl(nil, {0, 40, 350, 20}, "New Loadout", nil, nil, 100, function(buf) - controls.save.enabled = buf:match("%S") - end) - controls.save = new("ButtonControl"):ButtonControl(nil, {-45, 70, 80, 20}, "Save", function() - local loadout = controls.edit.buf - - local newSpec = new("PassiveSpec"):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"):ButtonControl(nil, {45, 70, 80, 20}, "Cancel", function() - main:ClosePopup() - end) - main:OpenPopup(370, 100, "Set Name", controls, "save", "edit", "cancel") - + self:OpenLoadoutNamePopup() self.controls.buildLoadouts:SetSel(1) return end @@ -758,6 +725,9 @@ end function buildMode:SyncLoadouts() self.controls.buildLoadouts.list = {"No Loadouts"} + -- Resolved loadouts (name plus the ids of the four associated sets), used by the loadout manager + self.loadoutList = self.loadoutList or { } + wipeTable(self.loadoutList) local filteredList = {"^7^7Loadouts:"} local treeList = {} @@ -797,7 +767,7 @@ function buildMode:SyncLoadouts() transferTable = {} end else - t_insert(treeList, (spec.treeVersion ~= latestTreeVersion and ("["..treeVersions[spec.treeVersion].display.."] ") or "")..(specTitle)) + t_insert(treeList, { specId = id, name = (spec.treeVersion ~= latestTreeVersion and ("["..treeVersions[spec.treeVersion].display.."] ") or "")..(specTitle) }) end end @@ -822,7 +792,7 @@ function buildMode:SyncLoadouts() transferTable = {} end else - setList[setTitle] = true + setList[setTitle] = set end end end @@ -832,15 +802,31 @@ function buildMode:SyncLoadouts() -- loop over all for exact match loadouts for id, tree in ipairs(treeList) do - if (oneItem or itemList[tree]) and (oneSkill or skillList[tree]) and (oneConfig or configList[tree]) then - t_insert(filteredList, tree) + local itemSetId = oneItem and self.itemsTab.itemSetOrderList[1] or itemList[tree.name] + local skillSetId = oneSkill and self.skillsTab.skillSetOrderList[1] or skillList[tree.name] + local configSetId = oneConfig and self.configTab.configSetOrderList[1] or configList[tree.name] + if itemSetId and skillSetId and configSetId then + t_insert(filteredList, tree.name) + t_insert(self.loadoutList, { name = tree.name, specId = tree.specId, itemSetId = itemSetId, skillSetId = skillSetId, configSetId = configSetId }) end end -- loop over the identifiers found within braces and set the loadout name to the TreeSet for _, tree in ipairs(sortedTreeListSpecialLinks) do local treeLinkId = tree.linkId - if ((oneItem or self.itemListSpecialLinks[treeLinkId]) and (oneSkill or self.skillListSpecialLinks[treeLinkId]) and (oneConfig or self.configListSpecialLinks[treeLinkId])) then - t_insert(filteredList, tree.setName .." {"..treeLinkId.."}") + local itemLink = self.itemListSpecialLinks[treeLinkId] + local skillLink = self.skillListSpecialLinks[treeLinkId] + local configLink = self.configListSpecialLinks[treeLinkId] + if ((oneItem or itemLink) and (oneSkill or skillLink) and (oneConfig or configLink)) then + local loadoutName = tree.setName .." {"..treeLinkId.."}" + t_insert(filteredList, loadoutName) + t_insert(self.loadoutList, { + name = loadoutName, + specId = tree.setId, + linkId = treeLinkId, + itemSetId = oneItem and self.itemsTab.itemSetOrderList[1] or itemLink.setId, + skillSetId = oneSkill and self.skillsTab.skillSetOrderList[1] or skillLink.setId, + configSetId = oneConfig and self.configTab.configSetOrderList[1] or configLink.setId, + }) end end end @@ -848,7 +834,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^7Sync") + t_insert(filteredList, "^7^7Manage Loadouts") t_insert(filteredList, "^7^7Help >>") if #filteredList > 0 then @@ -883,6 +869,61 @@ function buildMode:SyncLoadouts() return treeList, itemList, skillList, configList end +-- Creates a new loadout: a passive tree, item set, skill set and config set sharing the given name +function buildMode:NewLoadout(loadoutName) + local newSpec = new("PassiveSpec"):PassiveSpec(self, latestTreeVersion) + newSpec.title = loadoutName + t_insert(self.treeTab.specList, newSpec) + + local itemSet = self.itemsTab:NewItemSet(#self.itemsTab.itemSets + 1) + t_insert(self.itemsTab.itemSetOrderList, itemSet.id) + itemSet.title = loadoutName + + local skillSet = self.skillsTab:NewSkillSet(#self.skillsTab.skillSets + 1) + t_insert(self.skillsTab.skillSetOrderList, skillSet.id) + skillSet.title = loadoutName + + local configSet = self.configTab:NewConfigSet(#self.configTab.configSets + 1) + t_insert(self.configTab.configSetOrderList, configSet.id) + configSet.title = loadoutName + + self:SyncLoadouts() + self.modFlag = true +end + +-- Opens the naming popup for a new loadout +function buildMode:OpenLoadoutNamePopup() + local controls = { } + controls.label = new("LabelControl"):LabelControl(nil, {0, 20, 0, 16}, "^7Enter name for this loadout:") + controls.edit = new("EditControl"):EditControl(nil, {0, 40, 350, 20}, "New Loadout", nil, nil, 100, function(buf) + controls.save.enabled = buf:match("%S") + end) + controls.save = new("ButtonControl"):ButtonControl(nil, {-45, 70, 80, 20}, "Save", function() + self:NewLoadout(controls.edit.buf) + main:ClosePopup() + end) + controls.save.enabled = false + controls.cancel = new("ButtonControl"):ButtonControl(nil, {45, 70, 80, 20}, "Cancel", function() + main:ClosePopup() + end) + main:OpenPopup(370, 100, "Set Name", controls, "save", "edit", "cancel") +end + +-- Opens the loadout manager +function buildMode:OpenLoadoutManagePopup() + local syncButton = new("ButtonControl"):ButtonControl(nil, {-50, 260, 90, 20}, "Sync", function() + self:SyncLoadouts() + end) + syncButton.tooltipText = "Force the loadout list to update in case sets have been changed behind the scenes." + main:OpenPopup(470, 290, "Manage Loadouts", { + new("LoadoutListControl"):LoadoutListControl(nil, {0, 50, 450, 200}, self), + syncButton, + new("ButtonControl"):ButtonControl(nil, {50, 260, 90, 20}, "Done", function() + main:ClosePopup() + end), + }) +end + function buildMode:EstimatePlayerProgress() if self.spec then local PointsUsed, AscUsed, SecondaryAscUsed = self.spec:CountAllocNodes() From 1090b85fdaa0714d9a21db6432f19802fd5b07c2 Mon Sep 17 00:00:00 2001 From: Can Uysal Date: Sat, 15 Aug 2026 11:09:06 +0300 Subject: [PATCH 2/2] Add custom loadout creation to the loadout manager * Add "New/Copy Custom" button, which creates a loadout choosing per set whether to start fresh or copy an existing one * Default the choices to the currently active sets, so saving immediately copies the active loadout * Split the per-set copy and delete logic out of Copy/Delete so both paths share it * Add tests for copying the untitled default loadout, deleting a loadout, keeping shared sets, and resetting the dropdown selection --- help.txt | 1 + spec/System/TestLoadouts_spec.lua | 130 ++++++++++++ src/Classes/LoadoutListControl.lua | 319 ++++++++++++++++++++--------- 3 files changed, 351 insertions(+), 99 deletions(-) diff --git a/help.txt b/help.txt index f05c843fec3..b071e6d8304 100644 --- a/help.txt +++ b/help.txt @@ -137,6 +137,7 @@ The "New Loadout" option allows the user to create all four sets from a single p The "Manage Loadouts" option opens the loadout manager, which works like the set managers found on the Tree, Skills, Items, and Configuration tabs: - "New" creates all four sets from a single popup, the same as the "New Loadout" option - "Copy" duplicates all four sets of the selected loadout into a new loadout automatically + - "New/Copy Custom" creates a loadout from a mix of sets, choosing for each of the four whether to start fresh or copy an existing set. It defaults to the currently active sets, so saving straight away copies the active loadout - "Rename" renames all four sets of the selected loadout, preserving any identifiers inside braces { } - "Delete" deletes all four sets of the selected loadout; sets shared with other loadouts, or the last remaining set of a type, are kept - Double-clicking a loadout activates it diff --git a/spec/System/TestLoadouts_spec.lua b/spec/System/TestLoadouts_spec.lua index 6d877d50482..0b7997125c6 100644 --- a/spec/System/TestLoadouts_spec.lua +++ b/spec/System/TestLoadouts_spec.lua @@ -51,6 +51,136 @@ describe("TestLoadouts", function() assert.is_true(copy2Found) end) + it("creates a custom loadout from a mix of new and copied sets", function() + build:NewLoadout("Source") + local listControl = new("LoadoutListControl"):LoadoutListControl(nil, {0, 0, 450, 200}, build) + local source + for _, entry in ipairs(build.loadoutList) do + if entry.name == "Source" then + source = entry + end + end + assert.is_not_nil(source) + + -- Copy the tree and item set, but start the skill and config sets fresh + listControl:CreateLoadout("Custom", source.specId, source.itemSetId, nil, nil) + local custom + for _, entry in ipairs(build.loadoutList) do + if entry.name == "Custom" then + custom = entry + end + end + assert.is_not_nil(custom) + -- Every set is newly created, so none are shared with the source loadout + assert.are_not.equals(source.specId, custom.specId) + assert.are_not.equals(source.itemSetId, custom.itemSetId) + assert.are_not.equals(source.skillSetId, custom.skillSetId) + assert.are_not.equals(source.configSetId, custom.configSetId) + assert.are.equals("Custom", build.treeTab.specList[custom.specId].title) + assert.are.equals("Custom", build.itemsTab.itemSets[custom.itemSetId].title) + assert.are.equals("Custom", build.skillsTab.skillSets[custom.skillSetId].title) + assert.are.equals("Custom", build.configTab.configSets[custom.configSetId].title) + end) + + it("creates an all new custom loadout when nothing is copied", function() + local listControl = new("LoadoutListControl"):LoadoutListControl(nil, {0, 0, 450, 200}, build) + listControl:CreateLoadout("All New", nil, nil, nil, nil) + local found = false + for _, entry in ipairs(build.loadoutList) do + if entry.name == "All New" then + found = true + end + end + assert.is_true(found) + end) + + it("generates unique default names for custom loadouts", function() + local listControl = new("LoadoutListControl"):LoadoutListControl(nil, {0, 0, 450, 200}, build) + assert.are.equals("New Loadout Custom", listControl:UniqueName("New Loadout Custom")) + listControl:CreateLoadout("New Loadout Custom", nil, nil, nil, nil) + assert.are.equals("New Loadout Custom 2", listControl:UniqueName("New Loadout Custom")) + end) + + it("copies the untitled default loadout without erroring", function() + local listControl = new("LoadoutListControl"):LoadoutListControl(nil, {0, 0, 450, 200}, build) + -- A fresh build has one untitled set of each type, which resolves to a "Default" loadout + assert.are.equals(1, #build.loadoutList) + assert.are.equals("Default", build.loadoutList[1].name) + listControl:CopyLoadout(build.loadoutList[1]) + local copyFound = false + for _, entry in ipairs(build.loadoutList) do + if entry.name == "Default (copy)" then + copyFound = true + end + end + assert.is_true(copyFound) + end) + + it("deletes a newly added loadout without breaking the remaining sets", function() + build:NewLoadout("Second") + local listControl = new("LoadoutListControl"):LoadoutListControl(nil, {0, 0, 450, 200}, build) + assert.are.equals(2, #build.loadoutList) + local second + for _, entry in ipairs(build.loadoutList) do + if entry.name == "Second" then + second = entry + end + end + assert.is_not_nil(second) + + listControl:DeleteLoadout(second) + + assert.are.equals(1, #build.loadoutList) + assert.are.equals(1, #build.treeTab.specList) + assert.are.equals(1, #build.itemsTab.itemSetOrderList) + assert.are.equals(1, #build.skillsTab.skillSetOrderList) + assert.are.equals(1, #build.configTab.configSetOrderList) + -- The active sets must still point at sets that exist + assert.is_not_nil(build.treeTab.specList[build.treeTab.activeSpec]) + assert.is_not_nil(build.itemsTab.itemSets[build.itemsTab.activeItemSetId]) + assert.is_not_nil(build.skillsTab.skillSets[build.skillsTab.activeSkillSetId]) + assert.is_not_nil(build.configTab.configSets[build.configTab.activeConfigSetId]) + end) + + it("keeps sets that are shared with another loadout when deleting", function() + -- Two trees sharing a single item, skill and config set each + build.treeTab.specList[1].title = "Shared A" + local listControl = new("LoadoutListControl"):LoadoutListControl(nil, {0, 0, 450, 200}, build) + listControl:AddSpec(1, "Shared B") + build:SyncLoadouts() + assert.are.equals(2, #build.loadoutList) + + local sharedItemSetId = build.itemsTab.itemSetOrderList[1] + local toDelete + for _, entry in ipairs(build.loadoutList) do + if entry.name == "Shared B" then + toDelete = entry + end + end + listControl:DeleteLoadout(toDelete) + + -- The tree is gone, but the single item set is still used by "Shared A" + assert.are.equals(1, #build.treeTab.specList) + assert.is_not_nil(build.itemsTab.itemSets[sharedItemSetId]) + assert.are.equals(1, #build.loadoutList) + end) + + it("resets the dropdown selection after opening the manager", function() + local dropdown = build.controls.buildLoadouts + local manageIndex + for i, value in ipairs(dropdown.list) do + if value == "^7^7Manage Loadouts" then + manageIndex = i + end + end + assert.is_not_nil(manageIndex) + dropdown:SetSel(manageIndex) + -- Selection must fall back to the header, otherwise picking "Manage Loadouts" + -- a second time would not fire the callback again + assert.are.equals(1, dropdown.selIndex) + main:ClosePopup() + end) + it("resolves link identifier loadouts", function() build.treeTab.specList[1].title = "Tree {A}" local itemSet = build.itemsTab.itemSets[build.itemsTab.itemSetOrderList[1]] diff --git a/src/Classes/LoadoutListControl.lua b/src/Classes/LoadoutListControl.lua index 6716cb132dd..7e192c045f8 100644 --- a/src/Classes/LoadoutListControl.lua +++ b/src/Classes/LoadoutListControl.lua @@ -13,26 +13,33 @@ local LoadoutListClass = newClass("LoadoutListControl", "ListControl") function LoadoutListClass:LoadoutListControl(anchor, rect, build) self:ListControl(anchor, rect, 16, "VERTICAL", false, build.loadoutList) self.build = build - self.controls.copy = new("ButtonControl"):ButtonControl({"BOTTOMLEFT",self,"TOP"}, {2, -4, 60, 18}, "Copy", function() + -- The button row spans the full width of the list: four equal buttons plus the wider + -- "New/Copy Custom", separated by 4px gaps + local buttonWidth, buttonGap = 78, 4 + local customWidth = rect[3] - buttonWidth * 4 - buttonGap * 4 + self.controls.new = new("ButtonControl"):ButtonControl({"BOTTOMLEFT",self,"TOPLEFT"}, {0, -4, buttonWidth, 18}, "New", function() + build:OpenLoadoutNamePopup() + end) + self.controls.rename = new("ButtonControl"):ButtonControl({"LEFT",self.controls.new,"RIGHT"}, {buttonGap, 0, buttonWidth, 18}, "Rename", function() + self:RenameLoadout(self.selValue) + end) + self.controls.rename.enabled = function() + return self.selValue ~= nil + end + self.controls.copy = new("ButtonControl"):ButtonControl({"LEFT",self.controls.rename,"RIGHT"}, {buttonGap, 0, buttonWidth, 18}, "Copy", function() self:CopyLoadout(self.selValue) end) self.controls.copy.enabled = function() return self.selValue ~= nil end - self.controls.delete = new("ButtonControl"):ButtonControl({"LEFT",self.controls.copy,"RIGHT"}, {4, 0, 60, 18}, "Delete", function() + self.controls.delete = new("ButtonControl"):ButtonControl({"LEFT",self.controls.copy,"RIGHT"}, {buttonGap, 0, buttonWidth, 18}, "Delete", function() self:OnSelDelete(self.selIndex, self.selValue) end) self.controls.delete.enabled = function() return self.selValue ~= nil end - self.controls.rename = new("ButtonControl"):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"):ButtonControl({"RIGHT",self.controls.rename,"LEFT"}, {-4, 0, 60, 18}, "New", function() - build:OpenLoadoutNamePopup() + self.controls.custom = new("ButtonControl"):ButtonControl({"LEFT",self.controls.delete,"RIGHT"}, {buttonGap, 0, customWidth, 18}, "New/Copy Custom", function() + self:CreateCustomLoadoutPopup() end) return self end @@ -69,75 +76,183 @@ function LoadoutListClass:SelectLoadoutBySpecId(specId) end end --- Copies all four sets of the given loadout into a new loadout -function LoadoutListClass:CopyLoadout(loadout) - local build = self.build - local baseName = (loadout.name:gsub("%s*%{[%w,]+%}", "")) - if baseName == "" then - baseName = "Default" - end - local newName = baseName .. " (copy)" +-- Returns a loadout name not already in use, appending a numeric suffix if needed +function LoadoutListClass:UniqueName(baseName, copySuffix) + local newName = baseName .. (copySuffix and " (copy)" or "") local suffix = 1 while self:NameInUse(newName) do suffix = suffix + 1 - newName = baseName .. " (copy " .. suffix .. ")" + newName = baseName .. (copySuffix and (" (copy " .. suffix .. ")") or (" " .. suffix)) end + return newName +end - local spec = build.treeTab.specList[loadout.specId] - local newSpec = new("PassiveSpec"):PassiveSpec(build, spec.treeVersion) - newSpec.title = newName - newSpec.jewels = copyTable(spec.jewels) - newSpec:RestoreUndoState(spec:CreateUndoState()) - newSpec:BuildClusterJewelGraphs() +-- Adds a passive tree, either a copy of the given one or a fresh tree, and returns its index +function LoadoutListClass:AddSpec(specId, name) + local build = self.build + local newSpec + if specId then + local spec = build.treeTab.specList[specId] + newSpec = new("PassiveSpec"):PassiveSpec(build, spec.treeVersion) + newSpec.jewels = copyTable(spec.jewels) + newSpec:RestoreUndoState(spec:CreateUndoState()) + newSpec:BuildClusterJewelGraphs() + else + newSpec = new("PassiveSpec"):PassiveSpec(build, latestTreeVersion) + end + newSpec.title = name t_insert(build.treeTab.specList, newSpec) build.treeTab.modFlag = true + return #build.treeTab.specList +end - local itemsTab = build.itemsTab - local newItemSet = copyTable(itemsTab.itemSets[loadout.itemSetId]) - newItemSet.title = newName - newItemSet.id = 1 - while itemsTab.itemSets[newItemSet.id] do - newItemSet.id = newItemSet.id + 1 +-- Adds an item set, either a copy of the given one or a fresh set, and returns its id +function LoadoutListClass:AddItemSet(itemSetId, name) + local itemsTab = self.build.itemsTab + local newSet + if itemSetId then + newSet = copyTable(itemsTab.itemSets[itemSetId]) + newSet.id = 1 + while itemsTab.itemSets[newSet.id] do + newSet.id = newSet.id + 1 + end + itemsTab.itemSets[newSet.id] = newSet + else + newSet = itemsTab:NewItemSet() end - itemsTab.itemSets[newItemSet.id] = newItemSet - t_insert(itemsTab.itemSetOrderList, newItemSet.id) + newSet.title = name + t_insert(itemsTab.itemSetOrderList, newSet.id) itemsTab:AddUndoState() + return newSet.id +end - local skillsTab = build.skillsTab - local skillSet = skillsTab.skillSets[loadout.skillSetId] - local newSkillSet = copyTable(skillSet, true) - newSkillSet.title = newName - newSkillSet.socketGroupList = { } - for _, 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) +-- Adds a skill set, either a copy of the given one or a fresh set, and returns its id +function LoadoutListClass:AddSkillSet(skillSetId, name) + local skillsTab = self.build.skillsTab + local newSet + if skillSetId then + local skillSet = skillsTab.skillSets[skillSetId] + newSet = copyTable(skillSet, true) + newSet.socketGroupList = { } + for _, 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(newSet.socketGroupList, newGroup) end - t_insert(newSkillSet.socketGroupList, newGroup) - end - newSkillSet.id = 1 - while skillsTab.skillSets[newSkillSet.id] do - newSkillSet.id = newSkillSet.id + 1 + newSet.id = 1 + while skillsTab.skillSets[newSet.id] do + newSet.id = newSet.id + 1 + end + skillsTab.skillSets[newSet.id] = newSet + else + newSet = skillsTab:NewSkillSet() end - skillsTab.skillSets[newSkillSet.id] = newSkillSet - t_insert(skillsTab.skillSetOrderList, newSkillSet.id) + newSet.title = name + t_insert(skillsTab.skillSetOrderList, newSet.id) skillsTab:AddUndoState() + return newSet.id +end - local configTab = build.configTab - local newConfigSet = copyTable(configTab.configSets[loadout.configSetId]) - newConfigSet.title = newName - newConfigSet.id = 1 - while configTab.configSets[newConfigSet.id] do - newConfigSet.id = newConfigSet.id + 1 +-- Adds a config set, either a copy of the given one or a fresh set, and returns its id +function LoadoutListClass:AddConfigSet(configSetId, name) + local configTab = self.build.configTab + local newSet + if configSetId then + newSet = copyTable(configTab.configSets[configSetId]) + newSet.id = 1 + while configTab.configSets[newSet.id] do + newSet.id = newSet.id + 1 + end + configTab.configSets[newSet.id] = newSet + else + newSet = configTab:NewConfigSet() end - configTab.configSets[newConfigSet.id] = newConfigSet - t_insert(configTab.configSetOrderList, newConfigSet.id) + newSet.title = name + t_insert(configTab.configSetOrderList, newSet.id) configTab:AddUndoState() + return newSet.id +end +-- Creates a loadout from the given sets; a nil set id creates a fresh set of that type +function LoadoutListClass:CreateLoadout(name, specId, itemSetId, skillSetId, configSetId) + local specIndex = self:AddSpec(specId, name) + self:AddItemSet(itemSetId, name) + self:AddSkillSet(skillSetId, name) + self:AddConfigSet(configSetId, name) + self.build.modFlag = true self:UpdateItemsTabPassiveTreeDropdown() - build:SyncLoadouts() - self:SelectLoadoutBySpecId(#build.treeTab.specList) + self.build:SyncLoadouts() + self:SelectLoadoutBySpecId(specIndex) +end + +-- Copies all four sets of the given loadout into a new loadout +function LoadoutListClass:CopyLoadout(loadout) + local baseName = (loadout.name:gsub("%s*%{[%w,]+%}", "")) + if baseName == "" then + baseName = "Default" + end + self:CreateLoadout(self:UniqueName(baseName, true), loadout.specId, loadout.itemSetId, loadout.skillSetId, loadout.configSetId) +end + +-- Opens the popup for creating a loadout from a chosen mix of new and existing sets +function LoadoutListClass:CreateCustomLoadoutPopup() + local build = self.build + local controls = { } + + -- The first entry creates a fresh set, the rest copy an existing one + local function buildSetList(orderList, sets) + local list = { { label = "New" } } + for _, setId in ipairs(orderList) do + t_insert(list, { label = sets[setId].title or "Default", id = setId }) + end + return list + end + local treeList = { { label = "New" } } + for specId, spec in ipairs(build.treeTab.specList) do + t_insert(treeList, { + label = (spec.treeVersion ~= latestTreeVersion and ("["..treeVersions[spec.treeVersion].display.."] ") or "")..(spec.title or "Default"), + id = specId, + }) + end + + controls.label = new("LabelControl"):LabelControl(nil, {0, 20, 0, 16}, "^7Enter name for this loadout:") + controls.edit = new("EditControl"):EditControl(nil, {0, 40, 350, 20}, self:UniqueName("New Loadout Custom"), nil, nil, 100, function(buf) + controls.save.enabled = buf:match("%S") + end) + controls.treeSelect = new("DropDownControl"):DropDownControl(nil, {0, 90, 350, 20}, treeList) + controls.treeLabel = new("LabelControl"):LabelControl({"BOTTOMLEFT",controls.treeSelect,"TOPLEFT"}, {0, -4, 0, 16}, "^7Copy from Tree:") + controls.skillSelect = new("DropDownControl"):DropDownControl(nil, {0, 140, 350, 20}, buildSetList(build.skillsTab.skillSetOrderList, build.skillsTab.skillSets)) + controls.skillLabel = new("LabelControl"):LabelControl({"BOTTOMLEFT",controls.skillSelect,"TOPLEFT"}, {0, -4, 0, 16}, "^7Copy from Skill Set:") + controls.itemSelect = new("DropDownControl"):DropDownControl(nil, {0, 190, 350, 20}, buildSetList(build.itemsTab.itemSetOrderList, build.itemsTab.itemSets)) + controls.itemLabel = new("LabelControl"):LabelControl({"BOTTOMLEFT",controls.itemSelect,"TOPLEFT"}, {0, -4, 0, 16}, "^7Copy from Item Set:") + controls.configSelect = new("DropDownControl"):DropDownControl(nil, {0, 240, 350, 20}, buildSetList(build.configTab.configSetOrderList, build.configTab.configSets)) + controls.configLabel = new("LabelControl"):LabelControl({"BOTTOMLEFT",controls.configSelect,"TOPLEFT"}, {0, -4, 0, 16}, "^7Copy from Config Set:") + + -- Every set defaults to "New"; if a loadout is selected in the manager, its sets are + -- preselected instead so they can be kept or swapped out one at a time + local selected = self.selValue + if selected then + controls.treeSelect:SelByValue(selected.specId, "id") + controls.skillSelect:SelByValue(selected.skillSetId, "id") + controls.itemSelect:SelByValue(selected.itemSetId, "id") + controls.configSelect:SelByValue(selected.configSetId, "id") + end + + controls.save = new("ButtonControl"):ButtonControl(nil, {-45, 275, 80, 20}, "Save", function() + self:CreateLoadout(controls.edit.buf, + controls.treeSelect:GetSelValue().id, + controls.itemSelect:GetSelValue().id, + controls.skillSelect:GetSelValue().id, + controls.configSelect:GetSelValue().id) + main:ClosePopup() + end) + controls.cancel = new("ButtonControl"):ButtonControl(nil, {45, 275, 80, 20}, "Cancel", function() + main:ClosePopup() + end) + main:OpenPopup(370, 305, "Create Custom Loadout", controls, "save", "edit", "cancel") end function LoadoutListClass:RenameLoadout(loadout) @@ -213,50 +328,56 @@ function LoadoutListClass:OnSelClick(index, loadout, doubleClick) end end -function LoadoutListClass:OnSelDelete(index, loadout) +-- Deletes the four sets of the given loadout, keeping any that are shared with another +-- loadout or that are the last remaining set of their type +function LoadoutListClass:DeleteLoadout(loadout) local build = self.build - main:OpenConfirmPopup("Delete Loadout", "Are you sure you want to delete '"..loadout.name.."'?\nThis will delete the passive tree, item set, skill set and config set associated with it.\nSets shared with other loadouts, or the last remaining set of a type, will be kept.", "Delete", function() - local treeTab, itemsTab, skillsTab, configTab = build.treeTab, build.itemsTab, build.skillsTab, build.configTab - if #treeTab.specList > 1 and not self:IsSetShared("specId", loadout.specId, loadout) then - t_remove(treeTab.specList, loadout.specId) - if loadout.specId == treeTab.activeSpec then - treeTab:SetActiveSpec(m_max(1, loadout.specId - 1)) - else - treeTab.activeSpec = isValueInArray(treeTab.specList, build.spec) - end - treeTab.modFlag = true + local treeTab, itemsTab, skillsTab, configTab = build.treeTab, build.itemsTab, build.skillsTab, build.configTab + if #treeTab.specList > 1 and not self:IsSetShared("specId", loadout.specId, loadout) then + t_remove(treeTab.specList, loadout.specId) + if loadout.specId == treeTab.activeSpec then + treeTab:SetActiveSpec(m_max(1, loadout.specId - 1)) + else + treeTab.activeSpec = isValueInArray(treeTab.specList, build.spec) end - if #itemsTab.itemSetOrderList > 1 and not self:IsSetShared("itemSetId", loadout.itemSetId, loadout) then - local setIndex = isValueInArray(itemsTab.itemSetOrderList, loadout.itemSetId) - t_remove(itemsTab.itemSetOrderList, setIndex) - itemsTab.itemSets[loadout.itemSetId] = nil - if loadout.itemSetId == itemsTab.activeItemSetId then - itemsTab:SetActiveItemSet(itemsTab.itemSetOrderList[m_max(1, setIndex - 1)]) - end - itemsTab:AddUndoState() + treeTab.modFlag = true + end + if #itemsTab.itemSetOrderList > 1 and not self:IsSetShared("itemSetId", loadout.itemSetId, loadout) then + local setIndex = isValueInArray(itemsTab.itemSetOrderList, loadout.itemSetId) + t_remove(itemsTab.itemSetOrderList, setIndex) + itemsTab.itemSets[loadout.itemSetId] = nil + if loadout.itemSetId == itemsTab.activeItemSetId then + itemsTab:SetActiveItemSet(itemsTab.itemSetOrderList[m_max(1, setIndex - 1)]) end - if #skillsTab.skillSetOrderList > 1 and not self:IsSetShared("skillSetId", loadout.skillSetId, loadout) then - local setIndex = isValueInArray(skillsTab.skillSetOrderList, loadout.skillSetId) - t_remove(skillsTab.skillSetOrderList, setIndex) - skillsTab.skillSets[loadout.skillSetId] = nil - if loadout.skillSetId == skillsTab.activeSkillSetId then - skillsTab:SetActiveSkillSet(skillsTab.skillSetOrderList[m_max(1, setIndex - 1)]) - end - skillsTab:AddUndoState() + itemsTab:AddUndoState() + end + if #skillsTab.skillSetOrderList > 1 and not self:IsSetShared("skillSetId", loadout.skillSetId, loadout) then + local setIndex = isValueInArray(skillsTab.skillSetOrderList, loadout.skillSetId) + t_remove(skillsTab.skillSetOrderList, setIndex) + skillsTab.skillSets[loadout.skillSetId] = nil + if loadout.skillSetId == skillsTab.activeSkillSetId then + skillsTab:SetActiveSkillSet(skillsTab.skillSetOrderList[m_max(1, setIndex - 1)]) end - if #configTab.configSetOrderList > 1 and not self:IsSetShared("configSetId", loadout.configSetId, loadout) then - local setIndex = isValueInArray(configTab.configSetOrderList, loadout.configSetId) - t_remove(configTab.configSetOrderList, setIndex) - configTab.configSets[loadout.configSetId] = nil - if loadout.configSetId == configTab.activeConfigSetId then - configTab:SetActiveConfigSet(configTab.configSetOrderList[m_max(1, setIndex - 1)]) - end - configTab:AddUndoState() + skillsTab:AddUndoState() + end + if #configTab.configSetOrderList > 1 and not self:IsSetShared("configSetId", loadout.configSetId, loadout) then + local setIndex = isValueInArray(configTab.configSetOrderList, loadout.configSetId) + t_remove(configTab.configSetOrderList, setIndex) + configTab.configSets[loadout.configSetId] = nil + if loadout.configSetId == configTab.activeConfigSetId then + configTab:SetActiveConfigSet(configTab.configSetOrderList[m_max(1, setIndex - 1)]) end - self.selIndex = nil - self.selValue = nil - self:UpdateItemsTabPassiveTreeDropdown() - build:SyncLoadouts() + configTab:AddUndoState() + end + self.selIndex = nil + self.selValue = nil + self:UpdateItemsTabPassiveTreeDropdown() + build:SyncLoadouts() +end + +function LoadoutListClass:OnSelDelete(index, loadout) + main:OpenConfirmPopup("Delete Loadout", "Are you sure you want to delete '"..loadout.name.."'?\nThis will delete the passive tree, item set, skill set and config set associated with it.\nSets shared with other loadouts, or the last remaining set of a type, will be kept.", "Delete", function() + self:DeleteLoadout(loadout) end) end