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 <Wires77@users.noreply.github.com>
This commit is contained in:
Peechey
2024-07-21 16:06:09 -06:00
committed by GitHub
parent b4c22c901a
commit 2f99b7d603
9 changed files with 391 additions and 158 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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()

View File

@@ -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:"},