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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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/29] 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) From b5d2fcdc94f6d01912081516c4f21d99c08968b2 Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Mon, 11 May 2026 12:23:41 +0200 Subject: [PATCH 22/29] Make autoAttributeConfig persist on tree convert --- src/Classes/TreeTab.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Classes/TreeTab.lua b/src/Classes/TreeTab.lua index ecba04e4a5..7c80e2de1e 100644 --- a/src/Classes/TreeTab.lua +++ b/src/Classes/TreeTab.lua @@ -591,6 +591,9 @@ function TreeTabClass:ConvertToVersion(version, remove, success, ignoreRuthlessC local newSpec = new("PassiveSpec", self.build, version) newSpec.title = self.build.spec.title newSpec.jewels = copyTable(self.build.spec.jewels) + if self.build.spec.autoAttributeConfig then + newSpec.autoAttributeConfig = copyTable(self.build.spec.autoAttributeConfig) + end newSpec:RestoreUndoState(self.build.spec:CreateUndoState(), version) newSpec:BuildClusterJewelGraphs() t_insert(self.specList, self.activeSpec + 1, newSpec) From a1d0daed130fcd27a286b7927d2e271b3e664e1f Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Mon, 11 May 2026 13:48:45 +0200 Subject: [PATCH 23/29] Fix `itemInc` referring to `playerModDB` --- 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 3e14b49c73..b15d46c842 100644 --- a/src/Classes/PassiveSpec.lua +++ b/src/Classes/PassiveSpec.lua @@ -2467,7 +2467,7 @@ function PassiveSpecClass:GetAutoAttribute(cachedPlayerAttr, cachedPathAttrResul -- 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].itemInc = itemModDB: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 From 2e477d7a133f54a64785328f62485a495ce08130 Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Mon, 11 May 2026 14:46:59 +0200 Subject: [PATCH 24/29] Add default value for attributes to `Data.lua` For default mod generation upon node allocation this is taken from the `tree.json` as far as I understand. But for interactions e.g. with radius jewels is good to know how much a base attribute an attribute passive gives by default --- src/Classes/PassiveSpec.lua | 2 +- src/Modules/Data.lua | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Classes/PassiveSpec.lua b/src/Classes/PassiveSpec.lua index b15d46c842..e91023d966 100644 --- a/src/Classes/PassiveSpec.lua +++ b/src/Classes/PassiveSpec.lua @@ -2442,7 +2442,7 @@ end ---@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 defaultAttrNodeValue = data.misc.DefaultAttrNodeValue local playerAttr local attributeList = { "dex", "int", "str" } if cachedPlayerAttr ~= nil then diff --git a/src/Modules/Data.lua b/src/Modules/Data.lua index a92579ce6e..34e856f7f6 100644 --- a/src/Modules/Data.lua +++ b/src/Modules/Data.lua @@ -172,6 +172,7 @@ data.misc = { -- magic numbers ServerTickTime = 0.033, ServerTickRate = 1 / 0.033, AccuracyPerDexBase = 6, + DefaultAttrNodeValue = 5, LowPoolThreshold = 0.35, TemporalChainsEffectCap = 75, BuffExpirationSlowCap = 0.25, From 2386237e7783611c1bdf3cd72f98bb508bdd16b2 Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Mon, 11 May 2026 16:58:33 +0200 Subject: [PATCH 25/29] Move playerAttrCache init to separate func Realized that I have to reuse the initialization logic later, so separated it into its own reusable func also removed some (of my own) obsolete code from `TreeTab` --- src/Classes/PassiveSpec.lua | 76 +++++++++++++++++++++++-------------- src/Classes/TreeTab.lua | 2 +- 2 files changed, 48 insertions(+), 30 deletions(-) diff --git a/src/Classes/PassiveSpec.lua b/src/Classes/PassiveSpec.lua index e91023d966..f6f4752462 100644 --- a/src/Classes/PassiveSpec.lua +++ b/src/Classes/PassiveSpec.lua @@ -2445,38 +2445,12 @@ function PassiveSpecClass:GetAutoAttribute(cachedPlayerAttr, cachedPathAttrResul local defaultAttrNodeValue = data.misc.DefaultAttrNodeValue local playerAttr local attributeList = { "dex", "int", "str" } + + -- Mod-based analysis is only performed once per path to reduce performance impact, otherwise cachedPlayerAttr is used 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 = itemModDB: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 + playerAttr = self:InitAutoAttributePlayerCache() end -- Update weights based on attribute requirements if necessary @@ -2538,3 +2512,47 @@ function PassiveSpecClass:GetTempPathAttributeResults(modList, cachedAttrResults return attrResults end + +-- Calculates initial total effects of player attribute mods, which is cached to avoid parsing mods on each pass +---@param attrOffset table | nil optional table `{str = number, dex = number, int = number}`. Used to manually offset certain stats, e.g. when re-allocating entire tree. Ignored if `nil` +---@return table playerAttr `{str/dex/int = statTable }`, statTable `{base, inc, more, itemBase, itemInc, itemMore, mult, total = number}` +function PassiveSpecClass:InitAutoAttributePlayerCache(attrOffset) + local attributeList = { "dex", "int", "str" } + local playerModDB = self.build.calcsTab.mainEnv.player.modDB + local itemModDB = self.build.calcsTab.mainEnv.itemModDB + local autoAttributeConfig = self.autoAttributeConfig or { } + + -- Initialize player attribute values + local 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 = itemModDB: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 + + -- Apply offset if provided + if attrOffset then + playerAttr[attr].base = playerAttr[attr].base - (attrOffset[attr] or 0) + end + + playerAttr[attr].mult = (1 + (playerAttr[attr].inc / 100)) * playerAttr[attr].more + playerAttr[attr].total = playerAttr[attr].base * playerAttr[attr].mult + end + + return playerAttr +end + diff --git a/src/Classes/TreeTab.lua b/src/Classes/TreeTab.lua index 7c80e2de1e..2164d58758 100644 --- a/src/Classes/TreeTab.lua +++ b/src/Classes/TreeTab.lua @@ -879,7 +879,7 @@ function TreeTabClass:ConfigureAutoAttributePopup() 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 + if not (key:find("enabled") or key:find("apply") or key:find("cancel")) then control.enabled = state end end From cb4590ad3f62444c678bcdfe91347c11301b8850 Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Mon, 11 May 2026 18:52:29 +0200 Subject: [PATCH 26/29] Add option to re-allocate all attributes at once New button added to the config popup that will allow re-allocating all travel attribute passives according to configured weights. This requires an additional confirmation, and can also be undone with "CRTL + Z" --- src/Classes/PassiveSpec.lua | 49 +++++++++++++++++++++++++++++++++++++ src/Classes/TreeTab.lua | 37 +++++++++++++++++++++++++--- 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/src/Classes/PassiveSpec.lua b/src/Classes/PassiveSpec.lua index f6f4752462..ae8af6bd51 100644 --- a/src/Classes/PassiveSpec.lua +++ b/src/Classes/PassiveSpec.lua @@ -2556,3 +2556,52 @@ function PassiveSpecClass:InitAutoAttributePlayerCache(attrOffset) return playerAttr end +-- Reallocates all basic attribute nodes, based on desired uder weights +function PassiveSpecClass:AutoReallocAllAttributeNodes() + if not self.autoAttributeConfig or not self.autoAttributeConfig.enabled then + return + end + + -- init player attribute cach with current tree, items, and config + local defaultAttrValue = data.misc.DefaultAttrNodeValue + local allNodes = self.build.spec.allocNodes + local attrBaseOffset = { str = 0, dex = 0, int = 0 } -- base values that need to be subtracted from cache + local attrNodes = { } + + -- Check for currently allocated base attribute nodes and record their effects + for _, node in pairs(allNodes) do + if node.isAttribute then + local isBaseAttr = true -- assume it's a "normal" attribute node + if node.dn == "Strength" then + attrBaseOffset.str = attrBaseOffset.str + defaultAttrValue + elseif node.dn == "Dexterity" then + attrBaseOffset.dex = attrBaseOffset.dex + defaultAttrValue + elseif node.dn == "Intelligence" then + attrBaseOffset.int = attrBaseOffset.int + defaultAttrValue + else + -- isAttribute, but not set to str/dex/int likely means something like Pathfinder's "Traveler's Wisdom" + isBaseAttr = false + end + + -- add to list for re-allocation + if isBaseAttr then + t_insert(attrNodes,node) + end + end + end + --self:CreateUndoState() + + -- Initialise "corrected" playerAttrCache, i.e. player stats, but with attributes from base attribute passives subtracted + local playerAttrCache = self:InitAutoAttributePlayerCache(attrBaseOffset) + local attrIndex + for _, attrNode in pairs(attrNodes) do + attrIndex, playerAttrCache = self:GetAutoAttribute(playerAttrCache) + self:SwitchAttributeNode(attrNode.id, attrIndex) + end + -- Rebuild/refresh paths and update stats + self:BuildAllDependsAndPaths() + self:AddUndoState() + self.build.buildFlag = true + +end + diff --git a/src/Classes/TreeTab.lua b/src/Classes/TreeTab.lua index 2164d58758..2bb975e18c 100644 --- a/src/Classes/TreeTab.lua +++ b/src/Classes/TreeTab.lua @@ -893,9 +893,15 @@ function TreeTabClass:ConfigureAutoAttributePopup() } -- 'save' and 'cancel' buttons local mainButton = { + width = 100, + height = 20, + x = { }, -- left/right/center button x values assigned after y = 290, - x = 60, } + mainButton.x.center = 0 + mainButton.x.left = m_floor(- mainButton.width - (mainButton.width * 0.1)) + mainButton.x.right = m_floor(mainButton.width + (mainButton.width * 0.1)) + -- config settings local settingsSection = { width = 400, @@ -995,17 +1001,40 @@ 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.apply = new("ButtonControl", nil, { -mainButton.x, mainButton.y, 100, 20 }, "Apply", function() - + local function applyChanges() 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 + end + + -- Apply changes button + controls.apply = new("ButtonControl", nil, { mainButton.x.left, mainButton.y, mainButton.width, mainButton.height }, "Save Config", function() + applyChanges() main:ClosePopup() end) - controls.cancel = new("ButtonControl", nil, { mainButton.x, mainButton.y, 100, 20 }, "Cancel", function() + + -- Re-allocate all button + controls.reallocateAll = new("ButtonControl", nil, { mainButton.x.center, mainButton.y, mainButton.width, mainButton.height }, "Re-Allocate All", + function() + -- Open confirmation popup first + main:OpenConfirmPopup( + "Confirm Re-Allocation", + "This will re-allocate all attribute travel nodes based on your current Auto Attribute settings.\nContinue?", + "Confirm", + function() + applyChanges() -- save changes first + self.build.spec:AutoReallocAllAttributeNodes() + main:ClosePopup() -- close main popup + end + ) + end + ) + + -- Cancel button + controls.cancel = new("ButtonControl", nil, { mainButton.x.right, mainButton.y, mainButton.width, mainButton.height }, "Cancel", function() main:ClosePopup() end) From 04132c6692b1cbaccb95e1956b349b59b4f5426c Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Mon, 11 May 2026 18:59:56 +0200 Subject: [PATCH 27/29] Appease spell-checking gods --- src/Classes/PassiveSpec.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Classes/PassiveSpec.lua b/src/Classes/PassiveSpec.lua index ae8af6bd51..e56966ecac 100644 --- a/src/Classes/PassiveSpec.lua +++ b/src/Classes/PassiveSpec.lua @@ -2556,13 +2556,13 @@ function PassiveSpecClass:InitAutoAttributePlayerCache(attrOffset) return playerAttr end --- Reallocates all basic attribute nodes, based on desired uder weights +-- Reallocates all basic attribute nodes, based on desired user weights function PassiveSpecClass:AutoReallocAllAttributeNodes() if not self.autoAttributeConfig or not self.autoAttributeConfig.enabled then return end - - -- init player attribute cach with current tree, items, and config + + -- init player attribute cache with current tree, items, and config local defaultAttrValue = data.misc.DefaultAttrNodeValue local allNodes = self.build.spec.allocNodes local attrBaseOffset = { str = 0, dex = 0, int = 0 } -- base values that need to be subtracted from cache From 41920c95f8c8b48661d6a70e44fc9b4abf9d3bb4 Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Tue, 12 May 2026 12:29:01 +0200 Subject: [PATCH 28/29] Make re-allocation respect active weapon set Re-allocating all nodes would previously affect nodes that were in the weapon tree for the non-active weapon set --- src/Classes/PassiveSpec.lua | 10 ++++++---- src/Classes/TreeTab.lua | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Classes/PassiveSpec.lua b/src/Classes/PassiveSpec.lua index e56966ecac..a7b908cb16 100644 --- a/src/Classes/PassiveSpec.lua +++ b/src/Classes/PassiveSpec.lua @@ -2565,13 +2565,15 @@ function PassiveSpecClass:AutoReallocAllAttributeNodes() -- init player attribute cache with current tree, items, and config local defaultAttrValue = data.misc.DefaultAttrNodeValue local allNodes = self.build.spec.allocNodes + local activeWeaponSet = self.build.itemsTab.activeItemSet.useSecondWeaponSet and 2 or 1 local attrBaseOffset = { str = 0, dex = 0, int = 0 } -- base values that need to be subtracted from cache local attrNodes = { } -- Check for currently allocated base attribute nodes and record their effects for _, node in pairs(allNodes) do if node.isAttribute then - local isBaseAttr = true -- assume it's a "normal" attribute node + -- Only attributes in generic passives or currently active weapon set passives affected + local isAffectedAttr = node.allocMode == 0 or node.allocMode == activeWeaponSet if node.dn == "Strength" then attrBaseOffset.str = attrBaseOffset.str + defaultAttrValue elseif node.dn == "Dexterity" then @@ -2579,12 +2581,12 @@ function PassiveSpecClass:AutoReallocAllAttributeNodes() elseif node.dn == "Intelligence" then attrBaseOffset.int = attrBaseOffset.int + defaultAttrValue else - -- isAttribute, but not set to str/dex/int likely means something like Pathfinder's "Traveler's Wisdom" - isBaseAttr = false + -- isAttribute in current set, but not set to str/dex/int likely means something like Pathfinder's "Traveler's Wisdom" + isAffectedAttr = false end -- add to list for re-allocation - if isBaseAttr then + if isAffectedAttr then t_insert(attrNodes,node) end end diff --git a/src/Classes/TreeTab.lua b/src/Classes/TreeTab.lua index 2bb975e18c..14e8e4c01e 100644 --- a/src/Classes/TreeTab.lua +++ b/src/Classes/TreeTab.lua @@ -1022,7 +1022,7 @@ function TreeTabClass:ConfigureAutoAttributePopup() -- Open confirmation popup first main:OpenConfirmPopup( "Confirm Re-Allocation", - "This will re-allocate all attribute travel nodes based on your current Auto Attribute settings.\nContinue?", + "This will re-allocate all attribute travel nodes based on your Auto Attribute settings for the currently active weapon set.\nContinue?", "Confirm", function() applyChanges() -- save changes first From 70194890d9d4acd109c2f57f543af94c4005565f Mon Sep 17 00:00:00 2001 From: majochem <77203255+majochem@users.noreply.github.com> Date: Tue, 12 May 2026 16:49:27 +0200 Subject: [PATCH 29/29] Fix non-pathing effect caching (which I broke) When allocating multiple nodes at once, the effect of non-pathing is pre-calculated and accounted for when assigning the travel attributes. I broke that logic in a previous commit when I introduced a separate "offset" logic. I've now repaired it and standardized the offset logic --- src/Classes/PassiveSpec.lua | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/src/Classes/PassiveSpec.lua b/src/Classes/PassiveSpec.lua index a7b908cb16..fbc638e81d 100644 --- a/src/Classes/PassiveSpec.lua +++ b/src/Classes/PassiveSpec.lua @@ -2450,7 +2450,7 @@ function PassiveSpecClass:GetAutoAttribute(cachedPlayerAttr, cachedPathAttrResul if cachedPlayerAttr ~= nil then playerAttr = cachedPlayerAttr else - playerAttr = self:InitAutoAttributePlayerCache() + playerAttr = self:InitAutoAttributePlayerCache(cachedPathAttrResults) end -- Update weights based on attribute requirements if necessary @@ -2514,8 +2514,8 @@ function PassiveSpecClass:GetTempPathAttributeResults(modList, cachedAttrResults end -- Calculates initial total effects of player attribute mods, which is cached to avoid parsing mods on each pass ----@param attrOffset table | nil optional table `{str = number, dex = number, int = number}`. Used to manually offset certain stats, e.g. when re-allocating entire tree. Ignored if `nil` ----@return table playerAttr `{str/dex/int = statTable }`, statTable `{base, inc, more, itemBase, itemInc, itemMore, mult, total = number}` +---@param attrOffset table | nil optional table `{str/dex/int = { base = number, inc = number, more = number } }`. Used to manually offset certain stats, e.g. when re-allocating entire tree. Ignored if `nil` +---@return table playerAttr table with subtable for each attribute: `{str/dex/int = {base = number, inc = number, more = number, itemBase = number, itemInc = number, itemMore = number, mult = number, total = number} }` function PassiveSpecClass:InitAutoAttributePlayerCache(attrOffset) local attributeList = { "dex", "int", "str" } local playerModDB = self.build.calcsTab.mainEnv.player.modDB @@ -2529,12 +2529,12 @@ function PassiveSpecClass:InitAutoAttributePlayerCache(attrOffset) 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) + playerAttr[attr].base = playerModDB:Sum("BASE", nil, attrUpper) + playerAttr[attr].inc = playerModDB:Sum("INC", nil, attrUpper) + playerAttr[attr].more = playerModDB:More(nil, attrUpper) -- 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 + -- 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 = itemModDB:Sum("INC", nil, attrUpper) @@ -2545,8 +2545,10 @@ function PassiveSpecClass:InitAutoAttributePlayerCache(attrOffset) end -- Apply offset if provided - if attrOffset then - playerAttr[attr].base = playerAttr[attr].base - (attrOffset[attr] or 0) + if attrOffset and attrOffset[attr] then + playerAttr[attr].base = playerAttr[attr].base + (attrOffset[attr].base or 0) + playerAttr[attr].inc = playerAttr[attr].inc + (attrOffset[attr].inc or 0) + playerAttr[attr].more = playerAttr[attr].more * (attrOffset[attr].more or 1) end playerAttr[attr].mult = (1 + (playerAttr[attr].inc / 100)) * playerAttr[attr].more @@ -2566,7 +2568,11 @@ function PassiveSpecClass:AutoReallocAllAttributeNodes() local defaultAttrValue = data.misc.DefaultAttrNodeValue local allNodes = self.build.spec.allocNodes local activeWeaponSet = self.build.itemsTab.activeItemSet.useSecondWeaponSet and 2 or 1 - local attrBaseOffset = { str = 0, dex = 0, int = 0 } -- base values that need to be subtracted from cache + local attrModOffsets = { + str = { base = 0 }, + dex = { base = 0 }, + int = { base = 0 } + } -- only contains offsets for 'base' values local attrNodes = { } -- Check for currently allocated base attribute nodes and record their effects @@ -2575,13 +2581,13 @@ function PassiveSpecClass:AutoReallocAllAttributeNodes() -- Only attributes in generic passives or currently active weapon set passives affected local isAffectedAttr = node.allocMode == 0 or node.allocMode == activeWeaponSet if node.dn == "Strength" then - attrBaseOffset.str = attrBaseOffset.str + defaultAttrValue + attrModOffsets.str.base = attrModOffsets.str.base - defaultAttrValue elseif node.dn == "Dexterity" then - attrBaseOffset.dex = attrBaseOffset.dex + defaultAttrValue + attrModOffsets.dex.base = attrModOffsets.dex.base - defaultAttrValue elseif node.dn == "Intelligence" then - attrBaseOffset.int = attrBaseOffset.int + defaultAttrValue + attrModOffsets.int.base = attrModOffsets.int.base - defaultAttrValue else - -- isAttribute in current set, but not set to str/dex/int likely means something like Pathfinder's "Traveler's Wisdom" + -- isAttribute in current set, but not set to str/dex/int likely means something like Pathfinder's "Traveler's Wisdom" or otherwise modified isAffectedAttr = false end @@ -2591,10 +2597,9 @@ function PassiveSpecClass:AutoReallocAllAttributeNodes() end end end - --self:CreateUndoState() -- Initialise "corrected" playerAttrCache, i.e. player stats, but with attributes from base attribute passives subtracted - local playerAttrCache = self:InitAutoAttributePlayerCache(attrBaseOffset) + local playerAttrCache = self:InitAutoAttributePlayerCache(attrModOffsets) local attrIndex for _, attrNode in pairs(attrNodes) do attrIndex, playerAttrCache = self:GetAutoAttribute(playerAttrCache)