From 58a5e24f61f6b367940ecd3ccd604d940e42c4e9 Mon Sep 17 00:00:00 2001 From: EtherealCarnivore <42915554+EtherealCarnivore@users.noreply.github.com> Date: Tue, 5 May 2026 10:17:30 +0300 Subject: [PATCH 1/2] Ignore local .notes/ directory --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index aa34b5aaf7..37c7b6a047 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,6 @@ src/Data/TimelessJewelData/*.bin # Simplegraphic Debugging runtime/imgui.ini runtime/SimpleGraphic/SimpleGraphic.log + +# Local-only documentation notes (never commit upstream) +.notes/ From a61e723ce15bb2e5aa66b249f09c199617ccf176 Mon Sep 17 00:00:00 2001 From: EtherealCarnivore <42915554+EtherealCarnivore@users.noreply.github.com> Date: Wed, 6 May 2026 12:48:51 +0300 Subject: [PATCH 2/2] Thread of Hope all-rings planning toggle Adds a tree-view toggle (T) that draws all five Variable jewel rings around each allocated Thread of Hope socket and tints nodes by the ring they sit in, so a planner can see where every possible roll would land before settling on a radius. - Spec dependency/path math respects the toggle for Variable jewels - Massive radius recoloured from dark green to gold so all five rings stay visually distinct alongside the teal Medium ring - First-time top-left hint surfaces the T hotkey and self-dismisses on first press (persisted via main.toHHintDismissed in Misc) - Jewel tooltip gains a display-only Tip line for Variable radius; not added to rawLines, so import/export are unaffected --- src/Classes/ItemsTab.lua | 5 +++ src/Classes/PassiveSpec.lua | 78 +++++++++++++++++++++------------ src/Classes/PassiveTreeView.lua | 68 +++++++++++++++++++++++++++- src/Modules/Data.lua | 4 +- src/Modules/Main.lua | 4 ++ 5 files changed, 128 insertions(+), 31 deletions(-) diff --git a/src/Classes/ItemsTab.lua b/src/Classes/ItemsTab.lua index aaa060c8c7..86dd88ea34 100644 --- a/src/Classes/ItemsTab.lua +++ b/src/Classes/ItemsTab.lua @@ -3564,6 +3564,11 @@ function ItemsTabClass:AddItemTooltip(tooltip, item, slot, dbMode) end if item.jewelRadiusLabel then tooltip:AddLine(fontSizeBig, "^x7F7F7FRadius: ^7"..item.jewelRadiusLabel, "FONTIN SC") + if item.jewelRadiusLabel == "Variable" then + -- Display-only hint, not added to rawLines, so item import/export is unaffected. + tooltip:AddSeparator(4) + tooltip:AddLine(fontSizeBig, colorCodes.MAGIC.."Tip: Press ^x7F7F7FT"..colorCodes.MAGIC.." in the tree view to toggle all five ring sizes.", "FONTIN") + end end if item.jewelRadiusData and slot and item.jewelRadiusData[slot.nodeId] then local radiusData = item.jewelRadiusData[slot.nodeId] diff --git a/src/Classes/PassiveSpec.lua b/src/Classes/PassiveSpec.lua index c67b7ac27e..a270ed6843 100644 --- a/src/Classes/PassiveSpec.lua +++ b/src/Classes/PassiveSpec.lua @@ -1031,16 +1031,32 @@ function PassiveSpecClass:AddMasteryEffectOptionsToNode(node) node.allMasteryOptions = true end +-- Returns jewelRadius indices to consider for this item. When the tree-view planning +-- toggle is on for a Variable-radius jewel, all 5 Variable rings are treated as candidate +-- radii so dependency/path calculations cover every possible socketed roll. +function PassiveSpecClass:GetEffectiveRadiusIndices(item) + local toHMode = self.build.treeTab and self.build.treeTab.viewer + and self.build.treeTab.viewer.toHRingMode + if toHMode and item.jewelRadiusLabel == "Variable" then + return { 6, 7, 8, 9, 10 } + end + return { item.jewelRadiusIndex } +end + function PassiveSpecClass:NodesInIntuitiveLeapLikeRadius(node) local result = { } if self.jewels[node.id] and self.jewels[node.id] > 0 then local item = self.build.itemsTab.items[self.jewels[node.id]] local radiusIndex = item.jewelRadiusIndex if item and item.jewelData and item.jewelData.intuitiveLeapLike then - local inRadius = self.nodes[node.id].nodesInRadius and self.nodes[node.id].nodesInRadius[radiusIndex] - for affectedNodeId, affectedNode in pairs(inRadius or {}) do - if self.nodes[affectedNodeId].alloc then - t_insert(result, self.nodes[affectedNodeId]) + local seen = { } + for _, idx in ipairs(self:GetEffectiveRadiusIndices(item)) do + local inRadius = self.nodes[node.id].nodesInRadius and self.nodes[node.id].nodesInRadius[idx] + for affectedNodeId, affectedNode in pairs(inRadius or {}) do + if self.nodes[affectedNodeId].alloc and not seen[affectedNodeId] then + seen[affectedNodeId] = true + t_insert(result, self.nodes[affectedNodeId]) + end end end end @@ -1082,18 +1098,21 @@ function PassiveSpecClass:BuildAllDependsAndPaths() local item = self.build.itemsTab.items[itemId] if item and item.jewelRadiusIndex and self.allocNodes[nodeId] and item.jewelData and not item.jewelData.limitDisabled then local radiusIndex = item.jewelRadiusIndex - if self.nodes[nodeId].nodesInRadius and self.nodes[nodeId].nodesInRadius[radiusIndex][node.id] then - if itemId ~= 0 then - if item.jewelData.intuitiveLeapLike and not (item.jewelData.intuitiveLeapKeystoneOnly and node.type ~= "Keystone") then - -- This node depends on Intuitive Leap-like behaviour - -- This flag: - -- 1. Prevents generation of paths from this node unless it's also connected to the start - -- 2. Prevents allocation of path nodes when this node is being allocated - t_insert(node.intuitiveLeapLikesAffecting, self.nodes[nodeId]) - end - if item.jewelData.conqueredBy then - node.conqueredBy = item.jewelData.conqueredBy + for _, idx in ipairs(self:GetEffectiveRadiusIndices(item)) do + if self.nodes[nodeId].nodesInRadius and self.nodes[nodeId].nodesInRadius[idx][node.id] then + if itemId ~= 0 then + if item.jewelData.intuitiveLeapLike and not (item.jewelData.intuitiveLeapKeystoneOnly and node.type ~= "Keystone") then + -- This node depends on Intuitive Leap-like behaviour + -- This flag: + -- 1. Prevents generation of paths from this node unless it's also connected to the start + -- 2. Prevents allocation of path nodes when this node is being allocated + t_insert(node.intuitiveLeapLikesAffecting, self.nodes[nodeId]) + end + if item.jewelData.conqueredBy then + node.conqueredBy = item.jewelData.conqueredBy + end end + break end end @@ -1447,19 +1466,22 @@ function PassiveSpecClass:BuildAllDependsAndPaths() local prune = true for nodeId, itemId in pairs(self.jewels) do if self.allocNodes[nodeId] then - if itemId ~= 0 and ( - self.build.itemsTab.items[itemId] and ( - self.build.itemsTab.items[itemId].jewelData - and self.build.itemsTab.items[itemId].jewelData.intuitiveLeapLike - and self.build.itemsTab.items[itemId].jewelRadiusIndex - and self.nodes[nodeId].nodesInRadius - and self.nodes[nodeId].nodesInRadius[self.build.itemsTab.items[itemId].jewelRadiusIndex][depNode.id] - ) or ( - self.build.itemsTab.items[itemId].jewelData - and self.build.itemsTab.items[itemId].jewelData.impossibleEscapeKeystones - and self:NodeInKeystoneRadius(self.build.itemsTab.items[itemId].jewelData.impossibleEscapeKeystones, depNode.id, self.build.itemsTab.items[itemId].jewelRadiusIndex) - ) - ) then + local item = self.build.itemsTab.items[itemId] + local socketNode = self.nodes[nodeId] + local leapHit = false + if itemId ~= 0 and item and item.jewelData and item.jewelData.intuitiveLeapLike + and item.jewelRadiusIndex and socketNode.nodesInRadius then + for _, idx in ipairs(self:GetEffectiveRadiusIndices(item)) do + if socketNode.nodesInRadius[idx] and socketNode.nodesInRadius[idx][depNode.id] then + leapHit = true + break + end + end + end + local keyHit = itemId ~= 0 and item and item.jewelData + and item.jewelData.impossibleEscapeKeystones + and self:NodeInKeystoneRadius(item.jewelData.impossibleEscapeKeystones, depNode.id, item.jewelRadiusIndex) + if leapHit or keyHit then -- Hold off on the pruning; this node could be supported by Intuitive Leap-like jewel prune = false if not intuitiveLeaps[nodeId] then diff --git a/src/Classes/PassiveTreeView.lua b/src/Classes/PassiveTreeView.lua index 3370a436e2..8ab98556b9 100644 --- a/src/Classes/PassiveTreeView.lua +++ b/src/Classes/PassiveTreeView.lua @@ -65,6 +65,7 @@ local PassiveTreeViewClass = newClass("PassiveTreeView", function(self) self.searchStrCached = "" self.searchStrResults = {} self.showStatDifferences = true + self.toHRingMode = nil -- nil | true (planning toggle: show all Variable rings) self.hoverNode = nil end) @@ -116,6 +117,15 @@ function PassiveTreeViewClass:Draw(build, viewPort, inputEvents) end elseif event.key == "p" then self.showHeatMap = not self.showHeatMap + elseif event.key == "t" then + -- Toggle Thread of Hope all-rings planning view + self.toHRingMode = not self.toHRingMode or nil + if not main.toHHintDismissed then + main.toHHintDismissed = true + main:SaveSettings() + end + build.spec:BuildAllDependsAndPaths() + build.buildFlag = true elseif event.key == "d" and IsKeyDown("CTRL") then self.showStatDifferences = not self.showStatDifferences elseif event.key == "c" and IsKeyDown("CTRL") and self.hoverNode and self.hoverNode.type ~= "Socket" then @@ -635,6 +645,35 @@ function PassiveTreeViewClass:Draw(build, viewPort, inputEvents) end end + -- One pass over allocated jewel sockets: detects whether any Variable-radius jewel is + -- socketed (used to gate the first-time hint), and when the planning toggle is on, builds + -- a nodeId -> ring colour tint map. First-seen wins when sockets overlap; rings within one + -- socket are non-overlapping so per-socket order is deterministic. + local toHTintMap = self.toHRingMode and { } or nil + local hasVariableJewel = false + for socketNodeId, itemId in pairs(spec.jewels) do + if itemId ~= 0 and spec.allocNodes[socketNodeId] then + local item = build.itemsTab.items[itemId] + local socketNode = spec.nodes[socketNodeId] + if item and item.jewelRadiusLabel == "Variable" then + hasVariableJewel = true + if toHTintMap and socketNode and socketNode.nodesInRadius then + for ringIdx = 6, 10 do + local nodesInRing = socketNode.nodesInRadius[ringIdx] + local radData = build.data.jewelRadius[ringIdx] + if nodesInRing and radData then + for nId in pairs(nodesInRing) do + if not toHTintMap[nId] then + toHTintMap[nId] = radData.col + end + end + end + end + end + end + end + end + -- Draw the nodes for nodeId, node in pairs(spec.nodes) do -- Determine the base and overlay images for this node based on type and state @@ -850,11 +889,13 @@ function PassiveTreeViewClass:Draw(build, viewPort, inputEvents) if overlay then -- Draw overlay if node.type ~= "ClassStart" and node.type ~= "AscendClassStart" then + local hoverTinted = false if hoverNode and hoverNode ~= node then -- Mouse is hovering over a different node if hoverDep and hoverDep[node] then -- This node depends on the hover node, turn it red SetDrawColor(1, 0, 0) + hoverTinted = true elseif hoverNode.type == "Socket" and hoverNode.nodesInRadius then -- Hover node is a socket, check if this node falls within its radius and color it accordingly local socket, jewel = build.itemsTab:GetSocketAndJewelForNodeID(hoverNode.id) @@ -866,6 +907,7 @@ function PassiveTreeViewClass:Draw(build, viewPort, inputEvents) -- Draw Thread of Hope's annuli if data.inner ~= 0 then SetDrawColor(data.col) + hoverTinted = true break end end @@ -877,6 +919,7 @@ function PassiveTreeViewClass:Draw(build, viewPort, inputEvents) -- Draw normal jewel radii if data.inner == 0 then SetDrawColor(data.col) + hoverTinted = true break end end @@ -884,6 +927,10 @@ function PassiveTreeViewClass:Draw(build, viewPort, inputEvents) end end end + if not hoverTinted and toHTintMap and toHTintMap[nodeId] then + -- Planning toggle is on: tint nodes by the Thread of Hope ring they sit in + SetDrawColor(toHTintMap[nodeId]) + end end self:DrawAsset(tree.assets[overlay], scrX, scrY, scale) SetDrawColor(1, 1, 1) @@ -959,7 +1006,18 @@ function PassiveTreeViewClass:Draw(build, viewPort, inputEvents) end end elseif node.alloc then - if jewel and jewel.jewelRadiusIndex then + if self.toHRingMode and jewel and jewel.jewelRadiusLabel == "Variable" then + -- Planning toggle: draw all five Variable-radius annuli around the socket + for _, radData in ipairs(build.data.jewelRadius) do + local outerSize = radData.outer * scale + local innerSize = radData.inner * scale + 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 + elseif jewel and jewel.jewelRadiusIndex then -- Draw only the selected jewel radius local radData = build.data.jewelRadius[jewel.jewelRadiusIndex] local outerSize = radData.outer * scale @@ -1006,6 +1064,14 @@ function PassiveTreeViewClass:Draw(build, viewPort, inputEvents) end end end + + -- First-time hint: shown only while the toggle is off, the user has never pressed T, + -- and the build has at least one allocated Variable-radius jewel. + if hasVariableJewel and not self.toHRingMode and not main.toHHintDismissed then + SetDrawLayer(nil, 100) + DrawString(viewPort.x + 12, viewPort.y + 12, "LEFT", 18, "FONTIN", + "^xFFCC00Tip: ^7Press ^xFFCC00T^7 to view all Thread of Hope ring sizes.") + end end function PassiveTreeViewClass:DrawImageRotated(handle, x, y, width, height, angle, ...) if main.showAnimations == false then diff --git a/src/Modules/Data.lua b/src/Modules/Data.lua index 678f6578e7..d9ee235377 100644 --- a/src/Modules/Data.lua +++ b/src/Modules/Data.lua @@ -540,13 +540,13 @@ data.jewelRadii = { { inner = 0, outer = 1440, col = "^x66FFCC", label = "Medium" }, { inner = 0, outer = 1800, col = "^x2222CC", label = "Large" }, { inner = 0, outer = 2400, col = "^xC100FF", label = "Very Large" }, - { inner = 0, outer = 2880, col = "^x0B9300", label = "Massive" }, + { inner = 0, outer = 2880, col = "^xFFD700", label = "Massive" }, { inner = 960, outer = 1320, col = "^xD35400", label = "Variable" }, { inner = 1320, outer = 1680, col = "^x66FFCC", label = "Variable" }, { inner = 1680, outer = 2040, col = "^x2222CC", label = "Variable" }, { inner = 2040, outer = 2400, col = "^xC100FF", label = "Variable" }, - { inner = 2400, outer = 2880, col = "^x0B9300", label = "Variable" }, + { inner = 2400, outer = 2880, col = "^xFFD700", label = "Variable" }, } } diff --git a/src/Modules/Main.lua b/src/Modules/Main.lua index 49f78e6bbc..97d23c5a44 100644 --- a/src/Modules/Main.lua +++ b/src/Modules/Main.lua @@ -580,6 +580,9 @@ function main:LoadSettings(ignoreBuild) if node.attrib.edgeSearchHighlight then self.edgeSearchHighlight = node.attrib.edgeSearchHighlight == "true" end + if node.attrib.toHHintDismissed then + self.toHHintDismissed = node.attrib.toHHintDismissed == "true" + end if node.attrib.defaultGemQuality then self.defaultGemQuality = m_min(tonumber(node.attrib.defaultGemQuality) or 0, 23) end @@ -745,6 +748,7 @@ function main:SaveSettings() showTitlebarName = tostring(self.showTitlebarName), betaTest = tostring(self.betaTest), edgeSearchHighlight = tostring(self.edgeSearchHighlight), + toHHintDismissed = tostring(self.toHHintDismissed or false), defaultGemQuality = tostring(self.defaultGemQuality or 0), defaultCharLevel = tostring(self.defaultCharLevel or 1), defaultItemAffixQuality = tostring(self.defaultItemAffixQuality or 0.5),