diff --git a/help.txt b/help.txt index b728173120..b071e6d830 100644 --- a/help.txt +++ b/help.txt @@ -134,7 +134,14 @@ 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 + - "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 + - "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 0000000000..0b7997125c --- /dev/null +++ b/spec/System/TestLoadouts_spec.lua @@ -0,0 +1,200 @@ +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("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]] + 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 0000000000..7e192c045f --- /dev/null +++ b/src/Classes/LoadoutListControl.lua @@ -0,0 +1,399 @@ +-- 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 + -- 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"}, {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.custom = new("ButtonControl"):ButtonControl({"LEFT",self.controls.delete,"RIGHT"}, {buttonGap, 0, customWidth, 18}, "New/Copy Custom", function() + self:CreateCustomLoadoutPopup() + 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 + +-- 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 .. (copySuffix and (" (copy " .. suffix .. ")") or (" " .. suffix)) + end + return newName +end + +-- 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 + +-- 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 + newSet.title = name + t_insert(itemsTab.itemSetOrderList, newSet.id) + itemsTab:AddUndoState() + return newSet.id +end + +-- 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 + 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 + newSet.title = name + t_insert(skillsTab.skillSetOrderList, newSet.id) + skillsTab:AddUndoState() + return newSet.id +end + +-- 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 + 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() + 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) + 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 + +-- 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 + 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 + +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 + +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 dcfbc74e89..96e3c48b5b 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()