From b6eac49b005cb7426cae8d37da4cab5671c1fa40 Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:25:42 +0200 Subject: [PATCH 01/21] Add UI elements for Auto Attribute Allocation Adds a new button and a new popup to the TreeTab that allow the configuration of settings for automatic attribute allocation --- src/Classes/TreeTab.lua | 189 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 188 insertions(+), 1 deletion(-) diff --git a/src/Classes/TreeTab.lua b/src/Classes/TreeTab.lua index b06c51f954..e68c0862c4 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,11 @@ local TreeTabClass = newClass("TreeTab", "ControlHost", function(self, build) main:OpenPopup(470, 100, "Reset Tree", controls, nil, "edit", "cancel") end) + -- Automatic Attribute Allocation Button + -- TODO check if that's where/how I want autoAttribute button positioned + --local updateAutoAttributeConfigAnchor = function(anchor) self.controls.autoAttributeButton:SetAnchor("LEFT", anchor, "RIGHT") end + 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 +148,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) @@ -797,6 +804,186 @@ function TreeTabClass:ModifyAttributePopup(hoverNode) main:OpenPopup(550, 185, "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 = { } + -- Main popup window + local window = { + width = 450, + height = 330, + } + local config = copyTable(self.build.spec.autoAttributeConfig) + + -- TODO convert all sizes to static values instead? + + -- 'save' and 'cancel' buttons + local mainButton = { + y = m_floor(window.height * 0.9), + x = m_floor(window.width * 0.15), + } + local settingsSection = { + width = m_floor(window.width * 0.9), + height = m_floor(window.height * 0.5), + gapTop = m_floor(window.height * 0.25), + marginX = m_floor(window.width * 0.1), + marginY = 20, + } + local settingsColumns = { + [1] = { + id = "attribute", + header = "Attribute", + width = m_floor(window.width * 0.25), + height = 16, + }, + [2] = { + id = "weight", + header = "Weight", + width = m_floor(window.width * 0.15), + height = 16, + }, + [3] = { + id = "maxVal", + header = "Max Value", + width = m_floor(window.width * 0.15), + height = 16, + }, + [4] = { + id = "useMaxVal", + header = "Limit to Max?", + width = m_floor(window.width * 0.15), + height = 16, + }, + } + + -- Main Checkbox + controls.enabledLabel = new("LabelControl", nil, { m_floor(-window.width * 0.2), m_floor(window.height * 0.10), m_floor(window.width * 0.3), 16 }, "^7Automatic Attribute Allocation") + controls.enabledCheck = new("CheckBoxControl", { "LEFT", controls.enabledLabel, "RIGHT" }, { 10, 0, 18 }, "", function(value) config.enabled = 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 detail setting + -- Headers + 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"} + 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 .. "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 .. "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.save = new("ButtonControl", nil, { -mainButton.x, mainButton.y, 100, 20 }, "Save", 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, "save", nil, "cancel") +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 strting 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["ReqDex"] or 0 + autoAttributeConfig.int.weight = self.build.calcsTab.mainOutput["ReqInt"] or 0 + autoAttributeConfig.str.weight = self.build.calcsTab.mainOutput["ReqStr"] or 0 + 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) if listControl.selValue == nil then return From 9bc61e907709eea02c272b15654405b1d98afc22 Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:33:26 +0200 Subject: [PATCH 02/21] Add processing of `autoAttributeConfig` - If an `autoAttributeConfig` exists and is enabled, any attribute pathing nodes will be allocated according to the configured weightings and options. - Holding a hotkey will still have priority and right-clicking will also remain unchanged. - Attributes that are gained from non-attribute nodes on the path are taken into account *before* deciding the allocation --- src/Classes/PassiveSpec.lua | 104 ++++++++++++++++++++++++++++++++ src/Classes/PassiveTreeView.lua | 6 +- 2 files changed, 107 insertions(+), 3 deletions(-) diff --git a/src/Classes/PassiveSpec.lua b/src/Classes/PassiveSpec.lua index e6314c3503..d9c7e304f4 100644 --- a/src/Classes/PassiveSpec.lua +++ b/src/Classes/PassiveSpec.lua @@ -706,11 +706,26 @@ function PassiveSpecClass:AllocNode(node, altPath) node.allocMode = (node.ascendancyName or node.type == "Keystone" or node.type == "Socket" or node.containJewelSocket) and 0 or self.allocMode self.allocNodes[node.id] = node else + local cachedPlayerAttr = nil -- Used for iterative, automatic determination of desired attribute nodes + local cachedPathAttrResults = nil --Used for temp storage of mods gained from the nodes, which are not yet included in the playerModDb until after allocation + + if self.autoAttributeConfig and self.autoAttributeConfig.enabled and ((((altPath and altPath.pathDist) or 0) > 1) or ((node.pathDist or 0) > 1) ) then + 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) + 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 if pathNode.isAttribute then + if 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(pathNode.id, self.attributeIndex or 1) end self.allocNodes[pathNode.id] = pathNode @@ -2079,3 +2094,92 @@ 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 + + playerAttr.sumTotal = playerAttr.dex.total + playerAttr.int.total + playerAttr.str.total + playerAttr.dex.ratio = playerAttr.dex.total / playerAttr.sumTotal + playerAttr.int.ratio = playerAttr.int.total / playerAttr.sumTotal + playerAttr.str.ratio = playerAttr.str.total / playerAttr.sumTotal + + local maxDiff = 0 + local neededAttr = nil + + -- Update weights based on attribute requirements if necessary + if autoAttributeConfig.useAttrReq then + self.autoAttributeConfig = self.build.treeTab:UpdateAutoAttributeConfig(autoAttributeConfig) + end + + for _, attr in ipairs(attributeList) do + -- Check if the max value is set and if it's already been exceeded. + if autoAttributeConfig[attr].max == nil or (not autoAttributeConfig[attr].useMaxVal) or playerAttr[attr].total < autoAttributeConfig[attr].max then + local diff = autoAttributeConfig[attr].ratio - playerAttr[attr].ratio + if 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, attrResults) + attrResults = attrResults 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 bb609bac5a..bc3b264b57 100644 --- a/src/Classes/PassiveTreeView.lua +++ b/src/Classes/PassiveTreeView.lua @@ -325,11 +325,11 @@ 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 From 2d3bba38fd719a69a7f443a7f5fc45b377734e05 Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Tue, 23 Sep 2025 11:37:30 +0200 Subject: [PATCH 03/21] Enable save and load functionality `autoAttributeConfig` is saved on a per tree/spec basis to the xml, so that different trees can have different configs. --- src/Classes/PassiveSpec.lua | 49 +++++++++++++++++++++++++++++++++++++ src/Modules/Build.lua | 3 ++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/Classes/PassiveSpec.lua b/src/Classes/PassiveSpec.lua index d9c7e304f4..e2efba19da 100644 --- a/src/Classes/PassiveSpec.lua +++ b/src/Classes/PassiveSpec.lua @@ -130,6 +130,32 @@ function PassiveSpecClass:Load(xml, dbFileName) for nodeId in node.attrib.nodes:gmatch("%d+") do weaponSets[tonumber(nodeId)] = weaponSet end + elseif node.elem == "AutoAttributeConfig" then + -- TODO continue autoAttributeConfig loading + 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 +281,29 @@ 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) + end end diff --git a/src/Modules/Build.lua b/src/Modules/Build.lua index 1ead505231..8e8124a6ec 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) From 2b4d8f65f1267127db0f8b26ceb4a4598a2c098b Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Mon, 29 Sep 2025 10:51:21 +0200 Subject: [PATCH 04/21] Fix behavior for saved configs `autoAttributeConfigSaved` wasn't updated when saving a build --- src/Classes/PassiveSpec.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Classes/PassiveSpec.lua b/src/Classes/PassiveSpec.lua index e2efba19da..65367e5404 100644 --- a/src/Classes/PassiveSpec.lua +++ b/src/Classes/PassiveSpec.lua @@ -303,6 +303,7 @@ function PassiveSpecClass:Save(xml) t_insert(autoAttributeConfig, attrEntry) end t_insert(xml, autoAttributeConfig) + self.autoAttributeConfigSaved = copyTable(self.autoAttributeConfig) end end From b831f135244d4e3078647d7773b8f875b02f3235 Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Mon, 29 Sep 2025 11:24:52 +0200 Subject: [PATCH 05/21] Fix calculation for non-attribute passives - Didn't properly cache all attributes gained from all non-attribute nodes in path - Also changes `maxValue` to use `<=` rather than just `<` --- src/Classes/PassiveSpec.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Classes/PassiveSpec.lua b/src/Classes/PassiveSpec.lua index 65367e5404..9d3de845ab 100644 --- a/src/Classes/PassiveSpec.lua +++ b/src/Classes/PassiveSpec.lua @@ -759,11 +759,11 @@ function PassiveSpecClass:AllocNode(node, altPath) local cachedPlayerAttr = nil -- Used for iterative, automatic determination of desired attribute nodes local cachedPathAttrResults = nil --Used for temp storage of mods gained from the nodes, which are not yet included in the playerModDb until after allocation - if self.autoAttributeConfig and self.autoAttributeConfig.enabled and ((((altPath and altPath.pathDist) or 0) > 1) or ((node.pathDist or 0) > 1) ) then + if self.autoAttributeConfig and self.autoAttributeConfig.enabled and ((((altPath and #altPath) or 0) > 1) or ((node.pathDist or 0) > 1) ) then 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 = self:GetTempPathAttributeResults(pathNode.finalModList, cachedPathAttrResults) end end end @@ -2204,7 +2204,7 @@ function PassiveSpecClass:GetAutoAttribute(cachedPlayerAttr, cachedPathAttrResul for _, attr in ipairs(attributeList) do -- Check if the max value is set and if it's already been exceeded. - if autoAttributeConfig[attr].max == nil or (not autoAttributeConfig[attr].useMaxVal) or playerAttr[attr].total < autoAttributeConfig[attr].max then + if autoAttributeConfig[attr].max == nil or (not autoAttributeConfig[attr].useMaxVal) or playerAttr[attr].total <= autoAttributeConfig[attr].max then local diff = autoAttributeConfig[attr].ratio - playerAttr[attr].ratio if diff > maxDiff then maxDiff = diff @@ -2222,8 +2222,8 @@ function PassiveSpecClass:GetAutoAttribute(cachedPlayerAttr, cachedPathAttrResul end -- Analyzes a `finalModList` from a path with respect to effects on `dex`/ `int` / `str` for use in `GetAutoAttribute` -function PassiveSpecClass:GetTempPathAttributeResults(modList, attrResults) - attrResults = attrResults or { dex = { }, int= { }, str = { } } +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) From e3f9da66966375d5319333b556d0f62d365f6d3c Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Mon, 29 Sep 2025 11:43:51 +0200 Subject: [PATCH 06/21] Renable hotkey functionality as override option Even with a set config, attributes can be forced via hotkey or swapped via right click --- src/Classes/PassiveSpec.lua | 8 ++++---- src/Classes/PassiveTreeView.lua | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Classes/PassiveSpec.lua b/src/Classes/PassiveSpec.lua index 9d3de845ab..d6a11bf4cf 100644 --- a/src/Classes/PassiveSpec.lua +++ b/src/Classes/PassiveSpec.lua @@ -744,7 +744,7 @@ 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, hotkeyPressed) if not node.path then -- Node cannot be connected to the tree as there is no possible path return @@ -759,7 +759,7 @@ function PassiveSpecClass:AllocNode(node, altPath) local cachedPlayerAttr = nil -- Used for iterative, automatic determination of desired attribute nodes local cachedPathAttrResults = nil --Used for temp storage of mods gained from the nodes, which are not yet included in the playerModDb until after allocation - if self.autoAttributeConfig and self.autoAttributeConfig.enabled and ((((altPath and #altPath) or 0) > 1) or ((node.pathDist or 0) > 1) ) then + if (not hotkeyPressed) and self.autoAttributeConfig and self.autoAttributeConfig.enabled and ((((altPath and #altPath) or 0) > 1) or ((node.pathDist or 0) > 1) ) then 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 @@ -770,9 +770,9 @@ function PassiveSpecClass:AllocNode(node, altPath) 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 - if self.autoAttributeConfig and self.autoAttributeConfig.enabled then + if (not hotkeyPressed) 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 diff --git a/src/Classes/PassiveTreeView.lua b/src/Classes/PassiveTreeView.lua index bc3b264b57..2951485f24 100644 --- a/src/Classes/PassiveTreeView.lua +++ b/src/Classes/PassiveTreeView.lua @@ -333,7 +333,7 @@ function PassiveTreeViewClass:Draw(build, viewPort, inputEvents) 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, hotkeyPressed) spec:AddUndoState() build.buildFlag = true end From e49f9d950c0056bb15e931f4812f6a778196656a Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Mon, 29 Sep 2025 15:23:23 +0200 Subject: [PATCH 07/21] Toggle controls based on `controls.enabledCheck` Disable/Enable the other buttons/fields, when automatic attribute allocation checkbox is marked/unmarked --- src/Classes/TreeTab.lua | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/Classes/TreeTab.lua b/src/Classes/TreeTab.lua index e68c0862c4..b6caf39746 100644 --- a/src/Classes/TreeTab.lua +++ b/src/Classes/TreeTab.lua @@ -809,7 +809,17 @@ 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 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 + -- Main popup window local window = { width = 450, @@ -860,7 +870,11 @@ function TreeTabClass:ConfigureAutoAttributePopup() -- Main Checkbox controls.enabledLabel = new("LabelControl", nil, { m_floor(-window.width * 0.2), m_floor(window.height * 0.10), m_floor(window.width * 0.3), 16 }, "^7Automatic Attribute Allocation") - controls.enabledCheck = new("CheckBoxControl", { "LEFT", controls.enabledLabel, "RIGHT" }, { 10, 0, 18 }, "", function(value) config.enabled = 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) + 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 detail setting -- Headers @@ -914,7 +928,7 @@ function TreeTabClass:ConfigureAutoAttributePopup() 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.save = new("ButtonControl", nil, { -mainButton.x, mainButton.y, 100, 20 }, "Save", function() + controls.apply = new("ButtonControl", nil, { -mainButton.x, mainButton.y, 100, 20 }, "Apply", function() self.build.spec.autoAttributeConfig = self:UpdateAutoAttributeConfig(copyTable(config)) @@ -928,7 +942,9 @@ function TreeTabClass:ConfigureAutoAttributePopup() main:ClosePopup() end) - main:OpenPopup(window.width, window.height, "Auto Attribute Config", controls, "save", nil, "cancel") + 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 From 859a4ab22df2e058ee637a2cdca24f3f770ffcb6 Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Mon, 29 Sep 2025 15:25:42 +0200 Subject: [PATCH 08/21] Enable "tab" key navigation for `weight` and `max` --- src/Classes/TreeTab.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Classes/TreeTab.lua b/src/Classes/TreeTab.lua index b6caf39746..cef4026c78 100644 --- a/src/Classes/TreeTab.lua +++ b/src/Classes/TreeTab.lua @@ -886,6 +886,7 @@ function TreeTabClass:ConfigureAutoAttributePopup() 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) @@ -897,7 +898,9 @@ function TreeTabClass:ConfigureAutoAttributePopup() 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) From cb431e9c16a609637bc83b2bf359d60e8970aa34 Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Tue, 7 Oct 2025 12:13:32 +0200 Subject: [PATCH 09/21] Fix behavior for "intuitiveLeapLikesAffecting" Automatic attribute allocation wasn't working with effects like "From Nothing" or "Controlled Metamorphosis" --- src/Classes/PassiveSpec.lua | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/Classes/PassiveSpec.lua b/src/Classes/PassiveSpec.lua index d6a11bf4cf..99556dc323 100644 --- a/src/Classes/PassiveSpec.lua +++ b/src/Classes/PassiveSpec.lua @@ -750,16 +750,27 @@ function PassiveSpecClass:AllocNode(node, altPath, hotkeyPressed) 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 hotkeyPressed) 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 - local cachedPlayerAttr = nil -- Used for iterative, automatic determination of desired attribute nodes - local cachedPathAttrResults = nil --Used for temp storage of mods gained from the nodes, which are not yet included in the playerModDb until after allocation - if (not hotkeyPressed) 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 @@ -772,11 +783,7 @@ function PassiveSpecClass:AllocNode(node, altPath, hotkeyPressed) 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, configured auto attribute, or default to Strength if allocating before choosing an attribute if pathNode.isAttribute then - if (not hotkeyPressed) 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(pathNode.id, self.attributeIndex or 1) + handleAttributeNode(pathNode) end self.allocNodes[pathNode.id] = pathNode end From 0df97ce3af5d748d0d1b28a409a6ea0602922b1d Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Tue, 7 Oct 2025 12:33:56 +0200 Subject: [PATCH 10/21] Fix right-click behavior for "intuitiveLeapLikes" Previous fix led to right-click attribute switching taking into account auto attribute ratios, which is not intended --- src/Classes/PassiveSpec.lua | 6 +++--- src/Classes/PassiveTreeView.lua | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Classes/PassiveSpec.lua b/src/Classes/PassiveSpec.lua index 99556dc323..12aa8ed1ec 100644 --- a/src/Classes/PassiveSpec.lua +++ b/src/Classes/PassiveSpec.lua @@ -744,7 +744,7 @@ 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, hotkeyPressed) +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 @@ -754,7 +754,7 @@ function PassiveSpecClass:AllocNode(node, altPath, hotkeyPressed) 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 hotkeyPressed) and self.autoAttributeConfig and self.autoAttributeConfig.enabled then + 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 @@ -769,7 +769,7 @@ function PassiveSpecClass:AllocNode(node, altPath, hotkeyPressed) end self.allocNodes[node.id] = node else - if (not hotkeyPressed) and self.autoAttributeConfig and self.autoAttributeConfig.enabled and ((((altPath and #altPath) or 0) > 1) or ((node.pathDist or 0) > 1) ) then + 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 diff --git a/src/Classes/PassiveTreeView.lua b/src/Classes/PassiveTreeView.lua index 2951485f24..04be8105a4 100644 --- a/src/Classes/PassiveTreeView.lua +++ b/src/Classes/PassiveTreeView.lua @@ -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, hotkeyPressed) + 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 From 4b1201d391d49ee0aff89194cc2203f34fdd6bb2 Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Tue, 7 Oct 2025 13:00:00 +0200 Subject: [PATCH 11/21] Fix `maxValue` to use `<` instead of `<=` --- src/Classes/PassiveSpec.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Classes/PassiveSpec.lua b/src/Classes/PassiveSpec.lua index 12aa8ed1ec..b87c545c24 100644 --- a/src/Classes/PassiveSpec.lua +++ b/src/Classes/PassiveSpec.lua @@ -2211,7 +2211,7 @@ function PassiveSpecClass:GetAutoAttribute(cachedPlayerAttr, cachedPathAttrResul for _, attr in ipairs(attributeList) do -- Check if the max value is set and if it's already been exceeded. - if autoAttributeConfig[attr].max == nil or (not autoAttributeConfig[attr].useMaxVal) or playerAttr[attr].total <= autoAttributeConfig[attr].max then + if autoAttributeConfig[attr].max == nil or (not autoAttributeConfig[attr].useMaxVal) or playerAttr[attr].total < autoAttributeConfig[attr].max then local diff = autoAttributeConfig[attr].ratio - playerAttr[attr].ratio if diff > maxDiff then maxDiff = diff From 85a70cc14425084aec8037eb72c3f12fb3f1a9b9 Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Tue, 7 Oct 2025 15:16:26 +0200 Subject: [PATCH 12/21] Add tooltip hint on hovering over attribute node --- src/Classes/PassiveTreeView.lua | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/Classes/PassiveTreeView.lua b/src/Classes/PassiveTreeView.lua index 04be8105a4..b015fea59c 100644 --- a/src/Classes/PassiveTreeView.lua +++ b/src/Classes/PassiveTreeView.lua @@ -1385,6 +1385,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) @@ -1529,6 +1536,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 From 70b8238ec19d1e7576493cfa0a39aa4d4e9a4858 Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Tue, 7 Oct 2025 16:00:02 +0200 Subject: [PATCH 13/21] Add hint to `ModifyAttributePopup` --- src/Classes/TreeTab.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Classes/TreeTab.lua b/src/Classes/TreeTab.lua index cef4026c78..7da4e51645 100644 --- a/src/Classes/TreeTab.lua +++ b/src/Classes/TreeTab.lua @@ -801,7 +801,11 @@ 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 From c03f8d79fa64a9c3dcf98da4351c3ae87a53006a Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Tue, 7 Oct 2025 16:00:45 +0200 Subject: [PATCH 14/21] Add section on "Auto Attribute Config" to help.txt --- help.txt | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/help.txt b/help.txt index 739640a18f..7f798984df 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. + + From 69ed68b15c5dec83d3dbf639fde3394fcf93bc40 Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Tue, 7 Oct 2025 17:03:58 +0200 Subject: [PATCH 15/21] Fix typo "strting" to "starting" --- src/Classes/TreeTab.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Classes/TreeTab.lua b/src/Classes/TreeTab.lua index 1a83f3a3a0..233e86fa7d 100644 --- a/src/Classes/TreeTab.lua +++ b/src/Classes/TreeTab.lua @@ -969,7 +969,7 @@ function TreeTabClass:InitAutoAttributeConfig() end -- Update calculated and potentially static values that are not part of the autoAttributeConfig popup form ----@param autoAttributeConfig table | nil the autoAttributeConfig you're strting from, if any +---@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) From 3fc2659d90c77ceccd53a506f3a8328bd144796b Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Wed, 8 Oct 2025 10:43:31 +0200 Subject: [PATCH 16/21] Make UI dimensions static and clean up TODOs --- src/Classes/PassiveSpec.lua | 1 - src/Classes/TreeTab.lua | 41 ++++++++++++++++++------------------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/Classes/PassiveSpec.lua b/src/Classes/PassiveSpec.lua index 8727cc8016..e0570d1527 100644 --- a/src/Classes/PassiveSpec.lua +++ b/src/Classes/PassiveSpec.lua @@ -131,7 +131,6 @@ function PassiveSpecClass:Load(xml, dbFileName) weaponSets[tonumber(nodeId)] = weaponSet end elseif node.elem == "AutoAttributeConfig" then - -- TODO continue autoAttributeConfig loading local autoAttributeConfig = { } if node.attrib then autoAttributeConfig.enabled = node.attrib.enabled == "true" diff --git a/src/Classes/TreeTab.lua b/src/Classes/TreeTab.lua index 233e86fa7d..839f6c5eaa 100644 --- a/src/Classes/TreeTab.lua +++ b/src/Classes/TreeTab.lua @@ -134,9 +134,7 @@ local TreeTabClass = newClass("TreeTab", "ControlHost", function(self, build) main:OpenPopup(470, 100, "Reset Tree", controls, nil, "edit", "cancel") end) - -- Automatic Attribute Allocation Button - -- TODO check if that's where/how I want autoAttribute button positioned - --local updateAutoAttributeConfigAnchor = function(anchor) self.controls.autoAttributeButton:SetAnchor("LEFT", anchor, "RIGHT") 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 @@ -815,6 +813,8 @@ function TreeTabClass:ConfigureAutoAttributePopup() 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 @@ -823,65 +823,64 @@ function TreeTabClass:ConfigureAutoAttributePopup() end end end - + + -- UI dimensions -- Main popup window local window = { width = 450, height = 330, } - local config = copyTable(self.build.spec.autoAttributeConfig) - - -- TODO convert all sizes to static values instead? - -- 'save' and 'cancel' buttons local mainButton = { - y = m_floor(window.height * 0.9), - x = m_floor(window.width * 0.15), + y = 290, + x = 60, } + -- config settings local settingsSection = { - width = m_floor(window.width * 0.9), - height = m_floor(window.height * 0.5), - gapTop = m_floor(window.height * 0.25), - marginX = m_floor(window.width * 0.1), + width = 400, + height = 165, + gapTop = 80, + marginX = 45, marginY = 20, } local settingsColumns = { [1] = { id = "attribute", header = "Attribute", - width = m_floor(window.width * 0.25), + width = 110, height = 16, }, [2] = { id = "weight", header = "Weight", - width = m_floor(window.width * 0.15), + width = 65, height = 16, }, [3] = { id = "maxVal", header = "Max Value", - width = m_floor(window.width * 0.15), + width = 65, height = 16, }, [4] = { id = "useMaxVal", header = "Limit to Max?", - width = m_floor(window.width * 0.15), + width = 65, height = 16, }, } + -- Actual control elements -- Main Checkbox - controls.enabledLabel = new("LabelControl", nil, { m_floor(-window.width * 0.2), m_floor(window.height * 0.10), m_floor(window.width * 0.3), 16 }, "^7Automatic Attribute Allocation") + 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 detail setting - -- Headers + -- 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" } From 60391d160fee44db4c24fc0710722845ee0a0e36 Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Wed, 8 Oct 2025 12:13:45 +0200 Subject: [PATCH 17/21] Protect against `0` total attribute edge case --- src/Classes/PassiveSpec.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Classes/PassiveSpec.lua b/src/Classes/PassiveSpec.lua index e0570d1527..031f90c6c0 100644 --- a/src/Classes/PassiveSpec.lua +++ b/src/Classes/PassiveSpec.lua @@ -2205,7 +2205,7 @@ function PassiveSpecClass:GetAutoAttribute(cachedPlayerAttr, cachedPathAttrResul end end - playerAttr.sumTotal = playerAttr.dex.total + playerAttr.int.total + playerAttr.str.total + playerAttr.sumTotal = m_max(1, playerAttr.dex.total + playerAttr.int.total + playerAttr.str.total ) -- use m_max to protect against division by 0 (e.g. in "Omniscience"-like scenarios) playerAttr.dex.ratio = playerAttr.dex.total / playerAttr.sumTotal playerAttr.int.ratio = playerAttr.int.total / playerAttr.sumTotal playerAttr.str.ratio = playerAttr.str.total / playerAttr.sumTotal From 0bd5b49cd99bf190950ede47b76d9b8254acc489 Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Thu, 9 Oct 2025 13:31:35 +0200 Subject: [PATCH 18/21] Fix 'maxValue' not working for strength `maxDiff` was initialized at `0`, which led to problems when the difference in attribute ratio was nominally negative. There is secondary issue that needs to be fixed related to this, but at least this ensures that maximum value always applies as a limit. The other issue is that target ratios should be recalculated if a maximum value is reached because it changes the relative weights of the remaining attributes. Will be done in separate commit --- src/Classes/PassiveSpec.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Classes/PassiveSpec.lua b/src/Classes/PassiveSpec.lua index 031f90c6c0..3e24152541 100644 --- a/src/Classes/PassiveSpec.lua +++ b/src/Classes/PassiveSpec.lua @@ -2210,7 +2210,7 @@ function PassiveSpecClass:GetAutoAttribute(cachedPlayerAttr, cachedPathAttrResul playerAttr.int.ratio = playerAttr.int.total / playerAttr.sumTotal playerAttr.str.ratio = playerAttr.str.total / playerAttr.sumTotal - local maxDiff = 0 + local maxDiff = nil local neededAttr = nil -- Update weights based on attribute requirements if necessary @@ -2222,7 +2222,7 @@ function PassiveSpecClass:GetAutoAttribute(cachedPlayerAttr, cachedPathAttrResul -- Check if the max value is set and if it's already been exceeded. if autoAttributeConfig[attr].max == nil or (not autoAttributeConfig[attr].useMaxVal) or playerAttr[attr].total < autoAttributeConfig[attr].max then local diff = autoAttributeConfig[attr].ratio - playerAttr[attr].ratio - if diff > maxDiff then + if (maxDiff == nil) or (diff > maxDiff) then maxDiff = diff neededAttr = attr end From 8f490f14af39b95d952a460f45bb7e33399569c3 Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Thu, 9 Oct 2025 14:34:50 +0200 Subject: [PATCH 19/21] Fix ratio inaccuracy after reaching max values The weights and actual attribute values of attributes that had already reached maximum values were still taken into account when calculating current and target ratios, leading to wrong effective weights. Now, if weights are `str: 10 / dex: 2 / int: 1` and strength reaches its limit, it will be treated as `str: 0 / dex: 2 / int: 1` --- src/Classes/PassiveSpec.lua | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/src/Classes/PassiveSpec.lua b/src/Classes/PassiveSpec.lua index 3e24152541..ff5acf6893 100644 --- a/src/Classes/PassiveSpec.lua +++ b/src/Classes/PassiveSpec.lua @@ -2205,23 +2205,38 @@ function PassiveSpecClass:GetAutoAttribute(cachedPlayerAttr, cachedPathAttrResul end end - playerAttr.sumTotal = m_max(1, playerAttr.dex.total + playerAttr.int.total + playerAttr.str.total ) -- use m_max to protect against division by 0 (e.g. in "Omniscience"-like scenarios) - playerAttr.dex.ratio = playerAttr.dex.total / playerAttr.sumTotal - playerAttr.int.ratio = playerAttr.int.total / playerAttr.sumTotal - playerAttr.str.ratio = playerAttr.str.total / playerAttr.sumTotal - - local maxDiff = nil - local neededAttr = nil - -- 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 + 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 - -- Check if the max value is set and if it's already been exceeded. - if autoAttributeConfig[attr].max == nil or (not autoAttributeConfig[attr].useMaxVal) or playerAttr[attr].total < autoAttributeConfig[attr].max then - local diff = autoAttributeConfig[attr].ratio - playerAttr[attr].ratio + if playerAttr[attr].eligible then + local effConfigRatio = autoAttributeConfig[attr].weight / m_max(effConfigWeightTotal, 1 ) + local diff = effConfigRatio - playerAttr[attr].effRatio if (maxDiff == nil) or (diff > maxDiff) then maxDiff = diff neededAttr = attr From 4264498e41fce1e7fb12399e0121c77c27a24ea7 Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Thu, 9 Oct 2025 17:17:50 +0200 Subject: [PATCH 20/21] Add additional `weight = nil` protection --- src/Classes/PassiveSpec.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Classes/PassiveSpec.lua b/src/Classes/PassiveSpec.lua index ff5acf6893..1fb16040d9 100644 --- a/src/Classes/PassiveSpec.lua +++ b/src/Classes/PassiveSpec.lua @@ -2219,7 +2219,7 @@ function PassiveSpecClass:GetAutoAttribute(cachedPlayerAttr, cachedPathAttrResul else playerAttr[attr].eligible = true playerAttr[attr].effTotal = playerAttr[attr].total - effConfigWeightTotal = effConfigWeightTotal + autoAttributeConfig[attr].weight + effConfigWeightTotal = effConfigWeightTotal + (autoAttributeConfig[attr].weight or 0) end end @@ -2235,7 +2235,7 @@ function PassiveSpecClass:GetAutoAttribute(cachedPlayerAttr, cachedPathAttrResul -- 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 / m_max(effConfigWeightTotal, 1 ) + 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 From 952774deb7e39464aefa2357360c43f9a533bb60 Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Mon, 13 Oct 2025 15:16:30 +0200 Subject: [PATCH 21/21] Fix crash when loading save with `useAttrReq` The `Load()` function called `UpdateAutoAttributeConfig()` before `calcsTab.mainOutput` was initialized, which led to an error. I've added an additional `nil` check now --- src/Classes/TreeTab.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Classes/TreeTab.lua b/src/Classes/TreeTab.lua index 839f6c5eaa..6c556a9c01 100644 --- a/src/Classes/TreeTab.lua +++ b/src/Classes/TreeTab.lua @@ -993,9 +993,9 @@ function TreeTabClass:UpdateAutoAttributeConfig(autoAttributeConfig, addStaticIn -- Calculated values if autoAttributeConfig.useAttrReq then -- Make sure weights based on attribute requirements are up to date - autoAttributeConfig.dex.weight = self.build.calcsTab.mainOutput["ReqDex"] or 0 - autoAttributeConfig.int.weight = self.build.calcsTab.mainOutput["ReqInt"] or 0 - autoAttributeConfig.str.weight = self.build.calcsTab.mainOutput["ReqStr"] or 0 + 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)