From 3221797b35e5eac05e2abfe4b5de59bd8bb3ea19 Mon Sep 17 00:00:00 2001 From: Mickael Cagnion Date: Tue, 19 May 2026 10:55:11 +0200 Subject: [PATCH 1/8] Cache targeted radius jewel tooltip comparisons Avoid rebuilding radius-jewel comparison specs for repeated targeted tooltip hovers and skip limited-unique socket outputs that will not be shown. Preserve full multi-slot tooltip behavior when slot-only tooltips are disabled. Related local follow-up to PR 9746. --- spec/System/TestRadiusJewelStatDiff_spec.lua | 98 ++++++++++++++++++++ src/Classes/CompareTab.lua | 8 +- src/Classes/ItemsTab.lua | 68 ++++++++++---- 3 files changed, 155 insertions(+), 19 deletions(-) diff --git a/spec/System/TestRadiusJewelStatDiff_spec.lua b/spec/System/TestRadiusJewelStatDiff_spec.lua index 0fd2fa0389..e226a99793 100644 --- a/spec/System/TestRadiusJewelStatDiff_spec.lua +++ b/spec/System/TestRadiusJewelStatDiff_spec.lua @@ -122,6 +122,35 @@ local function setupAllocatedSocket() return spec, socketNode end +local function setupAllocatedSockets(count) + local spec = build.spec + local sockets = { } + local sortedSockets = { } + for _, node in pairs(spec.nodes) do + if node.isJewelSocket then + sortedSockets[#sortedSockets + 1] = node + end + end + table.sort(sortedSockets, function(a, b) + return a.id < b.id + end) + for _, socketNode in ipairs(sortedSockets) do + if allocatePathToNode(spec, socketNode) then + sockets[#sockets + 1] = socketNode + if #sockets >= count then + break + end + end + end + if #sockets < count then + pending("Could not allocate the requested number of jewel sockets for this tree layout") + return spec, sockets + end + spec:BuildAllDependsAndPaths() + runCallback("OnFrame") + return spec, sockets +end + local function rebuildBuild() build.buildFlag = true runCallback("OnFrame") @@ -600,4 +629,73 @@ describe("TestRadiusJewelStatDiff", function() "tooltip should contain a 'Removing this item' comparison header") end) + it("AddItemTooltip avoids rebuilding unused limited-unique socket comparisons without a target slot", function() + local spec, sockets = setupAllocatedSockets(2) + + local item = newThreadOfHope() + item.limit = 1 + equipJewelInSocket(item, sockets[1]) + spec:BuildAllDependsAndPaths() + runCallback("OnFrame") + + local specClass = getmetatable(spec) + local originalBuildAllDependsAndPaths = specClass.BuildAllDependsAndPaths + local rebuilds = 0 + specClass.BuildAllDependsAndPaths = function(self, ...) + rebuilds = rebuilds + 1 + return originalBuildAllDependsAndPaths(self, ...) + end + + local ok, err = pcall(function() + local tooltip = new("Tooltip") + build.itemsTab:AddItemTooltip(tooltip, item) + end) + specClass.BuildAllDependsAndPaths = originalBuildAllDependsAndPaths + if not ok then + error(err) + end + + assert.are.equals(1, rebuilds, + "limited unique radius jewels should rebuild only the same-unique slot that will be displayed") + end) + + it("AddItemTooltip reuses targeted radius jewel comparison specs until output changes", function() + local spec, sockets = setupAllocatedSockets(2) + + local item = newCustomLeapJewel("Cached Leap") + local slot = equipJewelInSocket(item, sockets[1]) + spec:BuildAllDependsAndPaths() + runCallback("OnFrame") + + local originalSlotOnlyTooltips = main.slotOnlyTooltips + main.slotOnlyTooltips = true + local specClass = getmetatable(spec) + local originalBuildAllDependsAndPaths = specClass.BuildAllDependsAndPaths + local rebuilds = 0 + specClass.BuildAllDependsAndPaths = function(self, ...) + rebuilds = rebuilds + 1 + return originalBuildAllDependsAndPaths(self, ...) + end + + local ok, err = pcall(function() + local tooltip = new("Tooltip") + build.itemsTab:AddItemTooltip(tooltip, item, slot) + tooltip = new("Tooltip") + build.itemsTab:AddItemTooltip(tooltip, item, slot) + assert.are.equals(1, rebuilds, + "targeted radius jewel hover should reuse its cached comparison spec") + + build.outputRevision = build.outputRevision + 1 + tooltip = new("Tooltip") + build.itemsTab:AddItemTooltip(tooltip, item, slot) + assert.are.equals(2, rebuilds, + "targeted radius jewel comparison spec cache should reset when output changes") + end) + specClass.BuildAllDependsAndPaths = originalBuildAllDependsAndPaths + main.slotOnlyTooltips = originalSlotOnlyTooltips + if not ok then + error(err) + end + end) + end) diff --git a/src/Classes/CompareTab.lua b/src/Classes/CompareTab.lua index bcd470eab7..52fcd9d078 100644 --- a/src/Classes/CompareTab.lua +++ b/src/Classes/CompareTab.lua @@ -3801,6 +3801,7 @@ function CompareTabClass:DrawItems(vp, compareEntry, inputEvents) local hoverX, hoverY = 0, 0 local hoverW, hoverH = 0, 0 local hoverItemsTab = nil + local hoverSlotName = nil -- Track item copy button clicks local clickedCopySlot = nil @@ -3902,6 +3903,7 @@ function CompareTabClass:DrawItems(vp, compareEntry, inputEvents) if rowHoverItem then hoverItem = rowHoverItem hoverItemsTab = rowHoverItemsTab + hoverSlotName = pHover and equipSlotName or cHover and copySlotName or nil hoverX, hoverY = rowHoverX, rowHoverY hoverW, hoverH = rowHoverW, rowHoverH end @@ -3984,8 +3986,10 @@ function CompareTabClass:DrawItems(vp, compareEntry, inputEvents) SetViewport() local maxTooltipWidth = m_min(600, m_max(260, vp.width - 24)) if not main.popups[1] and hoverItem and hoverItemsTab then - self.itemTooltip:Clear() - hoverItemsTab:AddItemTooltip(self.itemTooltip, hoverItem, nil, nil, maxTooltipWidth) + local hoverBuild = hoverItemsTab.build + if self.itemTooltip:CheckForUpdate(hoverItemsTab, hoverItem, hoverSlotName, maxTooltipWidth, main.slotOnlyTooltips, launch.devModeAlt, hoverBuild and hoverBuild.outputRevision) then + hoverItemsTab:AddItemTooltip(self.itemTooltip, hoverItem, hoverSlotName, nil, maxTooltipWidth) + end SetDrawLayer(nil, 100) self.itemTooltip:Draw(vp.x + hoverX, vp.y + checkboxOffset + hoverY, hoverW, hoverH, vp) SetDrawLayer(nil, 0) diff --git a/src/Classes/ItemsTab.lua b/src/Classes/ItemsTab.lua index 0b3852aba5..255a95a043 100644 --- a/src/Classes/ItemsTab.lua +++ b/src/Classes/ItemsTab.lua @@ -4153,12 +4153,34 @@ end ---@param itemsTab ItemsTab ---@param compareSlot ItemSlotControl ---@param replacementItem Item -local function buildSpecForJewelComparison(itemsTab, compareSlot, replacementItem) +---@param useCache boolean? +local function buildSpecForJewelComparison(itemsTab, compareSlot, replacementItem, useCache) local tempItemId + local replacementItemId = replacementItem and replacementItem.id + local replacementItemIsStored = replacementItemId and itemsTab.items[replacementItemId] == replacementItem + local canCache = useCache and (not replacementItem or replacementItemIsStored) + local cacheKey + local cache + if canCache then + local outputRevision = itemsTab.build and itemsTab.build.outputRevision or 0 + cache = itemsTab.targetedJewelComparisonSpecCache + if not cache or cache.outputRevision ~= outputRevision then + cache = { + outputRevision = outputRevision, + specs = { }, + } + itemsTab.targetedJewelComparisonSpecCache = cache + end + cacheKey = tostring(compareSlot.nodeId) .. ":" .. tostring(replacementItemId or "") + if cache.specs[cacheKey] then + return cache.specs[cacheKey] + end + end + local spec = cloneSpecForJewelComparison(itemsTab.build.spec) if replacementItem then - if replacementItem.id and itemsTab.items[replacementItem.id] == replacementItem then - spec.jewels[compareSlot.nodeId] = replacementItem.id + if replacementItemIsStored then + spec.jewels[compareSlot.nodeId] = replacementItemId else tempItemId = -1 while itemsTab.items[tempItemId] do @@ -4179,6 +4201,9 @@ local function buildSpecForJewelComparison(itemsTab, compareSlot, replacementIte if not ok then error(err, 0) end + if cacheKey then + cache.specs[cacheKey] = spec + end return spec end @@ -4917,18 +4942,18 @@ function ItemsTabClass:AddItemStatDifferences(tooltip, item, base, slot) end end - local function getReplacedItemAndOutput(compareSlot) - local selItem = self.items[compareSlot.selItemId] + local function getReplacedItemAndOutput(compareSlot, selItem, useJewelComparisonSpecCache) + selItem = selItem or self.items[compareSlot.selItemId] local override = { repSlotName = compareSlot.slotName, repItem = item ~= selItem and item or nil } if compareSlot.nodeId and (itemChangesPassiveTree(selItem) or itemChangesPassiveTree(item)) then - override.spec = buildSpecForJewelComparison(self, compareSlot, override.repItem) + override.spec = buildSpecForJewelComparison(self, compareSlot, override.repItem, useJewelComparisonSpecCache) end local output = calcFunc(override) return selItem, output end - local function addCompareForSlot(compareSlot, selItem, output) + local function addCompareForSlot(compareSlot, selItem, output, useJewelComparisonSpecCache) if not selItem or not output then - selItem, output = getReplacedItemAndOutput(compareSlot) + selItem, output = getReplacedItemAndOutput(compareSlot, nil, useJewelComparisonSpecCache) end local header if item == selItem then @@ -4941,29 +4966,38 @@ function ItemsTabClass:AddItemStatDifferences(tooltip, item, base, slot) -- if we have a specific slot to compare to, and the user has "Show -- tooltips only for affected slots" checked, we can just compare that - -- one slot + -- one slot. + local compareOnlySlot = type(slot) ~= "string" and slot or self.slots[slot] if main.slotOnlyTooltips and slot then - slot = type(slot) ~= "string" and slot or self.slots[slot] - if slot then addCompareForSlot(slot) end + if compareOnlySlot then addCompareForSlot(compareOnlySlot, nil, nil, true) end return end - - local slots = {} local isUnique = item.rarity == "UNIQUE" or item.rarity == "RELIC" local currentSameUniqueCount = 0 + local slotCandidates = {} for _, compareSlot in ipairs(compareSlots) do - local selItem, output = getReplacedItemAndOutput(compareSlot) + local selItem = self.items[compareSlot.selItemId] local isSameUnique = isUnique and selItem and item.name == selItem.name if isUnique and isSameUnique and item.limit then currentSameUniqueCount = currentSameUniqueCount + 1 end - table.insert(slots, - { selItem = selItem, output = output, compareSlot = compareSlot, isSameUnique = isSameUnique }) + table.insert(slotCandidates, + { selItem = selItem, compareSlot = compareSlot, isSameUnique = isSameUnique }) + end + local isLimitedUniqueAtLimit = (isUnique and item.limit and currentSameUniqueCount == item.limit) or false + + local slots = {} + for _, slotEntry in ipairs(slotCandidates) do + if not isLimitedUniqueAtLimit or slotEntry.isSameUnique then + local _, output = getReplacedItemAndOutput(slotEntry.compareSlot, slotEntry.selItem) + slotEntry.output = output + table.insert(slots, slotEntry) + end end -- limited uniques: only compare to slots with the same item if more don't fit - if currentSameUniqueCount == item.limit then + if isLimitedUniqueAtLimit then for _, slotEntry in ipairs(slots) do if slotEntry.isSameUnique then addCompareForSlot(slotEntry.compareSlot, slotEntry.selItem, slotEntry.output) From 9ed7d2d37638d028540b40d232cce40ee1c92b32 Mon Sep 17 00:00:00 2001 From: Mickael Cagnion Date: Thu, 21 May 2026 13:53:49 +0200 Subject: [PATCH 2/8] Skip UI path rebuilds for jewel tooltip specs Tooltip comparison specs only need calc state, but radius jewel comparisons were still rebuilding passive tree UI paths for every temporary spec. Skip that path rebuild while preserving socket distance recomputation for Split Personality-style jewel scaling. --- spec/System/TestRadiusJewelStatDiff_spec.lua | 62 ++++++++++++ src/Classes/ItemsTab.lua | 5 +- src/Classes/PassiveSpec.lua | 99 +++++++++++--------- 3 files changed, 119 insertions(+), 47 deletions(-) diff --git a/spec/System/TestRadiusJewelStatDiff_spec.lua b/spec/System/TestRadiusJewelStatDiff_spec.lua index e226a99793..5ba7921b52 100644 --- a/spec/System/TestRadiusJewelStatDiff_spec.lua +++ b/spec/System/TestRadiusJewelStatDiff_spec.lua @@ -201,6 +201,15 @@ local function newPlainJewel() "Implicits: 0\n") end +local function newSplitPersonality() + return new("Item", "Rarity: UNIQUE\n" .. + "Split Personality\n" .. + "Crimson Jewel\n" .. + "Implicits: 0\n" .. + "+5 to Strength\n" .. + "This Jewel's Socket has 25% increased effect per Allocated Passive Skill between it and your Class' starting location\n") +end + -- Helper: minimal Impossible Escape item. Uses "Radius: Small" and targets -- a specific keystone. The parser populates both impossibleEscapeKeystone -- and impossibleEscapeKeystones from the "in Radius of X" mod. @@ -698,4 +707,57 @@ describe("TestRadiusJewelStatDiff", function() end end) + it("AddItemTooltip skips UI path rebuilds for temporary radius jewel specs", function() + local spec, sockets = setupAllocatedSockets(2) + + local radiusItem = newThreadOfHope() + local radiusSlot = equipJewelInSocket(radiusItem, sockets[1]) + local splitItem = newSplitPersonality() + equipJewelInSocket(splitItem, sockets[2]) + spec:BuildAllDependsAndPaths() + runCallback("OnFrame") + + assert.is_true((spec.nodes[sockets[2].id].distanceToClassStart or 0) > 0, + "Split Personality socket should have a class-start distance in the base spec") + + local originalSlotOnlyTooltips = main.slotOnlyTooltips + main.slotOnlyTooltips = true + local specClass = getmetatable(spec) + local originalBuildAllDependsAndPaths = specClass.BuildAllDependsAndPaths + local originalSetNodeDistanceToClassStart = specClass.SetNodeDistanceToClassStart + local calculationOnlySpec + local distanceCalls = 0 + specClass.BuildAllDependsAndPaths = function(self, skipNodePathRebuild, ...) + local result = originalBuildAllDependsAndPaths(self, skipNodePathRebuild, ...) + if skipNodePathRebuild then + calculationOnlySpec = self + end + return result + end + specClass.SetNodeDistanceToClassStart = function(self, ...) + distanceCalls = distanceCalls + 1 + return originalSetNodeDistanceToClassStart(self, ...) + end + + local ok, err = pcall(function() + local tooltip = new("Tooltip") + build.itemsTab:AddItemTooltip(tooltip, radiusItem, radiusSlot) + end) + specClass.BuildAllDependsAndPaths = originalBuildAllDependsAndPaths + specClass.SetNodeDistanceToClassStart = originalSetNodeDistanceToClassStart + main.slotOnlyTooltips = originalSlotOnlyTooltips + if not ok then + error(err) + end + + assert.is_truthy(calculationOnlySpec, + "temporary tooltip specs should use the calculation-only path") + for _, node in pairs(calculationOnlySpec.nodes) do + assert.is_nil(node.path, "calculation-only tooltip specs should not retain UI node paths") + assert.is_nil(node.pathDist, "calculation-only tooltip specs should not retain UI path distances") + end + assert.is_true(distanceCalls > 0, + "temporary tooltip specs should still refresh jewel socket distances used by calc") + end) + end) diff --git a/src/Classes/ItemsTab.lua b/src/Classes/ItemsTab.lua index 255a95a043..00496cefde 100644 --- a/src/Classes/ItemsTab.lua +++ b/src/Classes/ItemsTab.lua @@ -4110,7 +4110,7 @@ local function cloneSpecForJewelComparison(spec) local nodeCopy = setmetatable({ }, getmetatable(node)) for key, value in pairs(node) do if key ~= "linked" and key ~= "depends" and key ~= "intuitiveLeapLikesAffecting" - and key ~= "path" and key ~= "power" then + and key ~= "path" and key ~= "pathDist" and key ~= "distanceToClassStart" and key ~= "power" then nodeCopy[key] = value end end @@ -4193,7 +4193,8 @@ local function buildSpecForJewelComparison(itemsTab, compareSlot, replacementIte spec.jewels[compareSlot.nodeId] = nil end local ok, err = xpcall(function() - spec:BuildAllDependsAndPaths() + -- Tooltip comparison specs only need calc state; node paths are UI data. + spec:BuildAllDependsAndPaths(true) end, debug.traceback) if tempItemId then itemsTab.items[tempItemId] = nil diff --git a/src/Classes/PassiveSpec.lua b/src/Classes/PassiveSpec.lua index 3e64512407..0e30c77369 100644 --- a/src/Classes/PassiveSpec.lua +++ b/src/Classes/PassiveSpec.lua @@ -1089,7 +1089,7 @@ function PassiveSpecClass:NodesInIntuitiveLeapLikeRadius(node) end -- Rebuilds dependencies and paths for all nodes -function PassiveSpecClass:BuildAllDependsAndPaths() +function PassiveSpecClass:BuildAllDependsAndPaths(skipNodePathRebuild) local timelessJewelTypeByConqueror = { vaal = 1, karui = 2, @@ -1625,8 +1625,13 @@ function PassiveSpecClass:BuildAllDependsAndPaths() -- Reset and rebuild all node paths for _, node in pairs(self.nodes) do - node.pathDist = (node.alloc and #node.intuitiveLeapLikesAffecting == 0) and 0 or 1000 - node.path = nil + if skipNodePathRebuild then + node.pathDist = nil + node.path = nil + else + node.pathDist = (node.alloc and #node.intuitiveLeapLikesAffecting == 0) and 0 or 1000 + node.path = nil + end if node.isJewelSocket or node.expansionJewel then node.distanceToClassStart = 0 end @@ -1638,49 +1643,51 @@ function PassiveSpecClass:BuildAllDependsAndPaths() end end - -- Use a multi-source 0-1 BFS to find the closest allocated node. Allocated - -- nodes have zero weight, while each unallocated node costs one passive point. - local queue = { } - for _, node in ipairs(rootList) do - node.pathDist = 0 - node.path = wipeTable(node.path) - t_insert(queue, node) - end - local queueStart = 1 - local queueLength = #queue - while queueStart <= queueLength do - local node = queue[queueStart] - queueStart = queueStart + 1 - local linked = node.linked - local nodeDist = node.pathDist - local nodePath = node.path - for i = 1, #linked do - local other = linked[i] - local weight = other.alloc and 0 or 1 - local distViaNode = nodeDist + weight - -- Paths cannot pass through start nodes, cross ascendancies, or move - -- away from masteries. Ascendant paths may leave at distance zero. - local canTraverse = node.type ~= "Mastery" - and other.type ~= "ClassStart" - and other.type ~= "AscendClassStart" - and (node.ascendancyName == other.ascendancyName or (nodeDist == 0 and not other.ascendancyName)) - if distViaNode < (other.pathDist or math.huge) and canTraverse then - if weight == 0 then - -- Free nodes go to the front so they can shorten paid paths immediately. - queueStart = queueStart - 1 - queue[queueStart] = other - else - queueLength = queueLength + 1 - queue[queueLength] = other - end + if not skipNodePathRebuild then + -- Use a multi-source 0-1 BFS to find the closest allocated node. Allocated + -- nodes have zero weight, while each unallocated node costs one passive point. + local queue = { } + for _, node in ipairs(rootList) do + node.pathDist = 0 + node.path = wipeTable(node.path) + t_insert(queue, node) + end + local queueStart = 1 + local queueLength = #queue + while queueStart <= queueLength do + local node = queue[queueStart] + queueStart = queueStart + 1 + local linked = node.linked + local nodeDist = node.pathDist + local nodePath = node.path + for i = 1, #linked do + local other = linked[i] + local weight = other.alloc and 0 or 1 + local distViaNode = nodeDist + weight + -- Paths cannot pass through start nodes, cross ascendancies, or move + -- away from masteries. Ascendant paths may leave at distance zero. + local canTraverse = node.type ~= "Mastery" + and other.type ~= "ClassStart" + and other.type ~= "AscendClassStart" + and (node.ascendancyName == other.ascendancyName or (nodeDist == 0 and not other.ascendancyName)) + if distViaNode < (other.pathDist or math.huge) and canTraverse then + if weight == 0 then + -- Free nodes go to the front so they can shorten paid paths immediately. + queueStart = queueStart - 1 + queue[queueStart] = other + else + queueLength = queueLength + 1 + queue[queueLength] = other + end - other.pathDist = distViaNode - local path = wipeTable(other.path) - path[1] = other - for pathIndex = 1, #nodePath do - path[pathIndex + 1] = nodePath[pathIndex] + other.pathDist = distViaNode + local path = wipeTable(other.path) + path[1] = other + for pathIndex = 1, #nodePath do + path[pathIndex + 1] = nodePath[pathIndex] + end + other.path = path end - other.path = path end end end @@ -1691,7 +1698,9 @@ function PassiveSpecClass:BuildAllDependsAndPaths() end end - self:BuildSplitPersonalityPath() + if not skipNodePathRebuild then + self:BuildSplitPersonalityPath() + end end function PassiveSpecClass:ReplaceNode(old, newNode) From 3635584404fad60ea067372c28ccec58fba94f42 Mon Sep 17 00:00:00 2001 From: Mickael Cagnion Date: Thu, 21 May 2026 14:36:27 +0200 Subject: [PATCH 3/8] Cache radius jewel tooltip outputs Reuse full radius jewel comparison outputs while the build output revision is unchanged. This reduces repeated Compare-tab hover work for slotOnlyTooltips=OFF without caching cloned specs or changing comparison behavior. --- spec/System/TestRadiusJewelStatDiff_spec.lua | 50 ++++++++++++++++++++ src/Classes/ItemsTab.lua | 49 ++++++++++++++++++- 2 files changed, 97 insertions(+), 2 deletions(-) diff --git a/spec/System/TestRadiusJewelStatDiff_spec.lua b/spec/System/TestRadiusJewelStatDiff_spec.lua index 5ba7921b52..49dac4dd53 100644 --- a/spec/System/TestRadiusJewelStatDiff_spec.lua +++ b/spec/System/TestRadiusJewelStatDiff_spec.lua @@ -760,4 +760,54 @@ describe("TestRadiusJewelStatDiff", function() "temporary tooltip specs should still refresh jewel socket distances used by calc") end) + it("AddItemTooltip reuses full radius jewel comparison outputs until output changes", function() + local spec, sockets = setupAllocatedSockets(2) + + local item = newCustomLeapJewel("Cached Full Leap") + local slot = equipJewelInSocket(item, sockets[1]) + spec:BuildAllDependsAndPaths() + runCallback("OnFrame") + + local originalSlotOnlyTooltips = main.slotOnlyTooltips + main.slotOnlyTooltips = false + build.itemsTab.jewelComparisonOutputCache = nil + build.itemsTab.targetedJewelComparisonSpecCache = nil + + local originalGetMiscCalculator = build.calcsTab.GetMiscCalculator + local calcCalls = 0 + build.calcsTab.GetMiscCalculator = function(self, ...) + local calcFunc, calcBase = originalGetMiscCalculator(self, ...) + return function(...) + calcCalls = calcCalls + 1 + return calcFunc(...) + end, calcBase + end + + local ok, err = pcall(function() + local tooltip = new("Tooltip") + build.itemsTab:AddItemTooltip(tooltip, item, slot) + local firstPassCalcCalls = calcCalls + assert.is_true(firstPassCalcCalls > 0, + "full radius jewel tooltip should calculate outputs on first pass") + + tooltip = new("Tooltip") + build.itemsTab:AddItemTooltip(tooltip, item, slot) + local secondPassCalcCalls = calcCalls - firstPassCalcCalls + assert.is_true(secondPassCalcCalls < firstPassCalcCalls, + "full radius jewel tooltip should reuse cached radius outputs on second pass") + + build.outputRevision = build.outputRevision + 1 + local beforeInvalidationCalcCalls = calcCalls + tooltip = new("Tooltip") + build.itemsTab:AddItemTooltip(tooltip, item, slot) + assert.is_true(calcCalls - beforeInvalidationCalcCalls > secondPassCalcCalls, + "full radius jewel output cache should reset when output changes") + end) + build.calcsTab.GetMiscCalculator = originalGetMiscCalculator + main.slotOnlyTooltips = originalSlotOnlyTooltips + if not ok then + error(err) + end + end) + end) diff --git a/src/Classes/ItemsTab.lua b/src/Classes/ItemsTab.lua index 00496cefde..5b2a5c71f2 100644 --- a/src/Classes/ItemsTab.lua +++ b/src/Classes/ItemsTab.lua @@ -4074,6 +4074,37 @@ local function itemChangesPassiveTree(item) and (item.jewelData.intuitiveLeapLike or item.jewelData.impossibleEscapeKeystone))) end +local function getStoredItemId(itemsTab, item) + if not item then + return "" + end + local itemId = item.id + if itemId and itemsTab.items[itemId] == item then + return tostring(itemId) + end +end + +local function getJewelComparisonOutputCache(itemsTab) + local outputRevision = itemsTab.build and itemsTab.build.outputRevision or 0 + local cache = itemsTab.jewelComparisonOutputCache + if not cache or cache.outputRevision ~= outputRevision then + cache = { + outputRevision = outputRevision, + outputs = { }, + } + itemsTab.jewelComparisonOutputCache = cache + end + return cache +end + +local function getJewelComparisonOutputCacheKey(itemsTab, compareSlot, replacementItem) + local replacementItemId = getStoredItemId(itemsTab, replacementItem) + if not replacementItemId then + return + end + return tostring(compareSlot.slotName) .. ":" .. tostring(compareSlot.nodeId or "") .. ":" .. tostring(compareSlot.selItemId or "") .. ":" .. replacementItemId +end + -- These jewels can replace passive nodes or disconnect allocated passives, so -- rebuild the passive tree before comparing their stats. -- Keep this list in sync with PassiveSpec's constructor, Init, and Select* @@ -4943,13 +4974,27 @@ function ItemsTabClass:AddItemStatDifferences(tooltip, item, base, slot) end end - local function getReplacedItemAndOutput(compareSlot, selItem, useJewelComparisonSpecCache) + local function getReplacedItemAndOutput(compareSlot, selItem, useJewelComparisonSpecCache, useJewelComparisonOutputCache) selItem = selItem or self.items[compareSlot.selItemId] local override = { repSlotName = compareSlot.slotName, repItem = item ~= selItem and item or nil } + local outputCache + local outputCacheKey if compareSlot.nodeId and (itemChangesPassiveTree(selItem) or itemChangesPassiveTree(item)) then + if useJewelComparisonOutputCache then + outputCacheKey = getJewelComparisonOutputCacheKey(self, compareSlot, override.repItem) + if outputCacheKey then + outputCache = getJewelComparisonOutputCache(self) + if outputCache.outputs[outputCacheKey] then + return selItem, outputCache.outputs[outputCacheKey] + end + end + end override.spec = buildSpecForJewelComparison(self, compareSlot, override.repItem, useJewelComparisonSpecCache) end local output = calcFunc(override) + if outputCacheKey then + outputCache.outputs[outputCacheKey] = output + end return selItem, output end local function addCompareForSlot(compareSlot, selItem, output, useJewelComparisonSpecCache) @@ -4991,7 +5036,7 @@ function ItemsTabClass:AddItemStatDifferences(tooltip, item, base, slot) local slots = {} for _, slotEntry in ipairs(slotCandidates) do if not isLimitedUniqueAtLimit or slotEntry.isSameUnique then - local _, output = getReplacedItemAndOutput(slotEntry.compareSlot, slotEntry.selItem) + local _, output = getReplacedItemAndOutput(slotEntry.compareSlot, slotEntry.selItem, nil, true) slotEntry.output = output table.insert(slots, slotEntry) end From de05c5916598ec6acce7922509e3d1a7563e4a4f Mon Sep 17 00:00:00 2001 From: Mickael Cagnion Date: Mon, 17 Aug 2026 19:34:26 +0200 Subject: [PATCH 4/8] Update radius tooltip specs for explicit constructors Keep the rebased regression tests compatible with the current class constructor API. --- spec/System/TestRadiusJewelStatDiff_spec.lua | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/spec/System/TestRadiusJewelStatDiff_spec.lua b/spec/System/TestRadiusJewelStatDiff_spec.lua index 49dac4dd53..e95da9a0db 100644 --- a/spec/System/TestRadiusJewelStatDiff_spec.lua +++ b/spec/System/TestRadiusJewelStatDiff_spec.lua @@ -202,7 +202,7 @@ local function newPlainJewel() end local function newSplitPersonality() - return new("Item", "Rarity: UNIQUE\n" .. + return new("Item"):Item("Rarity: UNIQUE\n" .. "Split Personality\n" .. "Crimson Jewel\n" .. "Implicits: 0\n" .. @@ -656,7 +656,7 @@ describe("TestRadiusJewelStatDiff", function() end local ok, err = pcall(function() - local tooltip = new("Tooltip") + local tooltip = new("Tooltip"):Tooltip() build.itemsTab:AddItemTooltip(tooltip, item) end) specClass.BuildAllDependsAndPaths = originalBuildAllDependsAndPaths @@ -687,15 +687,15 @@ describe("TestRadiusJewelStatDiff", function() end local ok, err = pcall(function() - local tooltip = new("Tooltip") + local tooltip = new("Tooltip"):Tooltip() build.itemsTab:AddItemTooltip(tooltip, item, slot) - tooltip = new("Tooltip") + tooltip = new("Tooltip"):Tooltip() build.itemsTab:AddItemTooltip(tooltip, item, slot) assert.are.equals(1, rebuilds, "targeted radius jewel hover should reuse its cached comparison spec") build.outputRevision = build.outputRevision + 1 - tooltip = new("Tooltip") + tooltip = new("Tooltip"):Tooltip() build.itemsTab:AddItemTooltip(tooltip, item, slot) assert.are.equals(2, rebuilds, "targeted radius jewel comparison spec cache should reset when output changes") @@ -740,7 +740,7 @@ describe("TestRadiusJewelStatDiff", function() end local ok, err = pcall(function() - local tooltip = new("Tooltip") + local tooltip = new("Tooltip"):Tooltip() build.itemsTab:AddItemTooltip(tooltip, radiusItem, radiusSlot) end) specClass.BuildAllDependsAndPaths = originalBuildAllDependsAndPaths @@ -784,13 +784,13 @@ describe("TestRadiusJewelStatDiff", function() end local ok, err = pcall(function() - local tooltip = new("Tooltip") + local tooltip = new("Tooltip"):Tooltip() build.itemsTab:AddItemTooltip(tooltip, item, slot) local firstPassCalcCalls = calcCalls assert.is_true(firstPassCalcCalls > 0, "full radius jewel tooltip should calculate outputs on first pass") - tooltip = new("Tooltip") + tooltip = new("Tooltip"):Tooltip() build.itemsTab:AddItemTooltip(tooltip, item, slot) local secondPassCalcCalls = calcCalls - firstPassCalcCalls assert.is_true(secondPassCalcCalls < firstPassCalcCalls, @@ -798,7 +798,7 @@ describe("TestRadiusJewelStatDiff", function() build.outputRevision = build.outputRevision + 1 local beforeInvalidationCalcCalls = calcCalls - tooltip = new("Tooltip") + tooltip = new("Tooltip"):Tooltip() build.itemsTab:AddItemTooltip(tooltip, item, slot) assert.is_true(calcCalls - beforeInvalidationCalcCalls > secondPassCalcCalls, "full radius jewel output cache should reset when output changes") From 67ab3680a5584b74f3220062c6393ef308fe5bd7 Mon Sep 17 00:00:00 2001 From: Mickael Cagnion Date: Fri, 21 Aug 2026 22:55:00 +0200 Subject: [PATCH 5/8] Fix Compare item tooltip cache invalidation Include Shift in the tooltip update key and clear cached update parameters before drawing the Equip comparison tooltip. --- src/Classes/CompareTab.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Classes/CompareTab.lua b/src/Classes/CompareTab.lua index 52fcd9d078..4e87c36b06 100644 --- a/src/Classes/CompareTab.lua +++ b/src/Classes/CompareTab.lua @@ -3987,7 +3987,7 @@ function CompareTabClass:DrawItems(vp, compareEntry, inputEvents) local maxTooltipWidth = m_min(600, m_max(260, vp.width - 24)) if not main.popups[1] and hoverItem and hoverItemsTab then local hoverBuild = hoverItemsTab.build - if self.itemTooltip:CheckForUpdate(hoverItemsTab, hoverItem, hoverSlotName, maxTooltipWidth, main.slotOnlyTooltips, launch.devModeAlt, hoverBuild and hoverBuild.outputRevision) then + if self.itemTooltip:CheckForUpdate(hoverItemsTab, hoverItem, hoverSlotName, maxTooltipWidth, main.slotOnlyTooltips, launch.devModeAlt, IsKeyDown("SHIFT"), hoverBuild and hoverBuild.outputRevision) then hoverItemsTab:AddItemTooltip(self.itemTooltip, hoverItem, hoverSlotName, nil, maxTooltipWidth) end SetDrawLayer(nil, 100) @@ -3997,7 +3997,7 @@ function CompareTabClass:DrawItems(vp, compareEntry, inputEvents) -- Draw stat comparison tooltip when hovering Equip button if not main.popups[1] and hoverEquipItem and hoverEquipSlotName and not hoverItem then - self.itemTooltip:Clear() + self.itemTooltip:Clear(true) self.itemTooltip.maxWidth = maxTooltipWidth local calcFunc, calcBase = self.calcs.getMiscCalculator(self.primaryBuild) if calcFunc then From 88327e74de51d2473b88764e093f827b8c6e087b Mon Sep 17 00:00:00 2001 From: Mickael Cagnion Date: Fri, 21 Aug 2026 23:48:32 +0200 Subject: [PATCH 6/8] Verify cached radius tooltip content Compare the first-pass tooltip text with the warm-cache result so the output-cache test covers semantic equivalence as well as reduced calculator calls. --- spec/System/TestRadiusJewelStatDiff_spec.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/System/TestRadiusJewelStatDiff_spec.lua b/spec/System/TestRadiusJewelStatDiff_spec.lua index e95da9a0db..ac93333fde 100644 --- a/spec/System/TestRadiusJewelStatDiff_spec.lua +++ b/spec/System/TestRadiusJewelStatDiff_spec.lua @@ -787,11 +787,14 @@ describe("TestRadiusJewelStatDiff", function() local tooltip = new("Tooltip"):Tooltip() build.itemsTab:AddItemTooltip(tooltip, item, slot) local firstPassCalcCalls = calcCalls + local firstPassTooltipText = tooltipText(tooltip) assert.is_true(firstPassCalcCalls > 0, "full radius jewel tooltip should calculate outputs on first pass") tooltip = new("Tooltip"):Tooltip() build.itemsTab:AddItemTooltip(tooltip, item, slot) + assert.are.equals(firstPassTooltipText, tooltipText(tooltip), + "cached radius outputs should preserve tooltip content") local secondPassCalcCalls = calcCalls - firstPassCalcCalls assert.is_true(secondPassCalcCalls < firstPassCalcCalls, "full radius jewel tooltip should reuse cached radius outputs on second pass") From be3d6a466565d7ea1279e600d3185783cd098b2d Mon Sep 17 00:00:00 2001 From: Mickael Cagnion Date: Sat, 22 Aug 2026 00:15:50 +0200 Subject: [PATCH 7/8] Clarify radius tooltip cache contracts Use the established slot-only vocabulary and document the cache-key sentinels and selective passive-path rebuild contract found during cumulative naming review. --- spec/System/TestRadiusJewelStatDiff_spec.lua | 8 +++---- src/Classes/ItemsTab.lua | 24 ++++++++++++-------- src/Classes/PassiveSpec.lua | 5 +++- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/spec/System/TestRadiusJewelStatDiff_spec.lua b/spec/System/TestRadiusJewelStatDiff_spec.lua index ac93333fde..89d9e26f1a 100644 --- a/spec/System/TestRadiusJewelStatDiff_spec.lua +++ b/spec/System/TestRadiusJewelStatDiff_spec.lua @@ -668,7 +668,7 @@ describe("TestRadiusJewelStatDiff", function() "limited unique radius jewels should rebuild only the same-unique slot that will be displayed") end) - it("AddItemTooltip reuses targeted radius jewel comparison specs until output changes", function() + it("AddItemTooltip reuses slot-only radius jewel comparison specs until output changes", function() local spec, sockets = setupAllocatedSockets(2) local item = newCustomLeapJewel("Cached Leap") @@ -692,13 +692,13 @@ describe("TestRadiusJewelStatDiff", function() tooltip = new("Tooltip"):Tooltip() build.itemsTab:AddItemTooltip(tooltip, item, slot) assert.are.equals(1, rebuilds, - "targeted radius jewel hover should reuse its cached comparison spec") + "slot-only radius jewel hover should reuse its cached comparison spec") build.outputRevision = build.outputRevision + 1 tooltip = new("Tooltip"):Tooltip() build.itemsTab:AddItemTooltip(tooltip, item, slot) assert.are.equals(2, rebuilds, - "targeted radius jewel comparison spec cache should reset when output changes") + "slot-only radius jewel comparison spec cache should reset when output changes") end) specClass.BuildAllDependsAndPaths = originalBuildAllDependsAndPaths main.slotOnlyTooltips = originalSlotOnlyTooltips @@ -771,7 +771,7 @@ describe("TestRadiusJewelStatDiff", function() local originalSlotOnlyTooltips = main.slotOnlyTooltips main.slotOnlyTooltips = false build.itemsTab.jewelComparisonOutputCache = nil - build.itemsTab.targetedJewelComparisonSpecCache = nil + build.itemsTab.slotOnlyJewelComparisonSpecCache = nil local originalGetMiscCalculator = build.calcsTab.GetMiscCalculator local calcCalls = 0 diff --git a/src/Classes/ItemsTab.lua b/src/Classes/ItemsTab.lua index 5b2a5c71f2..29d312b3d8 100644 --- a/src/Classes/ItemsTab.lua +++ b/src/Classes/ItemsTab.lua @@ -4074,7 +4074,9 @@ local function itemChangesPassiveTree(item) and (item.jewelData.intuitiveLeapLike or item.jewelData.impossibleEscapeKeystone))) end -local function getStoredItemId(itemsTab, item) +-- An empty key part represents no replacement; replacement items without a stored ID +-- return nil so their output is not cached. +local function getJewelComparisonItemCacheKeyPart(itemsTab, item) if not item then return "" end @@ -4098,11 +4100,11 @@ local function getJewelComparisonOutputCache(itemsTab) end local function getJewelComparisonOutputCacheKey(itemsTab, compareSlot, replacementItem) - local replacementItemId = getStoredItemId(itemsTab, replacementItem) - if not replacementItemId then + local replacementItemKeyPart = getJewelComparisonItemCacheKeyPart(itemsTab, replacementItem) + if not replacementItemKeyPart then return end - return tostring(compareSlot.slotName) .. ":" .. tostring(compareSlot.nodeId or "") .. ":" .. tostring(compareSlot.selItemId or "") .. ":" .. replacementItemId + return tostring(compareSlot.slotName) .. ":" .. tostring(compareSlot.nodeId or "") .. ":" .. tostring(compareSlot.selItemId or "") .. ":" .. replacementItemKeyPart end -- These jewels can replace passive nodes or disconnect allocated passives, so @@ -4184,23 +4186,23 @@ end ---@param itemsTab ItemsTab ---@param compareSlot ItemSlotControl ---@param replacementItem Item ----@param useCache boolean? -local function buildSpecForJewelComparison(itemsTab, compareSlot, replacementItem, useCache) +---@param useJewelComparisonSpecCache boolean? +local function buildSpecForJewelComparison(itemsTab, compareSlot, replacementItem, useJewelComparisonSpecCache) local tempItemId local replacementItemId = replacementItem and replacementItem.id local replacementItemIsStored = replacementItemId and itemsTab.items[replacementItemId] == replacementItem - local canCache = useCache and (not replacementItem or replacementItemIsStored) + local canCache = useJewelComparisonSpecCache and (not replacementItem or replacementItemIsStored) local cacheKey local cache if canCache then local outputRevision = itemsTab.build and itemsTab.build.outputRevision or 0 - cache = itemsTab.targetedJewelComparisonSpecCache + cache = itemsTab.slotOnlyJewelComparisonSpecCache if not cache or cache.outputRevision ~= outputRevision then cache = { outputRevision = outputRevision, specs = { }, } - itemsTab.targetedJewelComparisonSpecCache = cache + itemsTab.slotOnlyJewelComparisonSpecCache = cache end cacheKey = tostring(compareSlot.nodeId) .. ":" .. tostring(replacementItemId or "") if cache.specs[cacheKey] then @@ -4224,7 +4226,9 @@ local function buildSpecForJewelComparison(itemsTab, compareSlot, replacementIte spec.jewels[compareSlot.nodeId] = nil end local ok, err = xpcall(function() - -- Tooltip comparison specs only need calc state; node paths are UI data. + -- These temporary specs only feed the misc calculator, which does not read node.path/pathDist. + -- Jewel socket distances are still rebuilt for jewel scaling; + -- Split Personality highlight paths are also refreshed. spec:BuildAllDependsAndPaths(true) end, debug.traceback) if tempItemId then diff --git a/src/Classes/PassiveSpec.lua b/src/Classes/PassiveSpec.lua index 0e30c77369..ec410cfbe1 100644 --- a/src/Classes/PassiveSpec.lua +++ b/src/Classes/PassiveSpec.lua @@ -1088,7 +1088,10 @@ function PassiveSpecClass:NodesInIntuitiveLeapLikeRadius(node) return result end --- Rebuilds dependencies and paths for all nodes +-- Rebuilds dependencies and calculation distances for all nodes. +-- When node paths are skipped, node.path/pathDist remain unset while jewel socket +-- distanceToClassStart values and Split Personality paths are still refreshed. +---@param skipNodePathRebuild? boolean function PassiveSpecClass:BuildAllDependsAndPaths(skipNodePathRebuild) local timelessJewelTypeByConqueror = { vaal = 1, From 5cafb7f4f05299f08f2e68e6aadfd13396c71642 Mon Sep 17 00:00:00 2001 From: Mickael Cagnion Date: Sat, 22 Aug 2026 14:36:56 +0200 Subject: [PATCH 8/8] Unify radius jewel tooltip comparison caching Use the existing output cache for slot-only and multi-slot hovers instead of retaining temporary passive specs. Keep calculation-only specs free of UI path data while preserving socket distances. This reduces duplicate state and repeated calculations without changing tooltip content. --- spec/System/TestRadiusJewelStatDiff_spec.lua | 72 +++++++++++++++----- src/Classes/ItemsTab.lua | 61 +++++------------ src/Classes/PassiveSpec.lua | 14 ++-- 3 files changed, 80 insertions(+), 67 deletions(-) diff --git a/spec/System/TestRadiusJewelStatDiff_spec.lua b/spec/System/TestRadiusJewelStatDiff_spec.lua index 89d9e26f1a..2c4d95dc8a 100644 --- a/spec/System/TestRadiusJewelStatDiff_spec.lua +++ b/spec/System/TestRadiusJewelStatDiff_spec.lua @@ -668,7 +668,7 @@ describe("TestRadiusJewelStatDiff", function() "limited unique radius jewels should rebuild only the same-unique slot that will be displayed") end) - it("AddItemTooltip reuses slot-only radius jewel comparison specs until output changes", function() + it("AddItemTooltip reuses slot-only radius jewel comparison outputs until output changes", function() local spec, sockets = setupAllocatedSockets(2) local item = newCustomLeapJewel("Cached Leap") @@ -678,6 +678,7 @@ describe("TestRadiusJewelStatDiff", function() local originalSlotOnlyTooltips = main.slotOnlyTooltips main.slotOnlyTooltips = true + build.itemsTab.jewelComparisonOutputCache = nil local specClass = getmetatable(spec) local originalBuildAllDependsAndPaths = specClass.BuildAllDependsAndPaths local rebuilds = 0 @@ -685,22 +686,42 @@ describe("TestRadiusJewelStatDiff", function() rebuilds = rebuilds + 1 return originalBuildAllDependsAndPaths(self, ...) end + local originalGetMiscCalculator = build.calcsTab.GetMiscCalculator + local calcCalls = 0 + build.calcsTab.GetMiscCalculator = function(self, ...) + local calcFunc, calcBase = originalGetMiscCalculator(self, ...) + return function(...) + calcCalls = calcCalls + 1 + return calcFunc(...) + end, calcBase + end local ok, err = pcall(function() local tooltip = new("Tooltip"):Tooltip() build.itemsTab:AddItemTooltip(tooltip, item, slot) + local firstPassTooltipText = tooltipText(tooltip) + local firstPassCalcCalls = calcCalls + assert.is_true(firstPassCalcCalls > 0, + "slot-only radius jewel hover should calculate its output on first pass") tooltip = new("Tooltip"):Tooltip() build.itemsTab:AddItemTooltip(tooltip, item, slot) + assert.are.equals(firstPassTooltipText, tooltipText(tooltip), + "cached slot-only radius output should preserve tooltip content") assert.are.equals(1, rebuilds, - "slot-only radius jewel hover should reuse its cached comparison spec") + "slot-only radius jewel hover should reuse its cached comparison output") + assert.are.equals(firstPassCalcCalls, calcCalls, + "slot-only radius jewel hover should not recalculate a cached output") build.outputRevision = build.outputRevision + 1 tooltip = new("Tooltip"):Tooltip() build.itemsTab:AddItemTooltip(tooltip, item, slot) assert.are.equals(2, rebuilds, - "slot-only radius jewel comparison spec cache should reset when output changes") + "slot-only radius jewel output cache should reset when output changes") + assert.is_true(calcCalls > firstPassCalcCalls, + "slot-only radius jewel comparison should recalculate after output changes") end) specClass.BuildAllDependsAndPaths = originalBuildAllDependsAndPaths + build.calcsTab.GetMiscCalculator = originalGetMiscCalculator main.slotOnlyTooltips = originalSlotOnlyTooltips if not ok then error(err) @@ -725,11 +746,13 @@ describe("TestRadiusJewelStatDiff", function() local specClass = getmetatable(spec) local originalBuildAllDependsAndPaths = specClass.BuildAllDependsAndPaths local originalSetNodeDistanceToClassStart = specClass.SetNodeDistanceToClassStart + local originalBuildSplitPersonalityPath = specClass.BuildSplitPersonalityPath local calculationOnlySpec local distanceCalls = 0 - specClass.BuildAllDependsAndPaths = function(self, skipNodePathRebuild, ...) - local result = originalBuildAllDependsAndPaths(self, skipNodePathRebuild, ...) - if skipNodePathRebuild then + local splitPersonalityPathCalls = 0 + specClass.BuildAllDependsAndPaths = function(self, calculationOnly, ...) + local result = originalBuildAllDependsAndPaths(self, calculationOnly, ...) + if calculationOnly then calculationOnlySpec = self end return result @@ -738,26 +761,44 @@ describe("TestRadiusJewelStatDiff", function() distanceCalls = distanceCalls + 1 return originalSetNodeDistanceToClassStart(self, ...) end + specClass.BuildSplitPersonalityPath = function(self, ...) + splitPersonalityPathCalls = splitPersonalityPathCalls + 1 + return originalBuildSplitPersonalityPath(self, ...) + end local ok, err = pcall(function() local tooltip = new("Tooltip"):Tooltip() build.itemsTab:AddItemTooltip(tooltip, radiusItem, radiusSlot) + local calculationOnlyTooltipText = tooltipText(tooltip) + assert.is_truthy(calculationOnlySpec, + "temporary tooltip specs should use the calculation-only path") + for _, node in pairs(calculationOnlySpec.nodes) do + assert.is_nil(node.path, "calculation-only tooltip specs should not retain UI node paths") + assert.is_nil(node.pathDist, "calculation-only tooltip specs should not retain UI path distances") + end + assert.is_true(distanceCalls > 0, + "calculation-only tooltip specs should refresh jewel socket distances used by calc") + assert.are.equals(0, splitPersonalityPathCalls, + "calculation-only tooltip specs should not rebuild Split Personality highlight paths") + + build.itemsTab.jewelComparisonOutputCache = nil + specClass.BuildAllDependsAndPaths = function(self) + return originalBuildAllDependsAndPaths(self) + end + local fullRebuildTooltip = new("Tooltip"):Tooltip() + build.itemsTab:AddItemTooltip(fullRebuildTooltip, radiusItem, radiusSlot) + assert.are.equals(calculationOnlyTooltipText, tooltipText(fullRebuildTooltip), + "calculation-only radius jewel specs should preserve full-rebuild tooltip output") + assert.is_true(splitPersonalityPathCalls > 0, + "the full-rebuild comparison should exercise Split Personality highlight paths") end) specClass.BuildAllDependsAndPaths = originalBuildAllDependsAndPaths specClass.SetNodeDistanceToClassStart = originalSetNodeDistanceToClassStart + specClass.BuildSplitPersonalityPath = originalBuildSplitPersonalityPath main.slotOnlyTooltips = originalSlotOnlyTooltips if not ok then error(err) end - - assert.is_truthy(calculationOnlySpec, - "temporary tooltip specs should use the calculation-only path") - for _, node in pairs(calculationOnlySpec.nodes) do - assert.is_nil(node.path, "calculation-only tooltip specs should not retain UI node paths") - assert.is_nil(node.pathDist, "calculation-only tooltip specs should not retain UI path distances") - end - assert.is_true(distanceCalls > 0, - "temporary tooltip specs should still refresh jewel socket distances used by calc") end) it("AddItemTooltip reuses full radius jewel comparison outputs until output changes", function() @@ -771,7 +812,6 @@ describe("TestRadiusJewelStatDiff", function() local originalSlotOnlyTooltips = main.slotOnlyTooltips main.slotOnlyTooltips = false build.itemsTab.jewelComparisonOutputCache = nil - build.itemsTab.slotOnlyJewelComparisonSpecCache = nil local originalGetMiscCalculator = build.calcsTab.GetMiscCalculator local calcCalls = 0 diff --git a/src/Classes/ItemsTab.lua b/src/Classes/ItemsTab.lua index 29d312b3d8..0013a62d3b 100644 --- a/src/Classes/ItemsTab.lua +++ b/src/Classes/ItemsTab.lua @@ -4186,34 +4186,12 @@ end ---@param itemsTab ItemsTab ---@param compareSlot ItemSlotControl ---@param replacementItem Item ----@param useJewelComparisonSpecCache boolean? -local function buildSpecForJewelComparison(itemsTab, compareSlot, replacementItem, useJewelComparisonSpecCache) +local function buildSpecForJewelComparison(itemsTab, compareSlot, replacementItem) local tempItemId - local replacementItemId = replacementItem and replacementItem.id - local replacementItemIsStored = replacementItemId and itemsTab.items[replacementItemId] == replacementItem - local canCache = useJewelComparisonSpecCache and (not replacementItem or replacementItemIsStored) - local cacheKey - local cache - if canCache then - local outputRevision = itemsTab.build and itemsTab.build.outputRevision or 0 - cache = itemsTab.slotOnlyJewelComparisonSpecCache - if not cache or cache.outputRevision ~= outputRevision then - cache = { - outputRevision = outputRevision, - specs = { }, - } - itemsTab.slotOnlyJewelComparisonSpecCache = cache - end - cacheKey = tostring(compareSlot.nodeId) .. ":" .. tostring(replacementItemId or "") - if cache.specs[cacheKey] then - return cache.specs[cacheKey] - end - end - local spec = cloneSpecForJewelComparison(itemsTab.build.spec) if replacementItem then - if replacementItemIsStored then - spec.jewels[compareSlot.nodeId] = replacementItemId + if replacementItem.id and itemsTab.items[replacementItem.id] == replacementItem then + spec.jewels[compareSlot.nodeId] = replacementItem.id else tempItemId = -1 while itemsTab.items[tempItemId] do @@ -4226,9 +4204,9 @@ local function buildSpecForJewelComparison(itemsTab, compareSlot, replacementIte spec.jewels[compareSlot.nodeId] = nil end local ok, err = xpcall(function() - -- These temporary specs only feed the misc calculator, which does not read node.path/pathDist. - -- Jewel socket distances are still rebuilt for jewel scaling; - -- Split Personality highlight paths are also refreshed. + -- These temporary specs only feed the misc calculator, which does not read regular + -- node paths or Split Personality highlight paths. Jewel socket distances are still + -- rebuilt for Split Personality modifier scaling. spec:BuildAllDependsAndPaths(true) end, debug.traceback) if tempItemId then @@ -4237,9 +4215,6 @@ local function buildSpecForJewelComparison(itemsTab, compareSlot, replacementIte if not ok then error(err, 0) end - if cacheKey then - cache.specs[cacheKey] = spec - end return spec end @@ -4978,22 +4953,20 @@ function ItemsTabClass:AddItemStatDifferences(tooltip, item, base, slot) end end - local function getReplacedItemAndOutput(compareSlot, selItem, useJewelComparisonSpecCache, useJewelComparisonOutputCache) + local function getReplacedItemAndOutput(compareSlot, selItem) selItem = selItem or self.items[compareSlot.selItemId] local override = { repSlotName = compareSlot.slotName, repItem = item ~= selItem and item or nil } local outputCache local outputCacheKey if compareSlot.nodeId and (itemChangesPassiveTree(selItem) or itemChangesPassiveTree(item)) then - if useJewelComparisonOutputCache then - outputCacheKey = getJewelComparisonOutputCacheKey(self, compareSlot, override.repItem) - if outputCacheKey then - outputCache = getJewelComparisonOutputCache(self) - if outputCache.outputs[outputCacheKey] then - return selItem, outputCache.outputs[outputCacheKey] - end + outputCacheKey = getJewelComparisonOutputCacheKey(self, compareSlot, override.repItem) + if outputCacheKey then + outputCache = getJewelComparisonOutputCache(self) + if outputCache.outputs[outputCacheKey] then + return selItem, outputCache.outputs[outputCacheKey] end end - override.spec = buildSpecForJewelComparison(self, compareSlot, override.repItem, useJewelComparisonSpecCache) + override.spec = buildSpecForJewelComparison(self, compareSlot, override.repItem) end local output = calcFunc(override) if outputCacheKey then @@ -5001,9 +4974,9 @@ function ItemsTabClass:AddItemStatDifferences(tooltip, item, base, slot) end return selItem, output end - local function addCompareForSlot(compareSlot, selItem, output, useJewelComparisonSpecCache) + local function addCompareForSlot(compareSlot, selItem, output) if not selItem or not output then - selItem, output = getReplacedItemAndOutput(compareSlot, nil, useJewelComparisonSpecCache) + selItem, output = getReplacedItemAndOutput(compareSlot) end local header if item == selItem then @@ -5019,7 +4992,7 @@ function ItemsTabClass:AddItemStatDifferences(tooltip, item, base, slot) -- one slot. local compareOnlySlot = type(slot) ~= "string" and slot or self.slots[slot] if main.slotOnlyTooltips and slot then - if compareOnlySlot then addCompareForSlot(compareOnlySlot, nil, nil, true) end + if compareOnlySlot then addCompareForSlot(compareOnlySlot) end return end @@ -5040,7 +5013,7 @@ function ItemsTabClass:AddItemStatDifferences(tooltip, item, base, slot) local slots = {} for _, slotEntry in ipairs(slotCandidates) do if not isLimitedUniqueAtLimit or slotEntry.isSameUnique then - local _, output = getReplacedItemAndOutput(slotEntry.compareSlot, slotEntry.selItem, nil, true) + local _, output = getReplacedItemAndOutput(slotEntry.compareSlot, slotEntry.selItem) slotEntry.output = output table.insert(slots, slotEntry) end diff --git a/src/Classes/PassiveSpec.lua b/src/Classes/PassiveSpec.lua index ec410cfbe1..4bc4f97aa4 100644 --- a/src/Classes/PassiveSpec.lua +++ b/src/Classes/PassiveSpec.lua @@ -1089,10 +1089,10 @@ function PassiveSpecClass:NodesInIntuitiveLeapLikeRadius(node) end -- Rebuilds dependencies and calculation distances for all nodes. --- When node paths are skipped, node.path/pathDist remain unset while jewel socket --- distanceToClassStart values and Split Personality paths are still refreshed. ----@param skipNodePathRebuild? boolean -function PassiveSpecClass:BuildAllDependsAndPaths(skipNodePathRebuild) +-- Calculation-only specs leave UI path fields unset while still refreshing jewel +-- socket distanceToClassStart values used by the calculator. +---@param calculationOnly? boolean +function PassiveSpecClass:BuildAllDependsAndPaths(calculationOnly) local timelessJewelTypeByConqueror = { vaal = 1, karui = 2, @@ -1628,7 +1628,7 @@ function PassiveSpecClass:BuildAllDependsAndPaths(skipNodePathRebuild) -- Reset and rebuild all node paths for _, node in pairs(self.nodes) do - if skipNodePathRebuild then + if calculationOnly then node.pathDist = nil node.path = nil else @@ -1646,7 +1646,7 @@ function PassiveSpecClass:BuildAllDependsAndPaths(skipNodePathRebuild) end end - if not skipNodePathRebuild then + if not calculationOnly then -- Use a multi-source 0-1 BFS to find the closest allocated node. Allocated -- nodes have zero weight, while each unallocated node costs one passive point. local queue = { } @@ -1701,7 +1701,7 @@ function PassiveSpecClass:BuildAllDependsAndPaths(skipNodePathRebuild) end end - if not skipNodePathRebuild then + if not calculationOnly then self:BuildSplitPersonalityPath() end end