diff --git a/spec/System/TestPassiveSpec_spec.lua b/spec/System/TestPassiveSpec_spec.lua index 2a6a248db4..d4fe6a5214 100644 --- a/spec/System/TestPassiveSpec_spec.lua +++ b/spec/System/TestPassiveSpec_spec.lua @@ -11,6 +11,82 @@ describe("TestPassiveSpec", function() end end + local function findNodeByName(spec, name) + for nodeId, node in pairs(spec.nodes) do + if node.name == name or node.dn == name then + return nodeId, node + end + end + end + + local function firstNormalJewelSocket(spec) + for nodeId, node in pairs(spec.nodes) do + if node.isJewelSocket and node.name == "Jewel Socket" then + return nodeId, node + end + end + end + + local function normalJewelSockets(spec, count) + local sockets = { } + for nodeId, node in pairs(spec.nodes) do + if node.isJewelSocket and node.name == "Jewel Socket" then + table.insert(sockets, { nodeId = nodeId, node = node }) + end + end + table.sort(sockets, function(a, b) return a.nodeId < b.nodeId end) + if count then + while #sockets > count do + table.remove(sockets) + end + end + return sockets + end + + local function makeAmulet(rawMod) + local item = new("Item", [[ +Rarity: RARE +Test Locket +Gold Amulet +-------- +Item Level: 80 +-------- +]] .. (rawMod or "") .. [[ +]]) + return item + end + + local function makeCustomAmulet(rawMod) + local item = makeAmulet(rawMod) + build.itemsTab:AddItem(item, true) + build.itemsTab.slots["Amulet"]:SetSelItemId(item.id) + build.buildFlag = true + return item + end + + local function socketJewel(nodeId, raw) + local item = new("Item", raw) + build.itemsTab:AddItem(item, true) + build.spec.jewels[nodeId] = item.id + if build.itemsTab.sockets[nodeId] then + build.itemsTab.sockets[nodeId]:SetSelItemId(item.id) + end + build.buildFlag = true + return item + end + + local function socketIntJewel(nodeId) + return socketJewel(nodeId, [[ +Rarity: RARE +Test Mind +Sapphire +-------- +Item Level: 80 +-------- ++10 to Intelligence +]]) + end + it("ignores stale jewel socket item ids when loading saved builds", function() local spec = new("PassiveSpec", build, latestTreeVersion) local socketNodeId = firstLoadedSocketNode(spec) @@ -44,6 +120,365 @@ describe("TestPassiveSpec", function() assert.is_true(ok, err) end) + it("grants Zarokh's Gift as an active free sinister jewel socket from item mods", function() + makeCustomAmulet("Allocates Zarokh's Gift") + runCallback("OnFrame") + + local nodeId, node = findNodeByName(build.spec, "Zarokh's Gift") + assert.is_not_nil(nodeId) + assert.is_true(node.isJewelSocket) + assert.is_not_nil(build.spec.allocNodes[nodeId]) + assert.is_true(build.spec.allocNodes[nodeId].isFreeAllocate) + assert.is_true(build.spec.allocNodes[nodeId].isGrantedPassive) + assert.is_not_nil(build.itemsTab.sockets[nodeId]) + assert.is_false(build.itemsTab.sockets[nodeId].inactive) + + local pointsUsed = build.spec:CountAllocNodes() + assert.are.equals(0, pointsUsed) + + local xml = { } + build.spec:Save(xml) + local savedNodeIds = { } + for savedNodeId in xml.attrib.nodes:gmatch("%d+") do + savedNodeIds[tonumber(savedNodeId)] = true + end + assert.is_nil(savedNodeIds[nodeId]) + end) + + it("jewel socketed in item-granted Zarokh's Gift contributes in the same recompute", function() + makeCustomAmulet("Allocates Zarokh's Gift") + runCallback("OnFrame") + + local nodeId = assert(findNodeByName(build.spec, "Zarokh's Gift")) + local beforeInt = build.calcsTab.mainOutput.Int or 0 + + socketJewel(nodeId, [[ +Rarity: RARE +Test Mind +Sapphire +-------- +Item Level: 80 +-------- ++10 to Intelligence +]]) + runCallback("OnFrame") + + assert.True((build.calcsTab.mainOutput.Int or 0) >= beforeInt + 10) + end) + + it("calculator replacement without Zarokh's Gift ignores jewels in stale granted sockets", function() + makeCustomAmulet("Allocates Zarokh's Gift") + runCallback("OnFrame") + + local nodeId = assert(findNodeByName(build.spec, "Zarokh's Gift")) + socketIntJewel(nodeId) + runCallback("OnFrame") + + local intWithSocket = build.calcsTab.mainOutput.Int or 0 + local calcFunc = build.calcsTab:GetMiscCalculator() + local output = calcFunc({ repSlotName = "Amulet", repItem = makeAmulet("") }, false) + + assert.are.equals(intWithSocket - 10, output.Int or 0) + assert.is_not_nil(build.spec.allocNodes[nodeId]) + end) + + it("calculator replacement granting Zarokh's Gift uses socket jewels without mutating the build spec", function() + makeCustomAmulet("") + runCallback("OnFrame") + + local nodeId = assert(findNodeByName(build.spec, "Zarokh's Gift")) + local baseInt = build.calcsTab.mainOutput.Int or 0 + assert.is_nil(build.spec.allocNodes[nodeId]) + + socketIntJewel(nodeId) + runCallback("OnFrame") + assert.are.equals(baseInt, build.calcsTab.mainOutput.Int or 0) + + local calcFunc = build.calcsTab:GetMiscCalculator() + local output = calcFunc({ repSlotName = "Amulet", repItem = makeAmulet("Allocates Zarokh's Gift") }, false) + + assert.are.equals(baseInt + 10, output.Int or 0) + assert.is_nil(build.spec.allocNodes[nodeId]) + end) + + it("does not allow unique jewels in item-granted Zarokh's Gift", function() + makeCustomAmulet("Allocates Zarokh's Gift") + runCallback("OnFrame") + + local nodeId = assert(findNodeByName(build.spec, "Zarokh's Gift")) + local voices = new("Item", [[ +Rarity: UNIQUE +Voices +Sapphire +-------- +Limited to: 1 +Allocates 2 Sinister Jewel sockets +Corrupted +]]) + build.itemsTab:AddItem(voices, true) + + assert.is_false(build.itemsTab:IsItemValidForSlot(voices, "Jewel " .. nodeId)) + + build.spec.jewels[nodeId] = voices.id + build.itemsTab.sockets[nodeId]:SetSelItemId(voices.id) + build.buildFlag = true + runCallback("OnFrame") + + assert.is_nil(build.spec.allocNodes[62152]) + assert.is_nil(build.spec.allocNodes[26178]) + end) + + it("resolves Voices sinister socket grants by alias order", function() + local nodes = build.spec:ResolveGrantedPassiveNodes("3 sinister jewel sockets") + assert.are.equals(3, #nodes) + assert.are.equals("voices_jewel_slot1", nodes[1].aliasPassiveSocket) + assert.are.equals("voices_jewel_slot2", nodes[2].aliasPassiveSocket) + assert.are.equals("voices_jewel_slot3__", nodes[3].aliasPassiveSocket) + end) + + it("Voices grants free Sinister Jewel sockets from an active tree jewel", function() + local hostNodeId, hostNode = firstNormalJewelSocket(build.spec) + build.spec:AllocNode(hostNode) + socketJewel(hostNodeId, [[ +Rarity: UNIQUE +Voices +Sapphire +-------- +Limited to: 1 +Allocates 3 Sinister Jewel sockets +Corrupted +]]) + + runCallback("OnFrame") + + local activeSinister = { } + for _, node in pairs(build.spec.allocNodes) do + if node.name == "Sinister Jewel Socket" then + activeSinister[node.aliasPassiveSocket] = true + assert.is_true(node.isFreeAllocate) + end + end + assert.is_true(activeSinister["voices_jewel_slot1"]) + assert.is_true(activeSinister["voices_jewel_slot2"]) + assert.is_true(activeSinister["voices_jewel_slot3__"]) + assert.is_nil(activeSinister["voices_jewel_slot4"]) + end) + + it("removes Voices-granted sockets when the Voices jewel is removed", function() + local hostNodeId, hostNode = firstNormalJewelSocket(build.spec) + build.spec:AllocNode(hostNode) + socketJewel(hostNodeId, [[ +Rarity: UNIQUE +Voices +Sapphire +-------- +Limited to: 1 +Allocates 2 Sinister Jewel sockets +Corrupted +]]) + runCallback("OnFrame") + assert.is_not_nil(build.spec.allocNodes[62152]) + + build.spec.jewels[hostNodeId] = 0 + if build.itemsTab.sockets[hostNodeId] then + build.itemsTab.sockets[hostNodeId]:SetSelItemId(0) + end + build.buildFlag = true + runCallback("OnFrame") + + assert.is_nil(build.spec.allocNodes[62152]) + assert.is_nil(build.spec.allocNodes[26178]) + end) + + it("does not remove a user-allocated passive when an item grant goes away", function() + local nodeId, node = findNodeByName(build.spec, "Acceleration") + assert.is_not_nil(nodeId) + build.spec:AllocNode(node) + local pointsUsed = build.spec:CountAllocNodes() + assert.is_true(pointsUsed > 0) + + makeCustomAmulet("Allocates Acceleration") + runCallback("OnFrame") + assert.is_not_nil(build.spec.allocNodes[nodeId]) + assert.is_nil(build.spec.allocNodes[nodeId].isFreeAllocate) + + build.itemsTab.slots["Amulet"]:SetSelItemId(0) + build.buildFlag = true + runCallback("OnFrame") + + assert.is_not_nil(build.spec.allocNodes[nodeId]) + assert.are.equals(pointsUsed, build.spec:CountAllocNodes()) + end) + + it("does not persist ordinary anoint notables as granted tree allocations", function() + local nodeId = assert(findNodeByName(build.spec, "Acceleration")) + runCallback("OnFrame") + assert.are.equals(1, build.calcsTab.mainOutput.MovementSpeedMod) + + makeCustomAmulet("Allocates Acceleration") + runCallback("OnFrame") + + assert.is_nil(build.spec.allocNodes[nodeId]) + assert.is_true(build.calcsTab.mainEnv.grantedPassives[nodeId]) + assert.are.equals(1.03, build.calcsTab.mainOutput.MovementSpeedMod) + end) + + it("shows Zarokh's Gift in the amulet anoint lookup", function() + if not common.classes.NotableDBControl then + LoadModule("Classes/NotableDBControl") + end + local control = new("NotableDBControl", {"TOPLEFT", nil, "TOPLEFT"}, {0, 0, 360, 400}, build.itemsTab, build.spec.tree.nodes, "ANOINT") + control:ListBuilder() + + local found + for _, node in ipairs(control.list) do + if node.dn == "Zarokh's Gift" then + found = true + break + end + end + assert.is_true(found) + end) + + it("anoint tooltip comparison includes jewels in a prospective Zarokh's Gift socket", function() + makeCustomAmulet("") + runCallback("OnFrame") + + local zarokhNodeId, zarokhNode = assert(findNodeByName(build.spec, "Zarokh's Gift")) + socketIntJewel(zarokhNodeId) + build.itemsTab.displayItem = makeAmulet("") + + local capturedBase + local capturedNew + local originalAddStatComparesToTooltip = build.AddStatComparesToTooltip + build.AddStatComparesToTooltip = function(_, tooltip, outputBase, outputNew) + capturedBase = outputBase + capturedNew = outputNew + return 1 + end + build.itemsTab:AppendAnointTooltip({ AddLine = function() end }, zarokhNode) + build.AddStatComparesToTooltip = originalAddStatComparesToTooltip + + assert.are.equals((capturedBase.Int or 0) + 10, capturedNew.Int or 0) + assert.is_nil(build.spec.allocNodes[zarokhNodeId]) + end) + + it("removes secondary socket grants from jewels in a removed granted socket", function() + makeCustomAmulet("Allocates Zarokh's Gift") + runCallback("OnFrame") + + local zarokhNodeId = assert(findNodeByName(build.spec, "Zarokh's Gift")) + socketJewel(zarokhNodeId, [[ +Rarity: RARE +Test Eye +Sapphire +-------- +Item Level: 80 +-------- +Allocates 2 Sinister Jewel sockets +]]) + runCallback("OnFrame") + assert.is_not_nil(build.spec.allocNodes[62152]) + assert.is_not_nil(build.spec.allocNodes[26178]) + + build.itemsTab.slots["Amulet"]:SetSelItemId(0) + build.buildFlag = true + runCallback("OnFrame") + + assert.is_nil(build.spec.allocNodes[zarokhNodeId]) + assert.is_nil(build.spec.allocNodes[62152]) + assert.is_nil(build.spec.allocNodes[26178]) + end) + + it("replaces Zarokh's Gift with an ordinary amulet anoint and removes the socket jewel effect", function() + makeCustomAmulet("Allocates Zarokh's Gift") + runCallback("OnFrame") + + local zarokhNodeId = assert(findNodeByName(build.spec, "Zarokh's Gift")) + local accelerationNodeId = assert(findNodeByName(build.spec, "Acceleration")) + local baseInt = build.calcsTab.mainOutput.Int or 0 + socketIntJewel(zarokhNodeId) + runCallback("OnFrame") + assert.are.equals(baseInt + 10, build.calcsTab.mainOutput.Int or 0) + + local amulet = makeAmulet("Allocates Acceleration") + build.itemsTab:AddItem(amulet, true) + build.itemsTab.slots["Amulet"]:SetSelItemId(amulet.id) + build.buildFlag = true + runCallback("OnFrame") + + assert.is_nil(build.spec.allocNodes[zarokhNodeId]) + assert.are.equals(baseInt, build.calcsTab.mainOutput.Int or 0) + assert.is_true(build.calcsTab.mainEnv.grantedPassives[accelerationNodeId]) + assert.are.equals(1.03, build.calcsTab.mainOutput.MovementSpeedMod) + end) + + it("ignores grants from duplicate limited Voices jewels", function() + local sockets = normalJewelSockets(build.spec, 2) + assert.are.equals(2, #sockets) + build.spec:AllocNode(sockets[1].node) + build.spec:AllocNode(sockets[2].node) + socketJewel(sockets[1].nodeId, [[ +Rarity: UNIQUE +Voices +Sapphire +-------- +Limited to: 1 +Allocates 2 Sinister Jewel sockets +Corrupted +]]) + socketJewel(sockets[2].nodeId, [[ +Rarity: UNIQUE +Voices +Sapphire +-------- +Limited to: 1 +Allocates 3 Sinister Jewel sockets +Corrupted +]]) + + runCallback("OnFrame") + + local activeSinister = { } + for _, node in pairs(build.spec.allocNodes) do + if node.name == "Sinister Jewel Socket" then + activeSinister[node.aliasPassiveSocket] = true + end + end + assert.is_true(activeSinister["voices_jewel_slot1"]) + assert.is_true(activeSinister["voices_jewel_slot2"]) + assert.is_nil(activeSinister["voices_jewel_slot3__"]) + end) + + it("jewel socketed in a Voices-granted Sinister socket contributes in the same recompute", function() + local hostNodeId, hostNode = firstNormalJewelSocket(build.spec) + build.spec:AllocNode(hostNode) + socketJewel(hostNodeId, [[ +Rarity: UNIQUE +Voices +Sapphire +-------- +Limited to: 1 +Allocates 3 Sinister Jewel sockets +Corrupted +]]) + runCallback("OnFrame") + + local beforeInt = build.calcsTab.mainOutput.Int or 0 + socketJewel(62152, [[ +Rarity: RARE +Test Mind +Sapphire +-------- +Item Level: 80 +-------- ++10 to Intelligence +]]) + runCallback("OnFrame") + + assert.True((build.calcsTab.mainOutput.Int or 0) >= beforeInt + 10) + end) + it("remaps legacy class ids only for trees before 0.4", function() local function loadClass(treeVersion, classId) local spec = new("PassiveSpec", build, latestTreeVersion) diff --git a/src/Classes/ItemsTab.lua b/src/Classes/ItemsTab.lua index 1f2de90fc0..40298334fd 100644 --- a/src/Classes/ItemsTab.lua +++ b/src/Classes/ItemsTab.lua @@ -2233,6 +2233,9 @@ function ItemsTabClass:IsItemValidForSlot(item, slotName, itemSet, flagState) local node = self.build.spec.tree.nodes[tonumber(slotId)] or self.build.spec.nodes[tonumber(slotId)] if not node or item.type ~= "Jewel" then return false + elseif self.build.spec:IsSinisterJewelSocketNode(node) and (item.rarity == "UNIQUE" or item.rarity == "RELIC") then + -- Sinister Jewel Sockets can only accept non-unique jewels + return false elseif node.containJewelSocket then if item.rarity == "UNIQUE" or item.rarity == "RELIC" or (item.base and item.base.subType ~= nil) then -- Lich socket can only accept basic non-unique jewels @@ -3567,12 +3570,18 @@ function ItemsTabClass:AddItemTooltip(tooltip, item, slot, dbMode, maxWidth) tooltip:AddLine(fontSizeBig, formattedModLine, "FONTIN SC", bg) end - -- Show mods from granted Notables + -- Show mods from granted passives if modLine.modList[1] and modLine.modList[1].name == "GrantedPassive" then - local node = self.build.spec.tree.notableMap[modLine.modList[1].value] - if node then - for _, stat in ipairs(node.sd) do - tooltip:AddLine(fontSizeBig, "^x7F7F7F"..stat, "FONTIN SC") + for _, node in ipairs(self.build.spec:ResolveGrantedPassiveNodes(modLine.modList[1].value)) do + local displayed = false + if node.sd then + for _, stat in ipairs(node.sd) do + tooltip:AddLine(fontSizeBig, "^x7F7F7F"..stat, "FONTIN SC") + displayed = true + end + end + if not displayed and node.isJewelSocket then + tooltip:AddLine(fontSizeBig, "^x7F7F7F"..(node.name or "Jewel Socket"), "FONTIN SC") end end -- Add separator only for anoints diff --git a/src/Classes/NotableDBControl.lua b/src/Classes/NotableDBControl.lua index c0a9277aa9..a524a09a76 100644 --- a/src/Classes/NotableDBControl.lua +++ b/src/Classes/NotableDBControl.lua @@ -12,7 +12,7 @@ local m_floor = math.floor local m_huge = math.huge local s_format = string.format -local emotionList = {"Ire", "Guilt", "Greed", "Paranoia", "Envy", "Disgust", "Despair", "Fear", "Suffering", "Isolation" } +local emotionList = {"Ire", "Guilt", "Greed", "Paranoia", "Envy", "Disgust", "Despair", "Fear", "Suffering", "Isolation", "Melancholy", "Ferocity", "Contempt" } ---@param node table ---@return boolean @@ -22,7 +22,7 @@ end ---@class NotableDBControl : ListControl local NotableDBClass = newClass("NotableDBControl", "ListControl", function(self, anchor, rect, itemsTab, db, dbType) - local headerHeight = 68 + local headerHeight = 96 local innerRect = {rect[1], rect[2]+headerHeight, rect[3], rect[4]-headerHeight} self.ListControl(anchor, innerRect, 16, "VERTICAL", false) self.itemsTab = itemsTab @@ -67,9 +67,9 @@ local NotableDBClass = newClass("NotableDBControl", "ListControl", function(self self.listBuildFlag = true end end - local function emoCheck(name, relTo) - local anchor = {"LEFT", relTo, "RIGHT"} - local rect = {2, 0, 26, 26} + local function emoCheck(name, relTo, newRow) + local anchor = newRow and {"TOPLEFT", relTo, "BOTTOMLEFT"} or {"LEFT", relTo, "RIGHT"} + local rect = newRow and {0, 2, 26, 26} or {2, 0, 26, 26} local ctl = new("CheckBoxControl", anchor, rect, "", emoCheckOnChange(name), "Distilled "..name, true) if self.emotionImages then ctl:SetCheckImage(self.emotionImages[name]) end return ctl @@ -77,7 +77,9 @@ local NotableDBClass = newClass("NotableDBControl", "ListControl", function(self local emotionCheckBoxes = {} for i,emo in ipairs(emotionList) do - local emoCtl = emoCheck(emo, emotionCheckBoxes[i-1] or self.controls.emotionLabel) + local newRow = i == 8 + local relTo = newRow and emotionCheckBoxes[1] or emotionCheckBoxes[i-1] or self.controls.emotionLabel + local emoCtl = emoCheck(emo, relTo, newRow) emotionCheckBoxes[i] = emoCtl self.controls["emotionCheckbox"..emo] = emoCtl end @@ -302,7 +304,8 @@ function NotableDBClass:AddValueTooltip(tooltip, index, node) if node.sd[1] then tooltip:AddLine(16, "") for i, line in ipairs(node.sd) do - if line ~= " " and (node.mods[i].extra or not node.mods[i].list) then + local mod = node.mods and node.mods[i] + if line ~= " " and (not mod or mod.extra or not mod.list) then local line = colorCodes.UNSUPPORTED..line line = main.notSupportedModTooltips and (line .. main.notSupportedTooltipText) or line tooltip:AddLine(16, line) @@ -343,4 +346,4 @@ end ---@param node table function NotableDBClass:OnSelCopy(index, node) Copy(item.dn) -end \ No newline at end of file +end diff --git a/src/Classes/PassiveSpec.lua b/src/Classes/PassiveSpec.lua index e532c4ac48..1442be1859 100644 --- a/src/Classes/PassiveSpec.lua +++ b/src/Classes/PassiveSpec.lua @@ -226,8 +226,10 @@ end function PassiveSpecClass:Save(xml) local allocNodeIdList = { } local weaponSets = {} - for nodeId in pairs(self.allocNodes) do - t_insert(allocNodeIdList, nodeId) + for nodeId, node in pairs(self.allocNodes) do + if not (node.isGrantedPassive and node.isFreeAllocate) then + t_insert(allocNodeIdList, nodeId) + end if self.nodes[nodeId].allocMode and self.nodes[nodeId].allocMode ~= 0 then local weaponSet = self.nodes[nodeId].allocMode if not weaponSets[weaponSet] then @@ -1058,6 +1060,210 @@ function PassiveSpecClass:GetJewel(itemId) return item end +local function normalisePassiveName(name) + return type(name) == "string" and name:lower():gsub("^%s+", ""):gsub("%s+$", "") or nil +end + +function PassiveSpecClass:FindNodesByDisplayName(name, predicate) + local key = normalisePassiveName(name) + local out = { } + if not key then + return out + end + for _, node in pairs(self.nodes) do + local nodeName = normalisePassiveName(node.name or node.dn) + if nodeName == key and (not predicate or predicate(node)) then + t_insert(out, node) + end + end + table.sort(out, function(a, b) return a.id < b.id end) + return out +end + +function PassiveSpecClass:FindNodeByDisplayName(name, predicate) + return self:FindNodesByDisplayName(name, predicate)[1] +end + +local voicesSinisterSocketAliases = { + "voices_jewel_slot1", + "voices_jewel_slot2", + "voices_jewel_slot3__", + "voices_jewel_slot4", + "voices_jewel_slot5", +} + +function PassiveSpecClass:GetVoicesSinisterJewelSocketNodes(count) + local byAlias = { } + for _, node in pairs(self.nodes) do + if node.isJewelSocket and node.name == "Sinister Jewel Socket" and node.aliasPassiveSocket then + byAlias[node.aliasPassiveSocket] = node + end + end + local out = { } + for i = 1, m_min(count or 0, #voicesSinisterSocketAliases) do + local node = byAlias[voicesSinisterSocketAliases[i]] + if node then + t_insert(out, node) + end + end + return out +end + +function PassiveSpecClass:ResolveGrantedPassiveNodes(passive) + local out = { } + if type(passive) ~= "string" then + return out + end + + local notable = self.tree.notableMap[passive] + if notable then + t_insert(out, self.nodes[notable.id] or notable) + return out + end + + local sinisterCount = passive:match("^(%d+)%s+sinister jewel sockets$") + if sinisterCount then + return self:GetVoicesSinisterJewelSocketNodes(tonumber(sinisterCount)) + end + + local node = self:FindNodeByDisplayName(passive, function(node) + return node.isJewelSocket or node.type == "Socket" or node.type == "Notable" or node.type == "Keystone" + end) + if node then + t_insert(out, node) + end + return out +end + +function PassiveSpecClass:IsSinisterJewelSocketNode(node) + if not node then + return false + end + if node.name == "Sinister Jewel Socket" then + return true + end + for _, stat in ipairs(node.stats or node.sd or { }) do + if stat == "Sinister Jewel Socket" then + return true + end + end + return false +end + +local function isPersistentGrantedPassiveNode(node) + return node and (node.isJewelSocket or node.type == "Socket") +end + +local function getItemForGrantedPassiveSlot(spec, itemsTab, slot, allocNodes, override, activeWeaponSet, nodesModsList) + local slotName = slot.slotName + if slot.nodeId then + if not allocNodes[slot.nodeId] then + return + end + if slotName == override.repSlotName then + return override.repItem + end + local itemId = spec.jewels[slot.nodeId] + if (not itemId or itemId == 0) and itemsTab.sockets[slot.nodeId] then + itemId = itemsTab.sockets[slot.nodeId].selItemId + end + return itemsTab.items[itemId] + end + + if slot.weaponSet and slot.weaponSet ~= activeWeaponSet then + return + end + if slotName == "Ring 3" and nodesModsList and not nodesModsList:Flag(nil, "AdditionalRingSlot") then + return + end + if slotName == override.repSlotName then + return override.repItem + end + return itemsTab.items[slot.selItemId] +end + +function PassiveSpecClass:SetGrantedPassiveNodes(grantedNodeMap) + local changed = false + grantedNodeMap = grantedNodeMap or { } + + for nodeId, node in pairs(self.allocNodes) do + if node.isGrantedPassive and node.isFreeAllocate and not grantedNodeMap[nodeId] then + node.alloc = false + node.isGrantedPassive = nil + node.isFreeAllocate = nil + self.allocNodes[nodeId] = nil + changed = true + end + end + + for nodeId, node in pairs(grantedNodeMap) do + local specNode = self.nodes[nodeId] or node + if not self.allocNodes[nodeId] then + specNode.alloc = true + specNode.allocMode = 0 + specNode.isGrantedPassive = true + specNode.isFreeAllocate = true + self.allocNodes[nodeId] = specNode + changed = true + end + end + + if changed then + self:BuildAllDependsAndPaths() + end + return changed +end + +function PassiveSpecClass:CollectGrantedPassiveNodesFromItems(itemsTab, baseAllocNodes, ignoreJewelLimits, override, nodesModsList) + override = override or { } + local granted = { } + local allocNodes = { } + for nodeId, node in pairs(baseAllocNodes or self.allocNodes) do + if not (node.isGrantedPassive and node.isFreeAllocate) then + allocNodes[nodeId] = node + end + end + local activeWeaponSet = itemsTab.activeItemSet.useSecondWeaponSet and 2 or 1 + local jewelLimits = { } + local changed = true + local safety = 0 + + while changed and safety < 8 do + changed = false + safety = safety + 1 + + for _, slot in pairs(itemsTab.orderedSlots) do + local item = getItemForGrantedPassiveSlot(self, itemsTab, slot, allocNodes, override, activeWeaponSet, nodesModsList) + + if item and item.modList and not (slot.nodeId and not itemsTab:IsItemValidForSlot(item, slot.slotName)) then + if slot.nodeId and item.limit and not ignoreJewelLimits then + local limitKey = item.base.subType == "Timeless" and "Historic" or item.title + if jewelLimits[limitKey] and jewelLimits[limitKey] >= item.limit then + goto continue + end + jewelLimits[limitKey] = (jewelLimits[limitKey] or 0) + 1 + end + for _, mod in ipairs(item.modList) do + if mod.name == "GrantedPassive" then + local passive = mod.value + for _, node in ipairs(self:ResolveGrantedPassiveNodes(passive)) do + if isPersistentGrantedPassiveNode(node) and not granted[node.id] then + local specNode = self.nodes[node.id] or node + granted[node.id] = specNode + allocNodes[node.id] = specNode + changed = true + end + end + end + end + end + ::continue:: + end + end + + return granted +end + -- Perform a breadth-first search of the tree, starting from this node, and determine if it is the closest node to any other nodes function PassiveSpecClass:BuildPathFromNode(root) root.pathDist = 0 @@ -1588,7 +1794,10 @@ function PassiveSpecClass:BuildAllDependsAndPaths() for id, node in pairs(self.allocNodes) do node.visited = true node.connectedToStart = false - local anyStartFound = (node.type == "ClassStart" or node.type == "AscendClassStart") + local anyStartFound = (node.type == "ClassStart" or node.type == "AscendClassStart" or node.isFreeAllocate) + if node.isFreeAllocate then + node.connectedToStart = true + end for _, other in ipairs(node.linked) do local otherAlloc = other.alloc or alternateClassStartNodes[other.id] if otherAlloc and self:CanPathThroughAllocMode(node.allocMode or 0, other) and not isValueInArray(node.depends, other) then diff --git a/src/Modules/CalcSetup.lua b/src/Modules/CalcSetup.lua index be72984edd..961d1f42c4 100644 --- a/src/Modules/CalcSetup.lua +++ b/src/Modules/CalcSetup.lua @@ -770,6 +770,11 @@ function calcs.initEnv(build, mode, override, specEnv) nodes = copyTable(env.spec.allocNodes, true) end env.allocNodes = nodes + for nodeId, node in pairs(env.allocNodes) do + if node.isGrantedPassive and node.isFreeAllocate then + env.allocNodes[nodeId] = nil + end + end end local nodesModsList = calcs.buildModListForNodeList(env, env.allocNodes, true, true) @@ -812,6 +817,18 @@ function calcs.initEnv(build, mode, override, specEnv) -- Build and merge item modifiers, and create list of radius jewels if not accelerate.requirementsItems then + local grantedNodes = env.spec:CollectGrantedPassiveNodesFromItems(build.itemsTab, env.allocNodes, env.configInput.ignoreJewelLimits, override, nodesModsList) + if mode == "MAIN" then + if build.spec:SetGrantedPassiveNodes(grantedNodes) then + build.itemsTab:UpdateSockets() + end + end + for nodeId, node in pairs(grantedNodes) do + env.allocNodes[nodeId] = env.spec.nodes[nodeId] or node + env.grantedPassives[nodeId] = true + env.extraRadiusNodeList[nodeId] = nil + end + local items = {} local jewelLimits = {} local giantsBlood = weaponFlagState.giantsBlood @@ -864,6 +881,8 @@ function calcs.initEnv(build, mode, override, specEnv) -- Slot is a jewel socket, check if socket is allocated if not env.allocNodes[slot.nodeId] then goto continue + elseif item and not build.itemsTab:IsItemValidForSlot(item, slot.slotName) then + goto continue elseif item then if item.jewelData then item.jewelData.limitDisabled = nil @@ -1326,11 +1345,12 @@ function calcs.initEnv(build, mode, override, specEnv) -- Add granted passives (e.g., amulet anoints) if not accelerate.nodeAlloc then for _, passive in pairs(env.modDB:List(nil, "GrantedPassive")) do - local node = env.spec.tree.notableMap[passive] - if node and (not override.removeNodes or not override.removeNodes[node.id]) then - env.allocNodes[node.id] = env.spec.nodes[node.id] or node -- use the conquered node data, if available - env.grantedPassives[node.id] = true - env.extraRadiusNodeList[node.id] = nil + for _, node in ipairs(env.spec:ResolveGrantedPassiveNodes(passive)) do + if node and (not override.removeNodes or not override.removeNodes[node.id]) then + env.allocNodes[node.id] = env.spec.nodes[node.id] or node -- use the conquered node data, if available + env.grantedPassives[node.id] = true + env.extraRadiusNodeList[node.id] = nil + end end end end