From 2f99b7d603205dc3fa389794fdb2df21dcbd79a7 Mon Sep 17 00:00:00 2001 From: Peechey <92683202+Peechey@users.noreply.github.com> Date: Sun, 21 Jul 2024 16:06:09 -0600 Subject: [PATCH] Add support for multiple configurations (#7212) * add support for multiple configurations * fix scrolling * defaultIndex for boss preset and damageType dropdowns * fix for custom mods built into modList * clear configs before copying over changes for values that do not overlap between sets when changing activeSet * add configSets to Loadouts fix bug with Item/Skill/Config SetListControls refactor functions in Loadout logic for identical code for item/skill/config add SyncLoadouts to Delete in SetListControls and Convert functions in TreeTab update help section * add Sync to PassiveSpecListControl Delete * add Sync to ConfigSet Rename * Add unique formatting to options in dropdowns to avoid conflicting with user-created loadouts * revert bugfix as it caused another bug * typos * remove duplicateCheck and recolor logic, fix duplicate scenario where sets have same name and same group * typo * refactor doubling up of setting values, use self.configSets[self.activeConfigSetId] everywhere possible * fix for the "Default" loadout update Help Section with colour formatting info fix bug when reordering Tree Sets that are actively in loadouts * Alter scrollbar height for new UI elements * Sort loadouts by set name if it's there * Sort loadouts by tree order + properly reset all dropdowns --------- Co-authored-by: Wires77 --- help.txt | 17 +- src/Classes/ConfigSetListControl.lua | 110 +++++++++++ src/Classes/ConfigTab.lua | 253 ++++++++++++++++++------- src/Classes/ItemSetListControl.lua | 1 + src/Classes/PassiveSpecListControl.lua | 2 + src/Classes/SkillSetListControl.lua | 1 + src/Classes/TreeTab.lua | 2 + src/Modules/Build.lua | 151 +++++++-------- src/Modules/ConfigOptions.lua | 12 +- 9 files changed, 391 insertions(+), 158 deletions(-) create mode 100644 src/Classes/ConfigSetListControl.lua diff --git a/help.txt b/help.txt index 6a958722..8a0dfb39 100644 --- a/help.txt +++ b/help.txt @@ -115,12 +115,17 @@ Advanced tricks: ---[Loadouts] -Loadouts can be selected from the dropdown in the top middle of the Build Tab. Selecting a Loadout will load all three sets at once. These are automatically registered based on one of two conditions: - 1) All three sets share the same name, e.g. "Leveling" - 2) All three sets have the same alphanumeric identifier inside of braces { } at the end of the name, e.g. "Leveling Tree {1}", "Leveling Items {1}", "Leveling Skills {1}" - - If you would like a single set to be used in multiple Loadouts, you can put the identifiers in the braces separated by commas. For example, an Item Set could be named "Early Game {1,2}" and this would be recognized as a loadout given there's a Tree and Skill Set with {1}, {2}, or {1,2} in their names as well - - Lastly, the name of the Loadout in the dropdown is based on the name of the Tree Set, identifiers are shown for clarity when using sets multiple times -The "New Loadout" option allows the user to create all three sets from a single popup for convenience. +Loadouts can be selected from the dropdown in the top middle of the screen. Selecting a Loadout will load all four sets at once. These are automatically registered based on one of two conditions: + + 1) All four sets share the same name and colour formatting, e.g. "Leveling" + - If you have a set named ^4Leveling^7, it will not match to other sets named Leveling + + 2) All four sets have the same alphanumeric identifier inside of braces { } at the end of the name, e.g. "Leveling Tree {1}", "Leveling Items {1}", "Leveling Skills {1}", "Leveling Config {1}" + - If you would like a single set to be used in multiple Loadouts, you can put the identifiers in the braces separated by commas. For example, an Item Set could be named "Early Game {1,2}" and there could be Tree, Skill, and Config Sets with {1} and {2}, resulting in two loadouts linked to the same Item Set + - The name of the Loadout in the dropdown is based on the name of the Tree Set, identifiers are shown for clarity when using sets multiple times + - These sets can have differing colour formatting so long as the identifier texts match + +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. diff --git a/src/Classes/ConfigSetListControl.lua b/src/Classes/ConfigSetListControl.lua new file mode 100644 index 00000000..b87b3f5a --- /dev/null +++ b/src/Classes/ConfigSetListControl.lua @@ -0,0 +1,110 @@ +-- Path of Building +-- +-- Class: Config Set List +-- Config Set list control +-- +local t_insert = table.insert +local t_remove = table.remove +local m_max = math.max + +local ConfigSetListClass = newClass("ConfigSetListControl", "ListControl", function(self, anchor, x, y, width, height, configTab) + self.ListControl(anchor, x, y, width, height, 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, true) + newConfigSet.id = 1 + while configTab.configSets[newConfigSet.id] do + newConfigSet.id = newConfigSet.id + 1 + end + configTab.configSets[newConfigSet.id] = newConfigSet + self:RenameSet(newConfigSet, true) + 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:RenameSet(configTab.configSets[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:RenameSet(configTab:NewConfigSet(), true) + end) +end) + +function ConfigSetListClass:RenameSet(configSet, addOnName) + local controls = { } + controls.label = new("LabelControl", nil, 0, 20, 0, 16, "^7Enter name for this config set:") + controls.edit = new("EditControl", nil, 0, 40, 350, 20, configSet.title, nil, nil, 100, function(buf) + controls.save.enabled = buf:match("%S") + end) + controls.save = new("ButtonControl", nil, -45, 70, 80, 20, "Save", function() + configSet.title = controls.edit.buf + self.configTab.modFlag = true + if addOnName then + t_insert(self.list, configSet.id) + self.selIndex = #self.list + self.selValue = configSet + end + self.configTab:AddUndoState() + self.configTab.build:SyncLoadouts() + main:ClosePopup() + end) + controls.save.enabled = false + controls.cancel = new("ButtonControl", nil, 45, 70, 80, 20, "Cancel", function() + if addOnName then + self.configTab.configSets[configSet.id] = nil + end + main:ClosePopup() + end) + main:OpenPopup(370, 100, configSet.title and "Rename" or "Set Name", controls, "save", "edit", "cancel") +end + +function ConfigSetListClass:GetRowValue(column, index, configSetId) + local configSet = self.configTab.configSets[configSetId] + if column == 1 then + return (configSet.title or "Default") .. (configSetId == self.configTab.activeConfigSetId and " ^9(Current)" or "") + end +end + +function ConfigSetListClass:OnOrderChange() + self.configTab.modFlag = true +end + +function ConfigSetListClass:OnSelClick(index, configSetId, doubleClick) + if doubleClick and configSetId ~= self.configTab.activeConfigSetId then + self.configTab:SetActiveConfigSet(configSetId) + self.configTab:AddUndoState() + end +end + +function ConfigSetListClass:OnSelDelete(index, configSetId) + local configSet = self.configTab.configSets[configSetId] + if #self.list > 1 then + main:OpenConfirmPopup("Delete Config Set", "Are you sure you want to delete '"..(configSet.title or "Default").."'?", "Delete", function() + t_remove(self.list, index) + self.configTab.configSets[configSetId] = nil + self.selIndex = nil + self.selValue = nil + if configSetId == self.configTab.activeConfigSetId then + self.configTab:SetActiveConfigSet(self.list[m_max(1, index - 1)]) + end + self.configTab:AddUndoState() + self.configTab.build:SyncLoadouts() + end) + end +end + +function ConfigSetListClass:OnSelKeyDown(index, configSetId, key) + if key == "F2" then + self:RenameSet(self.configTab.configSets[configSetId]) + end +end diff --git a/src/Classes/ConfigTab.lua b/src/Classes/ConfigTab.lua index 3a28a3ad..25301a06 100644 --- a/src/Classes/ConfigTab.lua +++ b/src/Classes/ConfigTab.lua @@ -21,18 +21,37 @@ local ConfigTabClass = newClass("ConfigTab", "UndoHandler", "ControlHost", "Cont self.input = { } self.placeholder = { } self.defaultState = { } + + -- Initialise config sets + self.configSets = { } + self.configSetOrderList = { 1 } + self:NewConfigSet(1) + self:SetActiveConfigSet(1, true) self.enemyLevel = 1 self.sectionList = { } self.varControls = { } - self:BuildModList() - self.toggleConfigs = false self.controls.sectionAnchor = new("LabelControl", { "TOPLEFT", self, "TOPLEFT" }, 0, 20, 0, 0, "") - self.controls.search = new("EditControl", { "TOPLEFT", self.controls.sectionAnchor, "TOPLEFT" }, 8, -15, 360, 20, "", "Search", "%c", 100, function() + + -- Set selector + self.controls.setSelect = new("DropDownControl", { "TOPLEFT", self.controls.sectionAnchor, "TOPLEFT" }, 76, -12, 210, 20, nil, function(index, value) + self:SetActiveConfigSet(self.configSetOrderList[index]) + self:AddUndoState() + end) + self.controls.setSelect.enableDroppedWidth = true + self.controls.setSelect.enabled = function() + return #self.configSetOrderList > 1 + end + self.controls.setLabel = new("LabelControl", { "RIGHT", self.controls.setSelect, "LEFT" }, -2, 0, 0, 16, "^7Config set:") + self.controls.setManage = new("ButtonControl", { "LEFT", self.controls.setSelect, "RIGHT" }, 4, 0, 90, 20, "Manage...", function() + self:OpenConfigSetManagePopup() + end) + + self.controls.search = new("EditControl", { "TOPLEFT", self.controls.sectionAnchor, "TOPLEFT" }, 8, 15, 360, 20, "", "Search", "%c", 100, function() self:UpdateControls() end, nil, nil, true) self.controls.toggleConfigs = new("ButtonControl", { "LEFT", self.controls.search, "RIGHT" }, 10, 0, 200, 20, function() @@ -75,7 +94,7 @@ local ConfigTabClass = newClass("ConfigTab", "UndoHandler", "ControlHost", "Cont local function implyCond(varData) local mainEnv = self.build.calcsTab.mainEnv - if self.input[varData.var] then + if self.configSets[self.activeConfigSetId].input[varData.var] then if varData.implyCondList then for _, implyCond in ipairs(varData.implyCondList) do if (implyCond and mainEnv.conditionsUsed[implyCond]) then @@ -125,7 +144,7 @@ local ConfigTabClass = newClass("ConfigTab", "UndoHandler", "ControlHost", "Cont local lastSection for _, varData in ipairs(varList) do if varData.section then - lastSection = new("SectionControl", {"TOPLEFT",self.controls.sectionAnchor,"TOPLEFT"}, 0, 0, 360, 0, varData.section) + lastSection = new("SectionControl", {"TOPLEFT",self.controls.search,"BOTTOMLEFT"}, 0, 0, 360, 0, varData.section) lastSection.varControlList = { } lastSection.col = varData.col lastSection.height = function(self) @@ -143,7 +162,7 @@ local ConfigTabClass = newClass("ConfigTab", "UndoHandler", "ControlHost", "Cont local control if varData.type == "check" then control = new("CheckBoxControl", {"TOPLEFT",lastSection,"TOPLEFT"}, 234, 0, 18, varData.label, function(state) - self.input[varData.var] = state + self.configSets[self.activeConfigSetId].input[varData.var] = state self:AddUndoState() self:BuildModList() self.build.buildFlag = true @@ -151,9 +170,9 @@ local ConfigTabClass = newClass("ConfigTab", "UndoHandler", "ControlHost", "Cont elseif varData.type == "count" or varData.type == "integer" or varData.type == "countAllowZero" or varData.type == "float" then control = new("EditControl", {"TOPLEFT",lastSection,"TOPLEFT"}, 234, 0, 90, 18, "", nil, (varData.type == "integer" and "^%-%d") or (varData.type == "float" and "^%d.") or "%D", 7, function(buf, placeholder) if placeholder then - self.placeholder[varData.var] = tonumber(buf) + self.configSets[self.activeConfigSetId].placeholder[varData.var] = tonumber(buf) else - self.input[varData.var] = tonumber(buf) + self.configSets[self.activeConfigSetId].input[varData.var] = tonumber(buf) self:AddUndoState() self:BuildModList() end @@ -161,7 +180,7 @@ local ConfigTabClass = newClass("ConfigTab", "UndoHandler", "ControlHost", "Cont end) elseif varData.type == "list" then control = new("DropDownControl", {"TOPLEFT",lastSection,"TOPLEFT"}, 234, 0, 118, 16, varData.list, function(index, value) - self.input[varData.var] = value.val + self.configSets[self.activeConfigSetId].input[varData.var] = value.val self:AddUndoState() self:BuildModList() self.build.buildFlag = true @@ -169,9 +188,9 @@ local ConfigTabClass = newClass("ConfigTab", "UndoHandler", "ControlHost", "Cont elseif varData.type == "text" and not varData.resizable then control = new("EditControl", {"TOPLEFT",lastSection,"TOPLEFT"}, 8, 0, 344, 118, "", nil, "^%C\t\n", nil, function(buf, placeholder) if placeholder then - self.placeholder[varData.var] = tostring(buf) + self.configSets[self.activeConfigSetId].placeholder[varData.var] = tostring(buf) else - self.input[varData.var] = tostring(buf) + self.configSets[self.activeConfigSetId].input[varData.var] = tostring(buf) self:AddUndoState() self:BuildModList() end @@ -180,9 +199,9 @@ local ConfigTabClass = newClass("ConfigTab", "UndoHandler", "ControlHost", "Cont elseif varData.type == "text" and varData.resizable then control = new("ResizableEditControl", {"TOPLEFT",lastSection,"TOPLEFT"}, 8, 0, nil, 344, nil, nil, 118, 118 + 16 * 40, "", nil, "^%C\t\n", nil, function(buf, placeholder) if placeholder then - self.placeholder[varData.var] = tostring(buf) + self.configSets[self.activeConfigSetId].placeholder[varData.var] = tostring(buf) else - self.input[varData.var] = tostring(buf) + self.configSets[self.activeConfigSetId].input[varData.var] = tostring(buf) self:AddUndoState() self:BuildModList() end @@ -242,7 +261,7 @@ local ConfigTabClass = newClass("ConfigTab", "UndoHandler", "ControlHost", "Cont end if varData.ifOption then t_insert(shownFuncs, listOrSingleIfOption(varData.ifOption, function(ifOption) - return self.input[ifOption] + return self.configSets[self.activeConfigSetId].input[ifOption] end)) end if varData.ifCond then @@ -505,13 +524,13 @@ local ConfigTabClass = newClass("ConfigTab", "UndoHandler", "ControlHost", "Cont t_insert(self.controls, labelControl) end if varData.var then - self.input[varData.var] = varData.defaultState + self.configSets[self.activeConfigSetId].input[varData.var] = varData.defaultState control.state = varData.defaultState self.varControls[varData.var] = control - self.placeholder[varData.var] = varData.defaultPlaceholderState + self.configSets[self.activeConfigSetId].placeholder[varData.var] = varData.defaultPlaceholderState control.placeholder = varData.defaultPlaceholderState if varData.defaultIndex then - self.input[varData.var] = varData.list[varData.defaultIndex].val + self.configSets[self.activeConfigSetId].input[varData.var] = varData.list[varData.defaultIndex].val control.selIndex = varData.defaultIndex end if varData.type == "check" then @@ -531,7 +550,7 @@ local ConfigTabClass = newClass("ConfigTab", "UndoHandler", "ControlHost", "Cont if not varData.doNotHighlight then control.borderFunc = function() local shown = type(innerShown) == "boolean" and innerShown or innerShown() - local cur = self.input[varData.var] + local cur = self.configSets[self.activeConfigSetId].input[varData.var] local def = self:GetDefaultState(varData.var, type(cur)) if cur ~= nil and cur ~= def then if not shown then @@ -549,14 +568,14 @@ local ConfigTabClass = newClass("ConfigTab", "UndoHandler", "ControlHost", "Cont return false end local shown = type(innerShown) == "boolean" and innerShown or innerShown() - local cur = self.input[varData.var] + local cur = self.configSets[self.activeConfigSetId].input[varData.var] local def = self:GetDefaultState(varData.var, type(cur)) return not shown and cur ~= nil and cur ~= def or shown end local innerLabel = labelControl.label labelControl.label = function() local shown = type(innerShown) == "boolean" and innerShown or innerShown() - local cur = self.input[varData.var] + local cur = self.configSets[self.activeConfigSetId].input[varData.var] local def = self:GetDefaultState(varData.var, type(cur)) if not shown and cur ~= nil and cur ~= def then return colorCodes.NEGATIVE..StripEscapes(innerLabel) @@ -577,7 +596,7 @@ local ConfigTabClass = newClass("ConfigTab", "UndoHandler", "ControlHost", "Cont end local shown = type(innerShown) == "boolean" and innerShown or innerShown() - local cur = self.input[varData.var] + local cur = self.configSets[self.activeConfigSetId].input[varData.var] local def = self:GetDefaultState(varData.var, type(cur)) if not shown and cur ~= nil and cur ~= def then tooltip:AddLine(14, colorCodes.NEGATIVE.."This config option is conditional with missing source and is invalid.") @@ -593,26 +612,30 @@ local ConfigTabClass = newClass("ConfigTab", "UndoHandler", "ControlHost", "Cont end) function ConfigTabClass:Load(xml, fileName) - for _, node in ipairs(xml) do + self.activeConfigSetId = 1 + self.configSets = { } + self.configSetOrderList = { 1 } + + local function setInputAndPlaceholder(node, configSetId) if node.elem == "Input" then if not node.attrib.name then launch:ShowErrMsg("^1Error parsing '%s': 'Input' element missing name attribute", fileName) return true end if node.attrib.number then - self.input[node.attrib.name] = tonumber(node.attrib.number) + self.configSets[configSetId].input[node.attrib.name] = tonumber(node.attrib.number) elseif node.attrib.string then if node.attrib.name == "enemyIsBoss" then - self.input[node.attrib.name] = node.attrib.string:lower():gsub("(%l)(%w*)", function(a,b) return s_upper(a)..b end) + self.configSets[configSetId].input[node.attrib.name] = node.attrib.string:lower():gsub("(%l)(%w*)", function(a,b) return s_upper(a)..b end) :gsub("Uber Atziri", "Boss"):gsub("Shaper", "Pinnacle"):gsub("Sirus", "Pinnacle") -- backwards compat <=3.20, Uber Atziri Flameblast -> Atziri Flameblast elseif node.attrib.name == "presetBossSkills" then - self.input[node.attrib.name] = node.attrib.string:gsub("^Uber ", "") + self.configSets[configSetId].input[node.attrib.name] = node.attrib.string:gsub("^Uber ", "") else - self.input[node.attrib.name] = node.attrib.string + self.configSets[configSetId].input[node.attrib.name] = node.attrib.string end elseif node.attrib.boolean then - self.input[node.attrib.name] = node.attrib.boolean == "true" + self.configSets[configSetId].input[node.attrib.name] = node.attrib.boolean == "true" else launch:ShowErrMsg("^1Error parsing '%s': 'Input' element missing number, string or boolean attribute", fileName) return true @@ -623,23 +646,39 @@ function ConfigTabClass:Load(xml, fileName) return true end if node.attrib.number then - self.placeholder[node.attrib.name] = tonumber(node.attrib.number) + self.configSets[configSetId].placeholder[node.attrib.name] = tonumber(node.attrib.number) elseif node.attrib.string then - self.input[node.attrib.name] = node.attrib.string + self.configSets[configSetId].input[node.attrib.name] = node.attrib.string else launch:ShowErrMsg("^1Error parsing '%s': 'Placeholder' element missing number", fileName) return true end end end - self:BuildModList() - self:UpdateControls() + for index, node in ipairs(xml) do + if node.elem ~= "ConfigSet" then + if not self.configSets[1] then + self:NewConfigSet(1, "Default") + end + setInputAndPlaceholder(node, 1) + else + local configSetId = tonumber(node.attrib.id) + self:NewConfigSet(configSetId, node.attrib.title or "Default") + self.configSetOrderList[index] = configSetId + for _, child in ipairs(node) do + setInputAndPlaceholder(child, configSetId) + end + end + end + + self:SetActiveConfigSet(tonumber(xml.attrib.activeConfigSet) or 1) self:ResetUndo() + self.build:SyncLoadouts() end function ConfigTabClass:GetDefaultState(var, varType) - if self.placeholder[var] ~= nil then - return self.placeholder[var] + if self.configSets[self.activeConfigSetId].placeholder[var] ~= nil then + return self.configSets[self.activeConfigSetId].placeholder[var] end if self.defaultState[var] ~= nil then @@ -658,41 +697,50 @@ function ConfigTabClass:GetDefaultState(var, varType) end function ConfigTabClass:Save(xml) - for k, v in pairs(self.input) do - if v ~= self:GetDefaultState(k, type(v)) then - local child = { elem = "Input", attrib = { name = k } } - if type(v) == "number" then - child.attrib.number = tostring(v) - elseif type(v) == "boolean" then - child.attrib.boolean = tostring(v) - else - child.attrib.string = tostring(v) - end - t_insert(xml, child) - end - end - for k, v in pairs(self.placeholder) do - local child = { elem = "Placeholder", attrib = { name = k } } - if type(v) == "number" then - child.attrib.number = tostring(v) - else - child.attrib.string = tostring(v) - end + xml.attrib = { + activeConfigSet = tostring(self.activeConfigSetId) + } + for _, configSetId in ipairs(self.configSetOrderList) do + local configSet = self.configSets[configSetId] + local child = { elem = "ConfigSet", attrib = { id = tostring(configSetId), title = configSet.title } } t_insert(xml, child) + + for k, v in pairs(configSet.input) do + if v ~= self:GetDefaultState(k, type(v)) then + local node = { elem = "Input", attrib = { name = k } } + if type(v) == "number" then + node.attrib.number = tostring(v) + elseif type(v) == "boolean" then + node.attrib.boolean = tostring(v) + else + node.attrib.string = tostring(v) + end + t_insert(child, node) + end + end + for k, v in pairs(configSet.placeholder) do + local node = { elem = "Placeholder", attrib = { name = k } } + if type(v) == "number" then + node.attrib.number = tostring(v) + else + node.attrib.string = tostring(v) + end + t_insert(child, node) + end end end function ConfigTabClass:UpdateControls() for var, control in pairs(self.varControls) do if control._className == "EditControl" or control._className == "ResizableEditControl" then - control:SetText(tostring(self.input[var] or "")) - if self.placeholder[var] then - control:SetPlaceholder(tostring(self.placeholder[var])) + control:SetText(tostring(self.configSets[self.activeConfigSetId].input[var] or "")) + if self.configSets[self.activeConfigSetId].placeholder[var] then + control:SetPlaceholder(tostring(self.configSets[self.activeConfigSetId].placeholder[var])) end elseif control._className == "CheckBoxControl" then - control.state = self.input[var] + control.state = self.configSets[self.activeConfigSetId].input[var] elseif control._className == "DropDownControl" then - control:SelByValue(self.input[var], "val") + control:SelByValue(self.configSets[self.activeConfigSetId].input[var] or self:GetDefaultState(var), "val") end end end @@ -735,7 +783,7 @@ function ConfigTabClass:Draw(viewPort, inputEvents) local y = 14 section.shown = true local doShow = false - for _, varControl in ipairs(section.varControlList) do + for _, varControl in pairs(section.varControlList) do if varControl:IsShown() then doShow = true local width, height = varControl:GetSize() @@ -766,9 +814,19 @@ function ConfigTabClass:Draw(viewPort, inputEvents) maxColY = m_max(maxColY, colY[col]) end end + + local newSetList = { } + for index, configSetId in ipairs(self.configSetOrderList) do + local configSet = self.configSets[configSetId] + t_insert(newSetList, configSet.title or "Default") + if configSetId == self.activeConfigSetId then + self.controls.setSelect.selIndex = index + end + end + self.controls.setSelect:SetList(newSetList) self.controls.scrollBar.height = viewPort.height - self.controls.scrollBar:SetContentDimension(maxColY + 30, viewPort.height) + self.controls.scrollBar:SetContentDimension(maxColY + 58, viewPort.height) self.controls.sectionAnchor.y = 20 - self.controls.scrollBar.offset main:DrawBackground(viewPort) @@ -777,8 +835,8 @@ function ConfigTabClass:Draw(viewPort, inputEvents) end function ConfigTabClass:UpdateLevel() - local input = self.input - local placeholder = self.placeholder + local input = self.configSets[self.activeConfigSetId].input + local placeholder = self.configSets[self.activeConfigSetId].placeholder if input.enemyLevel and input.enemyLevel > 0 then self.enemyLevel = m_min(data.misc.MaxEnemyLevel, input.enemyLevel) elseif placeholder.enemyLevel and placeholder.enemyLevel > 0 then @@ -793,8 +851,8 @@ function ConfigTabClass:BuildModList() self.modList = modList local enemyModList = new("ModList") self.enemyModList = enemyModList - local input = self.input - local placeholder = self.placeholder + local input = self.configSets[self.activeConfigSetId].input + local placeholder = self.configSets[self.activeConfigSetId].placeholder self:UpdateLevel() -- enemy level handled here because it's needed to correctly set boss stats for _, varData in ipairs(varList) do if varData.apply then @@ -822,7 +880,7 @@ function ConfigTabClass:BuildModList() end function ConfigTabClass:ImportCalcSettings() - local input = self.input + local input = self.configSets[self.activeConfigSetId].input local calcsInput = self.build.calcsTab.input local function import(old, new) input[new] = calcsInput[old] @@ -859,14 +917,73 @@ function ConfigTabClass:ImportCalcSettings() end function ConfigTabClass:CreateUndoState() - return copyTable(self.input) + return copyTable(self.configSets[self.activeConfigSetId].input) end function ConfigTabClass:RestoreUndoState(state) - wipeTable(self.input) + wipeTable(self.configSets[self.activeConfigSetId].input) for k, v in pairs(state) do - self.input[k] = v + self.configSets[self.activeConfigSetId].input[k] = v end self:UpdateControls() self:BuildModList() end + +function ConfigTabClass:OpenConfigSetManagePopup() + main:OpenPopup(370, 290, "Manage Config Sets", { + new("ConfigSetListControl", nil, 0, 50, 350, 200, self), + new("ButtonControl", nil, 0, 260, 90, 20, "Done", function() + main:ClosePopup() + end), + }) +end + +-- Creates a new config set +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 + end + -- there are default values for input and placeholder that every new config set needs to have + for _, varData in ipairs(varList) do + if varData.var then + configSet.input[varData.var] = varData.defaultState + configSet.placeholder[varData.var] = varData.defaultPlaceholderState + if varData.defaultIndex then + configSet.input[varData.var] = varData.list[varData.defaultIndex].val + end + end + end + self.configSets[configSet.id] = configSet + return configSet +end + +-- Changes the active config set +function ConfigTabClass:SetActiveConfigSet(configSetId, init) + -- Initialize config sets if needed + if not self.configSetOrderList[1] then + self.configSetOrderList[1] = 1 + self:NewConfigSet(1) + end + + if not configSetId then + configSetId = self.activeConfigSetId + end + + if not self.configSets[configSetId] then + configSetId = self.configSetOrderList[1] + end + + self.input = self.configSets[configSetId].input + self.placeholder = self.configSets[configSetId].placeholder + self.activeConfigSetId = configSetId + + if not init then + self:UpdateControls() + self:BuildModList() + end + self.build.buildFlag = true +end diff --git a/src/Classes/ItemSetListControl.lua b/src/Classes/ItemSetListControl.lua index cd423fb1..11dabfa6 100644 --- a/src/Classes/ItemSetListControl.lua +++ b/src/Classes/ItemSetListControl.lua @@ -128,6 +128,7 @@ function ItemSetListClass:OnSelDelete(index, itemSetId) self.itemsTab:SetActiveItemSet(self.list[m_max(1, index - 1)]) end self.itemsTab:AddUndoState() + self.itemsTab.build:SyncLoadouts() end) end end diff --git a/src/Classes/PassiveSpecListControl.lua b/src/Classes/PassiveSpecListControl.lua index d68232b7..7b65d5a9 100644 --- a/src/Classes/PassiveSpecListControl.lua +++ b/src/Classes/PassiveSpecListControl.lua @@ -83,6 +83,7 @@ function PassiveSpecListClass:OnOrderChange() self.treeTab.activeSpec = isValueInArray(self.list, self.treeTab.build.spec) self.treeTab.modFlag = true self:UpdateItemsTabPassiveTreeDropdown() + self.treeTab.build:SyncLoadouts() end function PassiveSpecListClass:OnSelClick(index, spec, doubleClick) @@ -104,6 +105,7 @@ function PassiveSpecListClass:OnSelDelete(index, spec) end self.treeTab.modFlag = true self:UpdateItemsTabPassiveTreeDropdown() + self.treeTab.build:SyncLoadouts() end) end end diff --git a/src/Classes/SkillSetListControl.lua b/src/Classes/SkillSetListControl.lua index 7f39de29..c0752f2e 100644 --- a/src/Classes/SkillSetListControl.lua +++ b/src/Classes/SkillSetListControl.lua @@ -108,6 +108,7 @@ function SkillSetListClass:OnSelDelete(index, skillSetId) self.skillsTab:SetActiveSkillSet(self.list[m_max(1, index - 1)]) end self.skillsTab:AddUndoState() + self.skillsTab.build:SyncLoadouts() end) end end diff --git a/src/Classes/TreeTab.lua b/src/Classes/TreeTab.lua index cd46abc8..6333ce66 100644 --- a/src/Classes/TreeTab.lua +++ b/src/Classes/TreeTab.lua @@ -504,6 +504,8 @@ function TreeTabClass:ConvertToVersion(version, remove, success, ignoreRuthlessC if success then main:OpenMessagePopup("Tree Converted", "The tree has been converted to "..treeVersions[version].display..".\nNote that some or all of the passives may have been de-allocated due to changes in the tree.\n\nYou can switch back to the old tree using the tree selector at the bottom left.") end + -- on convert, check the names of the sets in case there's a match now + self.build:SyncLoadouts() end function TreeTabClass:ConvertAllToVersion(version) diff --git a/src/Modules/Build.lua b/src/Modules/Build.lua index 20bdd21f..081939af 100644 --- a/src/Modules/Build.lua +++ b/src/Modules/Build.lua @@ -234,21 +234,21 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild) -- self.buildFlag = true --end) self.controls.buildLoadouts = new("DropDownControl", {"LEFT",self.controls.ascendDrop,"RIGHT"}, 8, 0, 190, 20, {}, function(index, value) - if value == "Loadouts:" or value == "-----" then + if value == "^7^7Loadouts:" or value == "^7^7-----" then self.controls.buildLoadouts:SetSel(1) return end - if value == "Sync" then + if value == "^7^7Sync" then self:SyncLoadouts() self.controls.buildLoadouts:SetSel(1) return end - if value == "^7Help >>" then + if value == "^7^7Help >>" then main:OpenAboutPopup(7) self.controls.buildLoadouts:SetSel(1) return end - if value == "New Loadout" then + 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) @@ -269,6 +269,10 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild) 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() @@ -297,37 +301,35 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild) end end end - local newItemId = nil - for _, itemOrder in ipairs(self.itemsTab.itemSetOrderList) do - if value == self.itemsTab.itemSets[itemOrder].title then - newItemId = itemOrder - else - local linkMatch = string.match(value, "%{(%w+)%}") - if linkMatch then - newItemId = self.itemListSpecialLinks[linkMatch]["setId"] - end - end - end - local newSkillId = nil - for _, skillOrder in ipairs(self.skillsTab.skillSetOrderList) do - if value == self.skillsTab.skillSets[skillOrder].title then - newSkillId = skillOrder - else - local linkMatch = string.match(value, "%{(%w+)%}") - if linkMatch then - newSkillId = self.skillListSpecialLinks[linkMatch]["setId"] + + -- 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 + local newItemId = findSetId(self.itemsTab.itemSetOrderList, value, self.itemsTab.itemSets, self.itemListSpecialLinks) + local newSkillId = findSetId(self.skillsTab.skillSetOrderList, value, self.skillsTab.skillSets, self.skillListSpecialLinks) + local newConfigId = 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 then + if newSpecId == nil or newItemId == nil or newSkillId == nil or newConfigId == nil then return end self.treeTab:SetActiveSpec(newSpecId) self.itemsTab:SetActiveItemSet(newItemId) self.skillsTab:SetActiveSkillSet(newSkillId) + self.configTab:SetActiveConfigSet(newConfigId) self.controls.buildLoadouts:SelByValue(value) end) @@ -889,94 +891,87 @@ end function buildMode:SyncLoadouts(reset) self.controls.buildLoadouts.list = {"No Loadouts"} - local filteredList = {"Loadouts:"} + local filteredList = {"^7^7Loadouts:"} local treeList = {} local itemList = {} local skillList = {} + local configList = {} -- used when clicking on the dropdown to set the correct setId for each SetActiveSet() - self.treeListSpecialLinks, self.itemListSpecialLinks, self.skillListSpecialLinks = {}, {}, {} + self.treeListSpecialLinks, self.itemListSpecialLinks, self.skillListSpecialLinks, self.configListSpecialLinks = {}, {}, {}, {} - if self.treeTab ~= nil and self.itemsTab ~= nil and self.skillsTab ~= nil then + if self.treeTab ~= nil and self.itemsTab ~= nil and self.skillsTab ~= nil and self.configTab ~= nil then local transferTable = {} + local sortedTreeListSpecialLinks = {} for id, spec in ipairs(self.treeTab.specList) do local specTitle = spec.title or "Default" - t_insert(treeList, (spec.treeVersion ~= latestTreeVersion and ("["..treeVersions[spec.treeVersion].display.."] ") or "")..(specTitle)) -- only alphanumeric and comma are allowed in the braces { } local linkIdentifier = string.match(specTitle, "%{([%w,]+)%}") if linkIdentifier then -- iterate over each identifier, delimited by comma, and set the index so we can grab it later - -- setId index is the id of the set in the global list needed for SetActive + -- setId index is the id of the set in the global list needed for SetActiveSet -- setName is only used for Tree currently and we strip the braces to get the plain name of the set, this is used as the name of the loadout for linkId in string.gmatch(linkIdentifier, "[^%,]+") do transferTable["setId"] = id transferTable["setName"] = string.match(specTitle, "(.+)% {") + transferTable["linkId"] = linkId self.treeListSpecialLinks[linkId] = transferTable + t_insert(sortedTreeListSpecialLinks, transferTable) transferTable = {} end + else + t_insert(treeList, (spec.treeVersion ~= latestTreeVersion and ("["..treeVersions[spec.treeVersion].display.."] ") or "")..(specTitle)) + end + end + + -- item, skill, and config sets have identical structure + local function identifyLinks(setOrderList, tabSets, setList, specialLinks) + for id, set in ipairs(setOrderList) do + local setTitle = tabSets[set].title or "Default" + local linkIdentifier = string.match(setTitle, "%{([%w,]+)%}") + -- this if/else prioritizes group identifier in case the user creates sets with same name AND same identifiers + -- result is only the group is recognized and one loadout is created rather than a duplicate from each condition met + if linkIdentifier then + for linkId in string.gmatch(linkIdentifier, "[^%,]+") do + transferTable["setId"] = set + transferTable["setName"] = string.match(setTitle, "(.+)% {") + specialLinks[linkId] = transferTable + transferTable = {} + end + else + t_insert(setList, setTitle) + end end end - for id, item in ipairs(self.itemsTab.itemSetOrderList) do - local itemTitle = self.itemsTab.itemSets[item].title or "Default" - t_insert(itemList, itemTitle) - local linkIdentifier = string.match(itemTitle, "%{([%w,]+)%}") - if linkIdentifier then - for linkId in string.gmatch(linkIdentifier, "[^%,]+") do - transferTable["setId"] = item - transferTable["setName"] = string.match(itemTitle, "(.+)% {") - self.itemListSpecialLinks[linkId] = transferTable - transferTable = {} - end - end - end - for id, skill in ipairs(self.skillsTab.skillSetOrderList) do - local skillTitle = self.skillsTab.skillSets[skill].title or "Default" - t_insert(skillList, skillTitle) - local linkIdentifier = string.match(skillTitle, "%{([%w,]+)%}") - if linkIdentifier then - for linkId in string.gmatch(linkIdentifier, "[^%,]+") do - transferTable["setId"] = skill - transferTable["setName"] = string.match(skillTitle, "(.+)% {") - self.skillListSpecialLinks[linkId] = transferTable - transferTable = {} - end - end - end - local duplicateCheck = { } + identifyLinks(self.itemsTab.itemSetOrderList, self.itemsTab.itemSets, itemList, self.itemListSpecialLinks) + identifyLinks(self.skillsTab.skillSetOrderList, self.skillsTab.skillSets, skillList, self.skillListSpecialLinks) + identifyLinks(self.configTab.configSetOrderList, self.configTab.configSets, configList, self.configListSpecialLinks) + -- loop over all for exact match loadouts for id, tree in ipairs(treeList) do for id, skill in ipairs(skillList) do for id, item in ipairs(itemList) do - if (tree == skill and tree == item) then - if duplicateCheck[tree] then -- if already seen, re-colour NEGATIVE to alert user of duplicate - tree = colorCodes.NEGATIVE..tree + for id, config in ipairs(configList) do + if (tree == skill and tree == item and tree == config) then + t_insert(filteredList, tree) end - t_insert(filteredList, tree) - duplicateCheck[tree] = true end end end end -- loop over the identifiers found within braces and set the loadout name to the TreeSet - for treeLinkId, tree in pairs(self.treeListSpecialLinks) do - for itemLinkId, item in pairs(self.itemListSpecialLinks) do - for skillLinkId, skill in pairs(self.skillListSpecialLinks) do - if (treeLinkId == skillLinkId and treeLinkId == itemLinkId) then - local loadoutName = tree["setName"].." {"..treeLinkId.."}" - if duplicateCheck[loadoutName] then - loadoutName = colorCodes.NEGATIVE..loadoutName - end - t_insert(filteredList, loadoutName) - duplicateCheck[loadoutName] = true - end - end + for _, tree in ipairs(sortedTreeListSpecialLinks) do + local treeLinkId = tree.linkId + if (self.itemListSpecialLinks[treeLinkId] and self.skillListSpecialLinks[treeLinkId] and self.configListSpecialLinks[treeLinkId]) then + t_insert(filteredList, tree.setName .." {"..treeLinkId.."}") end end end - t_insert(filteredList, "-----") - t_insert(filteredList, "New Loadout") - t_insert(filteredList, "Sync") - t_insert(filteredList, "^7Help >>") + -- 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^7Help >>") if #filteredList > 0 then self.controls.buildLoadouts.list = filteredList @@ -986,7 +981,7 @@ function buildMode:SyncLoadouts(reset) self.controls.buildLoadouts:SetSel(1) end - return treeList, itemList, skillList + return treeList, itemList, skillList, configList end function buildMode:EstimatePlayerProgress() diff --git a/src/Modules/ConfigOptions.lua b/src/Modules/ConfigOptions.lua index c0668a95..cd2c9fd0 100644 --- a/src/Modules/ConfigOptions.lua +++ b/src/Modules/ConfigOptions.lua @@ -133,15 +133,15 @@ return { -- Section: General options { section = "General", col = 1 }, { var = "resistancePenalty", type = "list", label = "Resistance penalty:", list = {{val=0,label="None"},{val=-30,label="Act 5 (-30%)"},{val=-60,label="Act 10 (-60%)"}}, defaultIndex = 3 }, - { var = "bandit", type = "list", label = "Bandit quest:", tooltipFunc = banditTooltip, list = {{val="None",label="Kill all"},{val="Oak",label="Help Oak"},{val="Kraityn",label="Help Kraityn"},{val="Alira",label="Help Alira"}} }, - { var = "pantheonMajorGod", type = "list", label = "Major God:", tooltipFunc = applyPantheonDescription, list = { + { var = "bandit", type = "list", defaultIndex = 1, label = "Bandit quest:", tooltipFunc = banditTooltip, list = {{val="None",label="Kill all"},{val="Oak",label="Help Oak"},{val="Kraityn",label="Help Kraityn"},{val="Alira",label="Help Alira"}} }, + { var = "pantheonMajorGod", type = "list", defaultIndex = 1, label = "Major God:", tooltipFunc = applyPantheonDescription, list = { { label = "Nothing", val = "None" }, { label = "Soul of the Brine King", val = "TheBrineKing" }, { label = "Soul of Lunaris", val = "Lunaris" }, { label = "Soul of Solaris", val = "Solaris" }, { label = "Soul of Arakaali", val = "Arakaali" }, } }, - { var = "pantheonMinorGod", type = "list", label = "Minor God:", tooltipFunc = applyPantheonDescription, list = { + { var = "pantheonMinorGod", type = "list", defaultIndex = 1, label = "Minor God:", tooltipFunc = applyPantheonDescription, list = { { label = "Nothing", val = "None" }, { label = "Soul of Gruthkul", val = "Gruthkul" }, { label = "Soul of Yugul", val = "Yugul" }, @@ -1946,7 +1946,7 @@ Huge sets the radius to 11. { var = "enemyArmour", type = "count", label = "Enemy Base Armour:", apply = function(val, modList, enemyModList) enemyModList:NewMod("Armour", "BASE", val, "Config") end }, - { var = "presetBossSkills", type = "list", label = "Boss Skill Preset", tooltipFunc = bossSkillsTooltip, list = data.bossSkillsList, apply = function(val, modList, enemyModList, build) + { var = "presetBossSkills", type = "list", defaultIndex = 1, label = "Boss Skill Preset", tooltipFunc = bossSkillsTooltip, list = data.bossSkillsList, apply = function(val, modList, enemyModList, build) if not (val == "None") then local bossData = data.bossSkills[val] local isUber = build.configTab.varControls['enemyIsBoss'].list[build.configTab.varControls['enemyIsBoss'].selIndex].val == "Uber" @@ -2023,14 +2023,14 @@ Huge sets the radius to 11. end end }, { var = "enemyDamageRollRange", type = "integer", label = "Enemy Skill Roll Range %:", ifFlag = "BossSkillActive", tooltip = "The percentage of the roll range the enemy hits for \n eg at 100% the enemy deals its maximum damage", defaultPlaceholderState = 70, hideIfInvalid = true }, - { var = "enemyDamageType", type = "list", label = "Enemy Damage Type:", tooltip = "Controls which types of damage the EHP calculation uses:\n\tAverage: uses the Average of all typed damage types (not Untyped)\n\nIf a specific damage type is selected, that will be the only type used.", list = {{val="Average",label="Average"},{val="Untyped",label="Untyped"},{val="Melee",label="Melee"},{val="Projectile",label="Projectile"},{val="Spell",label="Spell"},{val="SpellProjectile",label="Projectile Spell"}} }, + { var = "enemyDamageType", type = "list", defaultIndex = 1, label = "Enemy Damage Type:", tooltip = "Controls which types of damage the EHP calculation uses:\n\tAverage: uses the Average of all typed damage types (not Untyped)\n\nIf a specific damage type is selected, that will be the only type used.", list = {{val="Average",label="Average"},{val="Untyped",label="Untyped"},{val="Melee",label="Melee"},{val="Projectile",label="Projectile"},{val="Spell",label="Spell"},{val="SpellProjectile",label="Projectile Spell"}} }, { var = "enemySpeed", type = "integer", label = "Enemy attack / cast time in ms:", defaultPlaceholderState = 700 }, { var = "enemyMultiplierPvpDamage", type = "count", label = "Custom PvP Damage multiplier percent:", ifFlag = "isPvP", tooltip = "This multiplies the damage of a given skill in pvp, for instance any with damage multiplier specific to pvp (from skill or support or item like sire of shards)", apply = function(val, modList, enemyModList) enemyModList:NewMod("MultiplierPvpDamage", "BASE", val, "Config") end }, { var = "enemyCritChance", type = "integer", label = "Enemy critical strike chance:", defaultPlaceholderState = 5 }, { var = "enemyCritDamage", type = "integer", label = "Enemy critical strike multiplier:", defaultPlaceholderState = 30 }, - { var = "enemyPhysicalDamage", type = "integer", label = "Enemy Skill Physical Damage:", tooltip = "This overrides the default damage amount used to estimate your damage reduction from armour.\nThe default is 1.5 times the enemy's base damage, which is the same value\nused in-game to calculate the estimate shown on the character sheet."}, + { var = "enemyPhysicalDamage", type = "integer", label = "Enemy Skill Physical Damage:", tooltip = "This overrides the default damage amount used to estimate your damage reduction from armour.\nThe default is 1.5 times the enemy's base damage, which is the same value\nused in-game to calculate the estimate shown on the character sheet.", defaultPlaceholderState = 7 }, { var = "enemyPhysicalOverwhelm", type = "integer", label = "Enemy Skill Physical Overwhelm:"}, { var = "enemyLightningDamage", type = "integer", label = "Enemy Skill ^xADAA47Lightning Damage:"}, { var = "enemyLightningPen", type = "integer", label = "Enemy Skill ^xADAA47Lightning Pen:"},