diff --git a/help.txt b/help.txt index 739640a18..7f798984d 100644 --- a/help.txt +++ b/help.txt @@ -215,3 +215,32 @@ It will fetch the builds most similar to your character and sort them by the lat For best results, make sure to select your main item set, tree, and skills before opening the popup. If you are using leveling gear/tree, it will match with other leveling builds. + +---[Auto Attribute Config] + +You can enable the automatic allocation of attributes via the "Auto Attribute Config" button at the bottom +of the "Tree" menu section. Each configuration is saved per tree. So if you have multiple trees, each will +have its configuration values. + +Weights: + If enabled, attribute travel nodes will automatically be assigned to Strength / Dexterity / Intelligence + according to your configured "weight" values. E.g. values of Str: 1 / Dex: 1 / Int: 2, would result in + roughly 25% of the attribute nodes being assigned to Strength and Dexterity and 50% to Intelligence. + + By default, attributes gained from items and other small passive nodes are taken into account when + calculating the actual vs. desired attribute ratios. + +Max Value: + If a "Max Value" is entered and the "Limit to Max?" checkbox is ticked, no more nodes will be allocated + to that attribute, once the maximum value is reached + +Attribute Requirements: + For ease of use, the "Use Attribute Requirements" checkbox can be ticked. This will result in weights + automatically being based on current attribute requirements from gems and gear. + +Item Mods: + If you want your attribute allocation to be gear-agnostic, you can tick the "Ignore Attribute Requirements" + checkbox. Any attribute bonuses gained from equipment will then not be taken into account during allocation. + Note: This does not affect modifiers to attribute requirements found on gear. + + diff --git a/src/Classes/PassiveSpec.lua b/src/Classes/PassiveSpec.lua index b8003ce72..1fb16040d 100644 --- a/src/Classes/PassiveSpec.lua +++ b/src/Classes/PassiveSpec.lua @@ -130,6 +130,31 @@ function PassiveSpecClass:Load(xml, dbFileName) for nodeId in node.attrib.nodes:gmatch("%d+") do weaponSets[tonumber(nodeId)] = weaponSet end + elseif node.elem == "AutoAttributeConfig" then + local autoAttributeConfig = { } + if node.attrib then + autoAttributeConfig.enabled = node.attrib.enabled == "true" + autoAttributeConfig.ignoreItemMods = node.attrib.ignoreItemMods == "true" + autoAttributeConfig.useAttrReq = node.attrib.useAttrReq == "true" + for _, attrEntry in ipairs(node) do + if (not attrEntry.elem) or (not attrEntry.attrib) then + launch:ShowErrMsg("^1Error parsing '%s': 'AutoAttributeConfig' element has invalid structure^7", dbFileName) + return true + end + autoAttributeConfig[attrEntry.elem] = { } + autoAttributeConfig[attrEntry.elem].max = attrEntry.attrib.max ~= "nil" and tonumber(attrEntry.attrib.max) or nil + autoAttributeConfig[attrEntry.elem].weight = attrEntry.attrib.weight ~= "nil" and tonumber(attrEntry.attrib.weight) or nil + autoAttributeConfig[attrEntry.elem].useMaxVal = attrEntry.attrib.useMaxVal == "true" + end + else + launch:ShowErrMsg("^1Error parsing '%s': 'AutoAttributeConfig' element missing 'attrib' attribute^7", dbFileName) + return true + end + -- Add static and calculated values + autoAttributeConfig = self.build.treeTab:UpdateAutoAttributeConfig(autoAttributeConfig, true) + self.autoAttributeConfig = copyTable(autoAttributeConfig) + self.autoAttributeConfigSaved = copyTable(autoAttributeConfig) --extra entry to detect changes later + end end end @@ -255,6 +280,30 @@ function PassiveSpecClass:Save(xml) t_insert(overrides, attributeOverride) end t_insert(xml, overrides) + + local autoAttributeConfig = { + elem = "AutoAttributeConfig" + } + if self.autoAttributeConfig then + -- This only saves values to the xml that are neither static, nor calculated. The rest is regenerated on load + autoAttributeConfig.attrib = { + enabled = tostring(self.autoAttributeConfig.enabled), + ignoreItemMods = tostring(self.autoAttributeConfig.ignoreItemMods), + useAttrReq = tostring(self.autoAttributeConfig.useAttrReq), + } + for _, attr in ipairs({"str", "dex", "int"}) do + local attrEntry = { elem = tostring(attr), + attrib = { + weight = tostring(self.autoAttributeConfig[attr].weight), + max = tostring(self.autoAttributeConfig[attr].max), + useMaxVal = tostring(self.autoAttributeConfig[attr].useMaxVal), + } + } + t_insert(autoAttributeConfig, attrEntry) + end + t_insert(xml, autoAttributeConfig) + self.autoAttributeConfigSaved = copyTable(self.autoAttributeConfig) + end end @@ -694,24 +743,46 @@ end -- Allocate the given node, if possible, and all nodes along the path to the node -- An alternate path to the node may be provided, otherwise the default path will be used -- The path must always contain the given node, as will be the case for the default path -function PassiveSpecClass:AllocNode(node, altPath) +function PassiveSpecClass:AllocNode(node, altPath, manualAttribute) if not node.path then -- Node cannot be connected to the tree as there is no possible path return end + local cachedPlayerAttr = nil -- Used for iterative, automatic determination of desired attribute nodes + local cachedPathAttrResults = nil --Used for temp storage of mod effects gained from the nodes, which are not yet included in the playerModDb until after allocation + local function handleAttributeNode(attrNode) + if not attrNode.isAttribute then return end + if (not manualAttribute) and self.autoAttributeConfig and self.autoAttributeConfig.enabled then + -- Note: cachedPathAttrResults is passed every time, but only used if `cachedPlayerAttr == nil` + self.attributeIndex, cachedPlayerAttr = self:GetAutoAttribute(cachedPlayerAttr, cachedPathAttrResults) + end + self:SwitchAttributeNode(attrNode.id, self.attributeIndex or 1) + end -- Allocate all nodes along the path if #node.intuitiveLeapLikesAffecting > 0 then node.alloc = true node.allocMode = (node.ascendancyName or node.type == "Keystone" or node.type == "Socket" or node.containJewelSocket) and 0 or self.allocMode + if node.isAttribute then + handleAttributeNode(node) + end self.allocNodes[node.id] = node else + if (not manualAttribute) and self.autoAttributeConfig and self.autoAttributeConfig.enabled and ((((altPath and #altPath) or 0) > 1) or ((node.pathDist or 0) > 1) ) then + -- Precalculate effects on attributes from non-attribues passives, if necessary + for _, pathNode in ipairs(altPath or node.path) do + if pathNode.finalModList and #pathNode.finalModList > 0 then + -- Choosing a function to return results, rather than passing the ModList itself because I don't want to modify the playerModDB later + cachedPathAttrResults = self:GetTempPathAttributeResults(pathNode.finalModList, cachedPathAttrResults) + end + end + end for _, pathNode in ipairs(altPath or node.path) do pathNode.alloc = true pathNode.allocMode = (node.ascendancyName or pathNode.type == "Keystone" or pathNode.type == "Socket" or pathNode.containJewelSocket) and 0 or self.allocMode - -- set path attribute nodes to latest chosen attribute or default to Strength if allocating before choosing an attribute + -- set path attribute nodes to latest chosen attribute, configured auto attribute, or default to Strength if allocating before choosing an attribute if pathNode.isAttribute then - self:SwitchAttributeNode(pathNode.id, self.attributeIndex or 1) + handleAttributeNode(pathNode) end self.allocNodes[pathNode.id] = pathNode end @@ -2089,3 +2160,107 @@ function PassiveSpecClass:SwitchAttributeNode(nodeId, attributeIndex) self.hashOverrides[nodeId] = newNode end end + +-- Function to auto calculate which attribute to allocate based on desired user weights +-- Should only be called if `self.autoAttributeConfig and self.autoAttributeConfig.enabled` +---@param cachedPlayerAttr table | nil optional table with cached playerAttribute values. Used when iterating over multiple attribute nodes without having to recalculate each time. Ignored if `nil` +---@param cachedPathAttrResults table | nil optional table that contains a cumulative effects of `finalModList` from non-attribute nodes on the path that need to be taken into account for attribute total estimation +---@return number attributeIndex, table playerAttr returns a number for the `attributeIndex` and the `playerAttr` table for future iterations +function PassiveSpecClass:GetAutoAttribute(cachedPlayerAttr, cachedPathAttrResults) + local autoAttributeConfig = self.autoAttributeConfig + local defaultAttrNodeValue = 5 -- doesn't seem to be anywhere in `data`, so I am storing it here, in case it ever changes + local playerAttr + local attributeList = { "dex", "int", "str" } + if cachedPlayerAttr ~= nil then + playerAttr = cachedPlayerAttr + else + -- Mod-based analysis is only performed once per path to reduce performance impact, otherwise cachedPlayerAttr is used + local playerModDB = self.build.calcsTab.mainEnv.player.modDB + local itemModDB = self.build.calcsTab.mainEnv.itemModDB + + -- Initialize player attribute values + playerAttr = { } + for _, attr in ipairs(attributeList) do + local attrUpper = attr:gsub("^%l", string.upper) + playerAttr[attr] = { } + + -- Calculating individual factor values instead of just using `mainOutput` because they are used to "simulate" effects for multi-node allocation + playerAttr[attr].base = playerModDB:Sum("BASE", nil, attrUpper) + (cachedPathAttrResults and cachedPathAttrResults[attr] and cachedPathAttrResults[attr].base or 0) + playerAttr[attr].inc = playerModDB:Sum("INC", nil, attrUpper) + (cachedPathAttrResults and cachedPathAttrResults[attr] and cachedPathAttrResults[attr].inc or 0) + playerAttr[attr].more = playerModDB:More(nil, attrUpper) * (cachedPathAttrResults and cachedPathAttrResults[attr] and cachedPathAttrResults[attr].more or 1) + + -- Remove item effects if configured + -- Note: I believe this currently wouldn't work with "override" mods or "Omniscience", but I don't think those exist in PoE2 atm + if autoAttributeConfig.ignoreItemMods then + playerAttr[attr].itemBase = itemModDB:Sum("BASE", nil, attrUpper) + playerAttr[attr].itemInc = playerModDB:Sum("INC", nil, attrUpper) + playerAttr[attr].itemMore = itemModDB:More(nil, attrUpper) + playerAttr[attr].base = playerAttr[attr].base - playerAttr[attr].itemBase + playerAttr[attr].inc = playerAttr[attr].inc - playerAttr[attr].itemInc + playerAttr[attr].more = playerAttr[attr].more / playerAttr[attr].itemMore + end + + playerAttr[attr].mult = (1 + (playerAttr[attr].inc / 100)) * playerAttr[attr].more + playerAttr[attr].total = playerAttr[attr].base * playerAttr[attr].mult + end + end + + -- Update weights based on attribute requirements if necessary + if autoAttributeConfig.useAttrReq then + self.autoAttributeConfig = self.build.treeTab:UpdateAutoAttributeConfig(autoAttributeConfig) + end + + -- Mark attributes ineligible if the max value is set and already exceeded. + local effConfigWeightTotal = 0 + for _, attr in ipairs(attributeList) do + if autoAttributeConfig[attr].max ~= nil and autoAttributeConfig[attr].useMaxVal and (playerAttr[attr].total >= autoAttributeConfig[attr].max) then + playerAttr[attr].eligible = false + playerAttr[attr].effTotal = 0 + else + playerAttr[attr].eligible = true + playerAttr[attr].effTotal = playerAttr[attr].total + effConfigWeightTotal = effConfigWeightTotal + (autoAttributeConfig[attr].weight or 0) + end + end + + -- Calculating effective totals and ratios that exclude attributes that already exceed max + playerAttr.effSumTotal = m_max(1, playerAttr.dex.effTotal + playerAttr.int.effTotal + playerAttr.str.effTotal ) -- use m_max to protect against division by 0 (e.g. in "Omniscience"-like scenarios) + playerAttr.dex.effRatio = playerAttr.dex.effTotal / playerAttr.effSumTotal + playerAttr.int.effRatio = playerAttr.int.effTotal / playerAttr.effSumTotal + playerAttr.str.effRatio = playerAttr.str.effTotal / playerAttr.effSumTotal + + local maxDiff = nil + local neededAttr = nil + + -- Find attribute with greatest diff from effective target ratio + for _, attr in ipairs(attributeList) do + if playerAttr[attr].eligible then + local effConfigRatio = (autoAttributeConfig[attr].weight or 0) / m_max(effConfigWeightTotal, 1 ) + local diff = effConfigRatio - playerAttr[attr].effRatio + if (maxDiff == nil) or (diff > maxDiff) then + maxDiff = diff + neededAttr = attr + end + end + end + -- Add effect of new attribute node to `playerAttr` for further iterations + if neededAttr ~= nil then + playerAttr[neededAttr].base = playerAttr[neededAttr].base + defaultAttrNodeValue + playerAttr[neededAttr].total = playerAttr[neededAttr].base * playerAttr[neededAttr].mult + end + + return autoAttributeConfig[neededAttr] and autoAttributeConfig[neededAttr].id or 1, playerAttr +end + +-- Analyzes a `finalModList` from a path with respect to effects on `dex`/ `int` / `str` for use in `GetAutoAttribute` +function PassiveSpecClass:GetTempPathAttributeResults(modList, cachedAttrResults) + local attrResults = cachedAttrResults or { dex = { }, int= { }, str = { } } + for attr, _ in pairs(attrResults) do + local attrUpper = attr:gsub("^%l", string.upper) + attrResults[attr].base = (attrResults[attr].base or 0) + modList:Sum("BASE", nil, attrUpper) + attrResults[attr].inc = (attrResults[attr].inc or 0) + modList:Sum("INC", nil, attrUpper) + attrResults[attr].more = (attrResults[attr].more or 1) * modList:More(nil, attrUpper) + end + + return attrResults +end diff --git a/src/Classes/PassiveTreeView.lua b/src/Classes/PassiveTreeView.lua index b781794cc..4ed8eaf52 100644 --- a/src/Classes/PassiveTreeView.lua +++ b/src/Classes/PassiveTreeView.lua @@ -325,15 +325,15 @@ function PassiveTreeViewClass:Draw(build, viewPort, inputEvents) build.buildFlag = true elseif hoverNode.path and not shouldBlockGlobalNodeAllocation(hoverNode) then -- Handle allocation of unallocated nodes - if hoverNode.isAttribute and not hotkeyPressed then - build.treeTab:ModifyAttributePopup(hoverNode) + if hoverNode.isAttribute and not hotkeyPressed and not (spec.autoAttributeConfig and spec.autoAttributeConfig.enabled) then + build.treeTab:ModifyAttributePopup(hoverNode) else -- the odd conditional here is so the popup only calls AllocNode inside and to avoid duplicating some code - -- same flow for hotkey attribute and non attribute nodes + -- same flow for hotkey attribute, automatic attributes, and non-attribute nodes if hotkeyPressed then processAttributeHotkeys(hoverNode.isAttribute) end - spec:AllocNode(hoverNode, self.tracePath and hoverNode == self.tracePath[#self.tracePath] and self.tracePath) + spec:AllocNode(hoverNode, self.tracePath and hoverNode == self.tracePath[#self.tracePath] and self.tracePath, hotkeyPressed) spec:AddUndoState() build.buildFlag = true end @@ -367,7 +367,7 @@ function PassiveTreeViewClass:Draw(build, viewPort, inputEvents) end spec:SwitchAttributeNode(hoverNode.id, spec.attributeIndex or 1) end - spec:AllocNode(hoverNode, self.tracePath and hoverNode == self.tracePath[#self.tracePath] and self.tracePath) + spec:AllocNode(hoverNode, self.tracePath and hoverNode == self.tracePath[#self.tracePath] and self.tracePath, true) -- passing `true` because both right-click and hotkey have priority over auto attribute allocation spec:AddUndoState() build.buildFlag = true end @@ -1390,6 +1390,13 @@ function PassiveTreeViewClass:AddNodeTooltip(tooltip, node, build, incSmallPassi end end + -- Attribute Allocation hints + if node.isAttribute then + tooltip:AddSeparator(14) + self:AddAutoAttributeConfigHintToTooltip(tooltip, node, build) + tooltip:AddSeparator(14) + end + -- Reminder text if node.reminderText then tooltip:AddSeparator(14) @@ -1534,6 +1541,22 @@ function PassiveTreeViewClass:AddGlobalNodeWarningsToTooltip(tooltip, node, buil end end +-- Helper function to add information about currently active auto attribute allocation config +function PassiveTreeViewClass:AddAutoAttributeConfigHintToTooltip(tooltip, node, build) + if not node.isAttribute then return end + local config = build.spec.autoAttributeConfig + + if config and config.enabled then + local hintTxt = colorCodes.TIP .. "Automatic Attribute Allocation is " .. colorCodes.POSITIVE .. "enabled^7" + local configTxt = "^7Weights: " + configTxt = configTxt .. colorCodes.STRENGTH .. "Str: ^7" .. (config.str.weight or 0) .. (config.str.useMaxVal and (" ^8[max: " .. (config.str.max or "0") .. "]") or "") .. " ^7| " + configTxt = configTxt .. colorCodes.DEXTERITY .. "Dex: ^7" .. (config.dex.weight or 0) .. (config.dex.useMaxVal and (" ^8[max: " .. (config.dex.max or "0") .. "]") or "") .. " ^7| " + configTxt = configTxt .. colorCodes.INTELLIGENCE .. "Int: ^7" .. (config.int.weight or 0) .. (config.int.useMaxVal and (" ^8[max: " .. (config.int.max or "0") .. "]") or "") .. "^7" + tooltip:AddLine(14, hintTxt) + tooltip:AddLine(14, configTxt) + end +end + function PassiveTreeViewClass:DrawAllocMode(allocMode, viewPort) local rgbColor if allocMode == 0 then diff --git a/src/Classes/TreeTab.lua b/src/Classes/TreeTab.lua index 5fb65dbf4..6c556a9c0 100644 --- a/src/Classes/TreeTab.lua +++ b/src/Classes/TreeTab.lua @@ -114,6 +114,8 @@ local TreeTabClass = newClass("TreeTab", "ControlHost", function(self, build) self.controls.compareSelect.maxDroppedWidth = 1000 self.controls.compareSelect.enableDroppedWidth = true self.controls.compareSelect.enableChangeBoxWidth = true + + -- Reset Tree Button self.controls.reset = new("ButtonControl", { "LEFT", self.controls.compareCheck, "RIGHT" }, { 8, 0, 100, 20 }, "Reset Tree", function() local controls = { } local buttonY = 65 @@ -132,6 +134,9 @@ local TreeTabClass = newClass("TreeTab", "ControlHost", function(self, build) main:OpenPopup(470, 100, "Reset Tree", controls, nil, "edit", "cancel") end) + -- Auto Attribute Config Button + self.controls.autoAttributeButton = new("ButtonControl", { "LEFT", self.controls.reset, "RIGHT" }, { 8, 0, 150, 20 }, "Auto Attribute Config", function() self:ConfigureAutoAttributePopup() end) + -- Tree Version Dropdown self.treeVersions = { } for _, num in ipairs(treeVersionList) do @@ -141,7 +146,7 @@ local TreeTabClass = newClass("TreeTab", "ControlHost", function(self, build) } t_insert(self.treeVersions, value) end - self.controls.versionText = new("LabelControl", { "LEFT", self.controls.reset, "RIGHT" }, { 8, 0, 0, 16 }, "Version:") + self.controls.versionText = new("LabelControl", { "LEFT", self.controls.autoAttributeButton, "RIGHT" }, { 8, 0, 0, 16 }, "Version:") self.controls.versionSelect = new("DropDownControl", { "LEFT", self.controls.versionText, "RIGHT" }, { 8, 0, 60, 20 }, self.treeVersions, function(index, selected) if selected.value ~= self.build.spec.treeVersion then self:OpenVersionConvertPopup(selected.value, true) @@ -794,7 +799,211 @@ function TreeTabClass:ModifyAttributePopup(hoverNode) ..colorCodes.RARE.."Right-click ^8an allocated node to toggle attribute types or to set an\n" .. "unallocated node to your last used attribute\n\n" ) - main:OpenPopup(550, 185, "Choose Attribute", controls, "save") + + controls.autoAttributeHint = new("LabelControl", {"TOPLEFT", controls.hotkeyTooltip, "BOTTOMLEFT"}, {0, 80, 0, 16}, + colorCodes.TIP .. "Hint: ^8You can also configure ratios for automatic attribute allocation\nClick the '^7Auto Attribute Config^8' button at the bottom of the tree menu" .."^7") + + main:OpenPopup(550, 265, "Choose Attribute", controls, "save") +end + +-- Popup for configuration of automatic attribute allocation +function TreeTabClass:ConfigureAutoAttributePopup() + if self.build.spec.autoAttributeConfig == nil then + self.build.spec.autoAttributeConfig = self:UpdateAutoAttributeConfig() -- will initialize if not yet set + end + + local controls = { } + local config = copyTable(self.build.spec.autoAttributeConfig) + + local function toggleOptions(state) + -- used to disable/enable config fields when main option is set + for key, control in pairs(controls) do + if not (key:find("Label123") or key:find("enabled") or key:find("apply") or key:find("cancel")) then + control.enabled = state + end + end + end + + -- UI dimensions + -- Main popup window + local window = { + width = 450, + height = 330, + } + -- 'save' and 'cancel' buttons + local mainButton = { + y = 290, + x = 60, + } + -- config settings + local settingsSection = { + width = 400, + height = 165, + gapTop = 80, + marginX = 45, + marginY = 20, + } + local settingsColumns = { + [1] = { + id = "attribute", + header = "Attribute", + width = 110, + height = 16, + }, + [2] = { + id = "weight", + header = "Weight", + width = 65, + height = 16, + }, + [3] = { + id = "maxVal", + header = "Max Value", + width = 65, + height = 16, + }, + [4] = { + id = "useMaxVal", + header = "Limit to Max?", + width = 65, + height = 16, + }, + } + + -- Actual control elements + -- Main Checkbox + controls.enabledLabel = new("LabelControl", nil, { -90, 35, 135, 16 }, "^7Automatic Attribute Allocation") + controls.enabledCheck = new("CheckBoxControl", { "LEFT", controls.enabledLabel, "RIGHT" }, { 10, 0, 18 }, "", + function(value) + config.enabled = value + toggleOptions(value) + end, "^7Enabling this option will automatically decide which attribute to allocate on travel nodes, \naccording to the configured weights and current total attributes", config.enabled) + + -- Section for config settings + -- Header columns + controls.settingsSection = new("SectionControl", nil, { 0, settingsSection.gapTop, settingsSection.width, settingsSection.height }, "^7Allocation Settings") + for i, column in ipairs(settingsColumns) do + local anchor = i == 1 and { "TOPLEFT", controls.settingsSection, "TOPLEFT" } or {"LEFT", controls[settingsColumns[i-1].id .. "Label"], "RIGHT" } + local marginY = i == 1 and settingsSection.marginY or 0 + controls[column.id .. "Label"] = new("LabelControl", anchor, { i ~= 1 and settingsSection.marginX or 8, marginY, column.width, column.height }, "^7" .. column.header) + end + -- Attribute settings + local attributeList = {"str", "dex", "int"} + local attrEditTabGroup = { } + for i, attr in ipairs (attributeList) do + controls[attr .. "Label"] = new("LabelControl", { "TOPLEFT", i == 1 and controls.attributeLabel or controls[attributeList[i-1] .. "Label"], "BOTTOMLEFT" }, { 0, settingsSection.marginY / 2, settingsColumns[1].width, settingsColumns[1].height - 2 }, colorCodes[config[attr].name:upper()] .. config[attr].name .. ":^7") + controls[attr .. "Weight"] = new("EditControl", {"LEFT", controls[attr .. "Label"], "LEFT"}, { settingsSection.marginX + controls.attributeLabel.width(), 0, settingsColumns[2].width, settingsColumns[2].height }, config[attr].weight, nil, "%D", nil, function(value) + if not config.useAttrReq then + config[attr].weight = tonumber(value) + else -- make sure weight display value is updated to current stats, if attribute requirements are to be used + local attrReq = self.build.calcsTab.mainOutput["Req" .. attr:gsub("^%l", string.upper)] or 0 + config[attr].weight = tonumber(attrReq) + controls[attr .. "Weight"]:SetText(tostring(attrReq), false) + end + end, nil, nil, true) + controls[attr .. "Weight"]:AddToTabGroup(attrEditTabGroup) + controls[attr .. "MaxVal"] = new("EditControl", {"LEFT", controls[attr .. "Weight"], "LEFT"}, { settingsSection.marginX + controls.weightLabel.width(), 0, settingsColumns[3].width, settingsColumns[3].height }, config[attr].max, nil, "%D", nil, function(value) config[attr].max = tonumber(value) end, nil, nil, true) + controls[attr .. "MaxVal"]:AddToTabGroup(attrEditTabGroup) + controls[attr .. "UseMaxVal"] = new("CheckBoxControl", {"LEFT", controls[attr .. "MaxVal"], "LEFT"}, { settingsSection.marginX + controls.maxValLabel.width(), 0, settingsColumns[4].height }, "", function(state) + if state then -- If box is switched to 'checked', only allow change if less than two boxes are checked + local maxCheckCount = (config.str.useMaxVal and 1 or 0) + (config.dex.useMaxVal and 1 or 0) + (config.int.useMaxVal and 1 or 0) + if maxCheckCount < 2 then + config[attr].useMaxVal = state + else + controls[attr .. "UseMaxVal"].state = false + end + else + config[attr].useMaxVal = state + end + end, "Enabling a \"Max Value\" will ignore the weight and stop allocating this attribute once the threshold is exceeded\n^8(no more than two attributes can be limited this way)^7", config[attr].useMaxVal) + end + + -- Use Attribute Requirements option + controls.useAttrReqLabel = new("LabelControl", { "TOPLEFT", controls.intLabel, "BOTTOMLEFT" }, { 0, settingsSection.marginY, settingsColumns[1].width, settingsColumns[1].height }, "^7Use Attribute Requirements") + controls.useAttrReqCheck = new("CheckBoxControl", { "TOPLEFT", controls.intMaxVal, "BOTTOMLEFT" }, { 0, settingsSection.marginY -1, 18 }, "", function(state) + config.useAttrReq = state + if state then + for _, attr in ipairs (attributeList) do + controls[attr .. "Weight"]:SetText(self.build.calcsTab.mainOutput["Req" .. attr:gsub("^%l", string.upper) .. "String"] or "0", true) + end + end + end, + "^7Enabling this option will automatically set the weights to current attribute requirements\n^8(You can still manually set \"Max Value\")^7", config.useAttrReq + ) + -- Ignore Item Mods option + controls.ignoreItemModsLabel = new("LabelControl", { "TOPLEFT", controls.useAttrReqLabel, "BOTTOMLEFT" }, { 0, 10, settingsColumns[1].width, settingsColumns[1].height, }, "^7Ignore Item Mods") + controls.ignoreItemModsCheck = new("CheckBoxControl", { "TOP", controls.useAttrReqCheck, "BOTTOM" }, { 0, 10, 18 }, "", function(value) config.ignoreItemMods = value end, "^7Enabling this option will ignore attributes gained from items, when calculating total player attributes\n^8(This includes both flat and percentage modifiers)^7", config.ignoreItemMods) + + controls.apply = new("ButtonControl", nil, { -mainButton.x, mainButton.y, 100, 20 }, "Apply", function() + + self.build.spec.autoAttributeConfig = self:UpdateAutoAttributeConfig(copyTable(config)) + + -- Enable "Save" build button, if autoAttributeConfig changed + if not tableDeepEquals(self.build.spec.autoAttributeConfig, self.build.spec.autoAttributeConfigSaved) then + self.autoAttrFlag = true + end + main:ClosePopup() + end) + controls.cancel = new("ButtonControl", nil, { mainButton.x, mainButton.y, 100, 20 }, "Cancel", function() + main:ClosePopup() + end) + + main:OpenPopup(window.width, window.height, "Auto Attribute Config", controls, "apply", nil, "cancel") + toggleOptions(controls.enabledCheck.state) + +end + +-- Create the default autoAttributeConfig in case the popup is opened for the first time +---@return table defaultConfig +function TreeTabClass:InitAutoAttributeConfig() + local defaultConfig = { + enabled = false, + ignoreItemMods = false, -- Whether to calculate player totals without the effects from items + useAttrReq = false, -- Whether weights are auto-populated based on current attribute requirements + dex = { weight = nil, max = nil, useMaxVal = false, id = 2, name = "Dexterity" }, -- "weight" and "max" determined by user, "id" and "name" is static + int = { weight = nil, max = nil, useMaxVal = false, id = 3, name = "Intelligence" }, + str = { weight = nil, max = nil, useMaxVal = false, id = 1, name = "Strength" }, + } + return defaultConfig +end + +-- Update calculated and potentially static values that are not part of the autoAttributeConfig popup form +---@param autoAttributeConfig table | nil the autoAttributeConfig you're starting from, if any +---@param addStaticInfo boolean | nil whether to add static infor like the 'id' and 'name' of attributes (e.g. when loading from a save file) +---@return table @returns the updated config +function TreeTabClass:UpdateAutoAttributeConfig(autoAttributeConfig, addStaticInfo) + -- Initialize config if empty + if autoAttributeConfig == nil then + autoAttributeConfig = self:InitAutoAttributeConfig() + end + + -- Static values (Should only be necessary when loading from xml) + if addStaticInfo then + local staticInfo = { + dex = { id = 2, name = "Dexterity" }, + int = { id = 3, name = "Intelligence" }, + str = { id = 1, name = "Strength" }, + } + for key, value in pairs(staticInfo) do + autoAttributeConfig[key].id = value.id + autoAttributeConfig[key].name = value.name + end + end + + -- Calculated values + if autoAttributeConfig.useAttrReq then + -- Make sure weights based on attribute requirements are up to date + autoAttributeConfig.dex.weight = self.build.calcsTab.mainOutput and (self.build.calcsTab.mainOutput["ReqDex"] or 0) or autoAttributeConfig.dex.weight -- Additional `nil` check for `mainOutput`, e.g. in case of initial load + autoAttributeConfig.int.weight = self.build.calcsTab.mainOutput and (self.build.calcsTab.mainOutput["ReqInt"] or 0) or autoAttributeConfig.int.weight + autoAttributeConfig.str.weight = self.build.calcsTab.mainOutput and (self.build.calcsTab.mainOutput["ReqStr"] or 0) or autoAttributeConfig.str.weight + end + + autoAttributeConfig.totalWeight = (autoAttributeConfig.dex.weight or 0) + (autoAttributeConfig.int.weight or 0) + (autoAttributeConfig.str.weight or 0) + autoAttributeConfig.dex.ratio = autoAttributeConfig.totalWeight == 0 and (1/3) or (autoAttributeConfig.dex.weight or 0) / autoAttributeConfig.totalWeight + autoAttributeConfig.int.ratio = autoAttributeConfig.totalWeight == 0 and (1/3) or (autoAttributeConfig.int.weight or 0) / autoAttributeConfig.totalWeight + autoAttributeConfig.str.ratio = autoAttributeConfig.totalWeight == 0 and (1/3) or (autoAttributeConfig.str.weight or 0) / autoAttributeConfig.totalWeight + + return autoAttributeConfig end function TreeTabClass:SaveMasteryPopup(node, listControl) diff --git a/src/Modules/Build.lua b/src/Modules/Build.lua index 170a4ccd6..0cb871da8 100644 --- a/src/Modules/Build.lua +++ b/src/Modules/Build.lua @@ -1066,6 +1066,7 @@ function buildMode:ResetModFlags() self.configTab.modFlag = false self.treeTab.modFlag = false self.treeTab.searchFlag = false + self.treeTab.autoAttrFlag = false self.spec.modFlag = false self.skillsTab.modFlag = false self.itemsTab.modFlag = false @@ -1185,7 +1186,7 @@ function buildMode:OnFrame(inputEvents) self.calcsTab:Draw(tabViewPort, inputEvents) end - self.unsaved = self.modFlag or self.notesTab.modFlag or self.partyTab.modFlag or self.configTab.modFlag or self.treeTab.modFlag or self.treeTab.searchFlag or self.spec.modFlag or self.skillsTab.modFlag or self.itemsTab.modFlag or self.calcsTab.modFlag + self.unsaved = self.modFlag or self.notesTab.modFlag or self.partyTab.modFlag or self.configTab.modFlag or self.treeTab.modFlag or self.treeTab.searchFlag or self.treeTab.autoAttrFlag or self.spec.modFlag or self.skillsTab.modFlag or self.itemsTab.modFlag or self.calcsTab.modFlag SetDrawLayer(5)