diff --git a/spec/System/TestPassiveTreeView_spec.lua b/spec/System/TestPassiveTreeView_spec.lua new file mode 100644 index 00000000000..161b3200d2e --- /dev/null +++ b/spec/System/TestPassiveTreeView_spec.lua @@ -0,0 +1,116 @@ +describe("PassiveTreeView jewel comparison", function() + local viewer + + local function newJewel(name, raw) + return { + name = name, + BuildRaw = function() + return raw + end, + } + end + + local function setCompareJewel(jewel, allocated) + local itemsTab = { + items = { [2] = jewel }, + sockets = { [100] = { selItemId = jewel and 2 or 0 } }, + } + viewer.compareSpec = { + allocNodes = { [100] = allocated }, + build = { itemsTab = itemsTab }, + } + return itemsTab + end + + before_each(function() + newBuild() + viewer = build.treeTab.viewer + end) + + it("matches jewels by their complete raw representation", function() + local primaryJewel = newJewel("Controller Metamorphosis", "Radius: Small") + local compareJewel = newJewel("Controller Metamorphosis", "Radius: Small") + setCompareJewel(compareJewel, true) + + local color = viewer:GetCompareNodeColor( + { id = 100, type = "Socket", alloc = true }, + { alloc = true }, + { jewels = { [100] = 1 } }, + { itemsTab = { items = { [1] = primaryJewel } } }, + "^xFFFFFF" + ) + + assert.are.equals("^xFFFFFF", color) + end) + + it("marks same-name jewels with different variants as changed", function() + local primaryJewel = newJewel("Controller Metamorphosis", "Radius: Small") + local compareJewel = newJewel("Controller Metamorphosis", "Radius: Large") + setCompareJewel(compareJewel, true) + + local red, green, blue = viewer:GetCompareNodeColor( + { id = 100, type = "Socket", alloc = true }, + { alloc = true }, + { jewels = { [100] = 1 } }, + { itemsTab = { items = { [1] = primaryJewel } } }, + "^xFFFFFF" + ) + + assert.are.same({ 0, 0, 1 }, { red, green, blue }) + end) + + it("shows both full jewel tooltips when allocated socket jewels differ", function() + local primaryJewel = newJewel("Controller Metamorphosis", "Radius: Small") + local compareJewel = newJewel("Controller Metamorphosis", "Radius: Large") + local compareItemsTab = setCompareJewel(compareJewel, true) + local primaryTooltipItems = { } + local compareTooltipItems = { } + local socket = { IsEnabled = function() return false end } + local tooltip = { + AddLine = function() end, + AddSeparator = function() end, + } + local primaryBuild = { + itemsTab = { + GetSocketAndJewelForNodeID = function() + return socket, primaryJewel + end, + AddItemTooltip = function(_, _, item) + table.insert(primaryTooltipItems, item) + end, + }, + } + compareItemsTab.AddItemTooltip = function(_, _, item) + table.insert(compareTooltipItems, item) + end + + viewer:AddNodeTooltip(tooltip, { id = 100, type = "Socket", alloc = true }, primaryBuild) + + assert.are.same({ primaryJewel }, primaryTooltipItems) + assert.are.same({ compareJewel }, compareTooltipItems) + end) + + it("shows the compared jewel tooltip when only the compared socket is allocated", function() + local compareJewel = newJewel("Controller Metamorphosis", "Radius: Large") + local compareItemsTab = setCompareJewel(compareJewel, true) + local compareTooltipItems = { } + local tooltip = { + AddLine = function() end, + AddSeparator = function() end, + } + local primaryBuild = { + itemsTab = { + GetSocketAndJewelForNodeID = function() + return { }, nil + end, + }, + } + compareItemsTab.AddItemTooltip = function(_, _, item) + table.insert(compareTooltipItems, item) + end + + viewer:AddNodeTooltip(tooltip, { id = 100, type = "Socket", alloc = false }, primaryBuild) + + assert.are.same({ compareJewel }, compareTooltipItems) + end) +end) diff --git a/src/Classes/PassiveTreeView.lua b/src/Classes/PassiveTreeView.lua index 6d90dee9b91..b6795d8f549 100644 --- a/src/Classes/PassiveTreeView.lua +++ b/src/Classes/PassiveTreeView.lua @@ -14,6 +14,10 @@ local m_floor = math.floor local band = bit.band local b_rshift = bit.rshift +local JEWEL_RADIUS_TINT_NEUTRAL = { 1, 1, 1, 0.7 } +local JEWEL_RADIUS_TINT_PRIMARY_ONLY = { 1, 0, 0, 0.7 } +local JEWEL_RADIUS_TINT_COMPARE_ONLY = { 0, 1, 0, 0.7 } + local gemTooltip = LoadModule("Classes/GemTooltip") local PassiveTreeViewClass = newClass("PassiveTreeView", function(self) self.ring = NewImageHandle() @@ -144,6 +148,13 @@ function PassiveTreeViewClass:GetJewelSocketOverlay(jewel, isExpansion) end end +local function compareJewelsEqual(a, b) + if not a or not b then + return a == b + end + return a:BuildRaw() == b:BuildRaw() +end + -- Returns the draw color for a node when compare overlay is active. -- Handles diff coloring for allocated/unallocated, mastery changes, and jewel socket differences. function PassiveTreeViewClass:GetCompareNodeColor(node, compareNode, spec, build, nodeDefaultColor) @@ -160,9 +171,7 @@ function PassiveTreeViewClass:GetCompareNodeColor(node, compareNode, spec, build local pJewelId = spec.jewels[node.id] local pJewel = pJewelId and build.itemsTab.items[pJewelId] local cJewel = self:GetCompareJewel(node.id) - local pName = pJewel and pJewel.name or "" - local cName = cJewel and cJewel.name or "" - if pName ~= cName then + if not compareJewelsEqual(pJewel, cJewel) then return 0, 0, 1 end end @@ -1115,80 +1124,89 @@ function PassiveTreeViewClass:Draw(build, viewPort, inputEvents) end -- Draw ring overlays for jewel sockets + local function drawJewelRadius(jewel, scrX, scrY, tint) + local radData = build.data.jewelRadius[jewel.jewelRadiusIndex] + local outerSize = radData.outer * scale + local innerSize = radData.inner * scale * 1.06 + SetDrawColor(tint[1], tint[2], tint[3], tint[4]) + if jewel.title:match("Impossible Escape") then + -- Impossible Escape ring shows on the allocated Keystone + for keystoneName, _ in pairs(jewel.jewelData.impossibleEscapeKeystones) do + local keystone = spec.tree.keystoneMap[keystoneName] + if keystone and keystone.x and keystone.y then + innerSize = 150 * scale + local keyX, keyY = treeToScreen(keystone.x, keystone.y) + self:DrawImageRotated(self.jewelShadedOuterRing, keyX, keyY, outerSize * 2, outerSize * 2, -0.8) + self:DrawImageRotated(self.jewelShadedOuterRingFlipped, keyX, keyY, outerSize * 2, outerSize * 2, 1) + self:DrawImageRotated(self.jewelShadedInnerRing, keyX, keyY, innerSize * 2, innerSize * 2, -1.2) + self:DrawImageRotated(self.jewelShadedInnerRingFlipped, keyX, keyY, innerSize * 2, innerSize * 2, 1.0) + end + end + elseif jewel.title:match("^Brutal Restraint") then + self:DrawImageRotated(self.maraketh1, scrX, scrY, outerSize * 2, outerSize * 2, -0.7) + self:DrawImageRotated(self.maraketh2, scrX, scrY, outerSize * 2, outerSize * 2, 0.7) + elseif jewel.title:match("^Elegant Hubris") then + self:DrawImageRotated(self.eternal1, scrX, scrY, outerSize * 2, outerSize * 2, -0.7) + self:DrawImageRotated(self.eternal2, scrX, scrY, outerSize * 2, outerSize * 2, 0.7) + elseif jewel.title:match("^Glorious Vanity") then + self:DrawImageRotated(self.vaal1, scrX, scrY, outerSize * 2, outerSize * 2, -0.7) + self:DrawImageRotated(self.vaal2, scrX, scrY, outerSize * 2, outerSize * 2, 0.7) + elseif jewel.title:match("^Lethal Pride") then + self:DrawImageRotated(self.karui1, scrX, scrY, outerSize * 2, outerSize * 2, -0.7) + self:DrawImageRotated(self.karui2, scrX, scrY, outerSize * 2, outerSize * 2, 0.7) + elseif jewel.title:match("^Militant Faith") then + self:DrawImageRotated(self.templar1, scrX, scrY, outerSize * 2, outerSize * 2, -0.7) + self:DrawImageRotated(self.templar2, scrX, scrY, outerSize * 2, outerSize * 2, 0.7) + elseif jewel.title:match("^Heroic Tragedy") then + self:DrawImageRotated(self.kalguur1, scrX, scrY, outerSize * 2, outerSize * 2, -0.7) + self:DrawImageRotated(self.kalguur2, scrX, scrY, outerSize * 2, outerSize * 2, 0.7) + else + self:DrawImageRotated(self.jewelShadedOuterRing, scrX, scrY, outerSize * 2, outerSize * 2, -0.7) + self:DrawImageRotated(self.jewelShadedOuterRingFlipped, scrX, scrY, outerSize * 2, outerSize * 2, 0.7) + self:DrawImageRotated(self.jewelShadedInnerRing, scrX, scrY, innerSize * 2, innerSize * 2, -0.7) + self:DrawImageRotated(self.jewelShadedInnerRingFlipped, scrX, scrY, innerSize * 2, innerSize * 2, 0.7) + end + end SetDrawLayer(nil, 25) for nodeId in pairs(tree.sockets) do local node = spec.nodes[nodeId] if node and node.name ~= "Charm Socket" and (not node.expansionJewel or node.expansionJewel.size == 2) then local scrX, scrY = treeToScreen(node.x, node.y) local socket, jewel = build.itemsTab:GetSocketAndJewelForNodeID(nodeId) + local compareNode = self.compareSpec and self.compareSpec.nodes[nodeId] or nil + local cJewel = self.compareSpec and self:GetCompareJewel(nodeId) or nil if node == hoverNode then - local isThreadOfHope = jewel and jewel.jewelRadiusLabel == "Variable" - if isThreadOfHope then - for _, radData in ipairs(build.data.jewelRadius) do - local outerSize = radData.outer * scale - local innerSize = radData.inner * scale - -- Jewel in socket is Thread of Hope or similar, draw it's annulus + local effectiveJewel = jewel or cJewel + local isThreadOfHope = effectiveJewel and effectiveJewel.jewelRadiusLabel == "Variable" + for _, radData in ipairs(build.data.jewelRadius) do + local outerSize = radData.outer * scale + local innerSize = radData.inner * scale + if isThreadOfHope then + -- Thread of Hope-like: draw the annulus (only radii with a non-zero inner) if innerSize ~= 0 then SetDrawColor(radData.col) DrawImage(self.ring, scrX - outerSize, scrY - outerSize, outerSize * 2, outerSize * 2) DrawImage(self.ring, scrX - innerSize, scrY - innerSize, innerSize * 2, innerSize * 2) end - end - else - for _, radData in ipairs(build.data.jewelRadius) do - local outerSize = radData.outer * scale - local innerSize = radData.inner * scale - -- Jewel in socket is not Thread of Hope or similar, draw normal jewel radius + else + -- Standard jewel: draw the full-disc radii (inner == 0) if innerSize == 0 then SetDrawColor(radData.col) DrawImage(self.ring, scrX - outerSize, scrY - outerSize, outerSize * 2, outerSize * 2) end end end - elseif node.alloc then - if jewel and jewel.jewelRadiusIndex then - -- Draw only the selected jewel radius - local radData = build.data.jewelRadius[jewel.jewelRadiusIndex] - local outerSize = radData.outer * scale - local innerSize = radData.inner * scale * 1.06 - SetDrawColor(1,1,1,0.7) - if jewel.title:match("Impossible Escape") then - -- Impossible Escape ring shows on the allocated Keystone - for keystoneName, _ in pairs(jewel.jewelData.impossibleEscapeKeystones) do - local keystone = spec.tree.keystoneMap[keystoneName] - if keystone and keystone.x and keystone.y then - innerSize = 150 * scale - local keyX, keyY = treeToScreen(keystone.x, keystone.y) - self:DrawImageRotated(self.jewelShadedOuterRing, keyX, keyY, outerSize * 2, outerSize * 2, -0.8) - self:DrawImageRotated(self.jewelShadedOuterRingFlipped, keyX, keyY, outerSize * 2, outerSize * 2, 1) - self:DrawImageRotated(self.jewelShadedInnerRing, keyX, keyY, innerSize * 2, innerSize * 2, -1.2) - self:DrawImageRotated(self.jewelShadedInnerRingFlipped, keyX, keyY, innerSize * 2, innerSize * 2, 1.0) - end - end - elseif jewel.title:match("^Brutal Restraint") then - self:DrawImageRotated(self.maraketh1, scrX, scrY, outerSize * 2, outerSize * 2, -0.7) - self:DrawImageRotated(self.maraketh2, scrX, scrY, outerSize * 2, outerSize * 2, 0.7) - elseif jewel.title:match("^Elegant Hubris") then - self:DrawImageRotated(self.eternal1, scrX, scrY, outerSize * 2, outerSize * 2, -0.7) - self:DrawImageRotated(self.eternal2, scrX, scrY, outerSize * 2, outerSize * 2, 0.7) - elseif jewel.title:match("^Glorious Vanity") then - self:DrawImageRotated(self.vaal1, scrX, scrY, outerSize * 2, outerSize * 2, -0.7) - self:DrawImageRotated(self.vaal2, scrX, scrY, outerSize * 2, outerSize * 2, 0.7) - elseif jewel.title:match("^Lethal Pride") then - self:DrawImageRotated(self.karui1, scrX, scrY, outerSize * 2, outerSize * 2, -0.7) - self:DrawImageRotated(self.karui2, scrX, scrY, outerSize * 2, outerSize * 2, 0.7) - elseif jewel.title:match("^Militant Faith") then - self:DrawImageRotated(self.templar1, scrX, scrY, outerSize * 2, outerSize * 2, -0.7) - self:DrawImageRotated(self.templar2, scrX, scrY, outerSize * 2, outerSize * 2, 0.7) - elseif jewel.title:match("^Heroic Tragedy") then - self:DrawImageRotated(self.kalguur1, scrX, scrY, outerSize * 2, outerSize * 2, -0.7) - self:DrawImageRotated(self.kalguur2, scrX, scrY, outerSize * 2, outerSize * 2, 0.7) - else - self:DrawImageRotated(self.jewelShadedOuterRing, scrX, scrY, outerSize * 2, outerSize * 2, -0.7) - self:DrawImageRotated(self.jewelShadedOuterRingFlipped, scrX, scrY, outerSize * 2, outerSize * 2, 0.7) - self:DrawImageRotated(self.jewelShadedInnerRing, scrX, scrY, innerSize * 2, innerSize * 2, -0.7) - self:DrawImageRotated(self.jewelShadedInnerRingFlipped, scrX, scrY, innerSize * 2, innerSize * 2, 0.7) - end + end + if node.alloc or (compareNode and compareNode.alloc) then + local pHasRadius = jewel and jewel.jewelRadiusIndex + local cHasRadius = cJewel and cJewel.jewelRadiusIndex + local sameJewel = compareJewelsEqual(jewel, cJewel) + if pHasRadius then + local tint = (not self.compareSpec or sameJewel) and JEWEL_RADIUS_TINT_NEUTRAL or JEWEL_RADIUS_TINT_PRIMARY_ONLY + drawJewelRadius(jewel, scrX, scrY, tint) + end + if cHasRadius and not sameJewel then + drawJewelRadius(cJewel, scrX, scrY, JEWEL_RADIUS_TINT_COMPARE_ONLY) end end end @@ -1407,30 +1425,40 @@ function PassiveTreeViewClass:AddNodeTooltip(tooltip, node, build) self.skillTooltip:Clear() tooltip.center = true tooltip.maxWidth = 800 + -- Appends the compare spec's jewel tooltip if it has a jewel in this allocated socket. + local function addCompareJewelSection(socket, withLabel) + local cJewel = self.compareSpec and self:GetCompareJewel(node.id) + local cAllocated = self.compareSpec and self.compareSpec.allocNodes and self.compareSpec.allocNodes[node.id] + if not cJewel or not cAllocated then + return false + end + if withLabel then + tooltip:AddSeparator(14) + tooltip:AddLine(14, colorCodes.DEXTERITY .. "Compared build:") + end + self.compareSpec.build.itemsTab:AddItemTooltip(tooltip, cJewel, socket) + return true + end + -- Special case for sockets if node.type == "Socket" and node.alloc then local socket, jewel = build.itemsTab:GetSocketAndJewelForNodeID(node.id) + local cJewel = self.compareSpec and self:GetCompareJewel(node.id) or nil if jewel then build.itemsTab:AddItemTooltip(tooltip, jewel, socket) - if node.distanceToClassStart and node.distanceToClassStart > 0 then - tooltip:AddSeparator(14) - tooltip:AddLine(16, string.format("^7Distance to start: %d", node.distanceToClassStart)) + if not compareJewelsEqual(jewel, cJewel) then + addCompareJewelSection(socket, true) end - else + elseif not addCompareJewelSection(socket, false) then self:AddNodeName(tooltip, node, build) end - -- Show compare build's jewel info when in overlay compare mode - if self.compareSpec then - local cJewel = self:GetCompareJewel(node.id) - local cAllocated = self.compareSpec.allocNodes and self.compareSpec.allocNodes[node.id] - if cJewel then - tooltip:AddSeparator(14) - tooltip:AddLine(16, colorCodes.WARNING .. "Compared build jewel:") - tooltip:AddLine(16, (cJewel.rarity == "UNIQUE" and colorCodes.UNIQUE or cJewel.rarity == "RARE" and colorCodes.RARE or cJewel.rarity == "MAGIC" and colorCodes.MAGIC or "^7") .. cJewel.name) - elseif cAllocated then - tooltip:AddSeparator(14) - tooltip:AddLine(16, colorCodes.WARNING .. "Compared build: ^7(empty socket)") - end + if self.compareSpec and self.compareSpec.allocNodes and self.compareSpec.allocNodes[node.id] and not cJewel then + tooltip:AddSeparator(14) + tooltip:AddLine(16, colorCodes.WARNING .. "Compared build: ^7(empty socket)") + end + if node.distanceToClassStart and node.distanceToClassStart > 0 then + tooltip:AddSeparator(14) + tooltip:AddLine(16, string.format("^7Distance to start: %d", node.distanceToClassStart)) end tooltip:AddSeparator(14) if socket:IsEnabled() then @@ -1441,16 +1469,9 @@ function PassiveTreeViewClass:AddNodeTooltip(tooltip, node, build) end -- For unallocated sockets, show compare build's jewel if it has one - if node.type == "Socket" and not node.alloc and self.compareSpec then - local cJewel = self:GetCompareJewel(node.id) - local cItemsTab = self.compareSpec.build and self.compareSpec.build.itemsTab - local cAllocated = self.compareSpec.allocNodes and self.compareSpec.allocNodes[node.id] - if cJewel and cAllocated then - -- Show the compare build's jewel tooltip instead of generic socket info - local socket = build.itemsTab:GetSocketAndJewelForNodeID(node.id) - cItemsTab:AddItemTooltip(tooltip, cJewel, socket) - tooltip:AddSeparator(14) - tooltip:AddLine(14, colorCodes.DEXTERITY .. "Jewel from compared build") + if node.type == "Socket" and not node.alloc then + local socket = build.itemsTab:GetSocketAndJewelForNodeID(node.id) + if addCompareJewelSection(socket, false) then tooltip:AddLine(14, colorCodes.TIP.."Tip: Hold Shift or Ctrl to hide this tooltip.") return end