From ec7964f9211a4de90cfdd6a8afac85575b3041e6 Mon Sep 17 00:00:00 2001 From: Jacul Date: Sat, 26 Sep 2026 11:28:45 -0400 Subject: [PATCH] Add support for Baryanic Leylines notable Apply the increased radius to non-unique Time-Lost jewels in calculations and simulated node allocations. Update tree rings, node tooltips, and socket previews to reflect the effective radius, including comparisons between specs with different Leylines allocations. Add tests for the increased radius in calculations and hover previews, and for jewels it should not affect. --- spec/System/TestPassiveSpec_spec.lua | 90 ++++++++++++++++++++++++++++ src/Classes/PassiveSpec.lua | 11 ++++ src/Classes/PassiveTreeView.lua | 25 +++++--- src/Data/ModCache.lua | 2 +- src/Modules/CalcSetup.lua | 10 +++- src/Modules/Data.lua | 23 +++++++ src/Modules/ItemSlotHelper.lua | 9 +++ src/Modules/ModParser.lua | 1 + 8 files changed, 160 insertions(+), 11 deletions(-) diff --git a/spec/System/TestPassiveSpec_spec.lua b/spec/System/TestPassiveSpec_spec.lua index 81c8172e1e..caa4e30ddf 100644 --- a/spec/System/TestPassiveSpec_spec.lua +++ b/spec/System/TestPassiveSpec_spec.lua @@ -220,6 +220,96 @@ Item Level: 80 assert.are.equals(2, build.calcsTab.mainEnv.weaponSetEnvs[2].weaponSet) end) + local function selectLeylinesAscendancy(spec) + -- Leylines only counts for its own ascendancy, and changing class resets the + -- tree, so this must run before anything else is allocated + local ascend = spec.tree.ascendNameMap["Disciple of Varashta"] + spec:SelectClass(ascend.classId) + spec:SelectAscendClass(ascend.ascendClassId) + return select(2, assert(findNodeByName(spec, "Baryanic Leylines"))) + end + + local function socketSmallRadiusJewel(rarity, base) + local nodeId, node = firstNormalJewelSocket(build.spec) + build.spec:AllocNode(node) + local jewel = socketJewel(nodeId, table.concat({ + "Rarity: " .. rarity, + "Test Jewel", + base, + "--------", + "Radius: Small", + "--------", + "Item Level: 80", + "--------", + "+10 to Intelligence", + }, "\n")) + runCallback("OnFrame") + return nodeId, node, jewel + end + + local function radiusJewelNodeCount(env, socketNodeId) + for _, radiusJewel in ipairs(env.radiusJewelList) do + if radiusJewel.nodeId == socketNodeId then + local count = 0 + for _ in pairs(radiusJewel.nodes) do + count = count + 1 + end + return count + end + end + end + + local function nodesInRadiusCount(node, radiusIndex) + local count = 0 + for _ in pairs(node.nodesInRadius[radiusIndex]) do + count = count + 1 + end + return count + end + + it("Baryanic Leylines increases the radius of non-unique Time-Lost jewels", function() + local leylines = selectLeylinesAscendancy(build.spec) + local socketNodeId, socketNode, jewel = socketSmallRadiusJewel("RARE", "Time-Lost Sapphire") + local baseCount = nodesInRadiusCount(socketNode, jewel.jewelRadiusIndex) + local increasedCount = nodesInRadiusCount(socketNode, data.timeLostJewelIncreasedRadiusIndex[jewel.jewelRadiusIndex]) + assert.True(increasedCount > baseCount) + assert.are.equals(baseCount, radiusJewelNodeCount(build.calcsTab.mainEnv, socketNodeId)) + + build.spec:AllocNode(leylines) + build.buildFlag = true + runCallback("OnFrame") + + assert.are.equals(increasedCount, radiusJewelNodeCount(build.calcsTab.mainEnv, socketNodeId)) + end) + + it("Baryanic Leylines hover previews use the radius the jewel would have", function() + local leylines = selectLeylinesAscendancy(build.spec) + local socketNodeId, socketNode, jewel = socketSmallRadiusJewel("RARE", "Time-Lost Sapphire") + local baseCount = nodesInRadiusCount(socketNode, jewel.jewelRadiusIndex) + local increasedCount = nodesInRadiusCount(socketNode, data.timeLostJewelIncreasedRadiusIndex[jewel.jewelRadiusIndex]) + + local env = build.calcsTab.calcs.initEnv(build, "CALCULATOR", { addNodes = { [leylines] = true } }) + assert.are.equals(increasedCount, radiusJewelNodeCount(env, socketNodeId)) + + build.spec:AllocNode(leylines) + build.buildFlag = true + runCallback("OnFrame") + + env = build.calcsTab.calcs.initEnv(build, "CALCULATOR", { removeNodes = { [leylines] = true } }) + assert.are.equals(baseCount, radiusJewelNodeCount(env, socketNodeId)) + end) + + it("Baryanic Leylines does not change the radius of unique or non-Time-Lost jewels", function() + for _, jewelType in ipairs({ { "UNIQUE", "Time-Lost Sapphire" }, { "RARE", "Sapphire" } }) do + newBuild() + local leylines = selectLeylinesAscendancy(build.spec) + build.spec:AllocNode(leylines) + local socketNodeId, socketNode, jewel = socketSmallRadiusJewel(jewelType[1], jewelType[2]) + + assert.are.equals(nodesInRadiusCount(socketNode, jewel.jewelRadiusIndex), radiusJewelNodeCount(build.calcsTab.mainEnv, socketNodeId), jewelType[1] .. " " .. jewelType[2]) + end + end) + it("does not apply jewel socket passive skill effect to jewels in item-granted Zarokh's Gift", function() local normalNodeId, normalNode = firstNormalJewelSocket(build.spec) build.spec:AllocNode(normalNode) diff --git a/src/Classes/PassiveSpec.lua b/src/Classes/PassiveSpec.lua index 154ddf2bbe..fd432a4762 100644 --- a/src/Classes/PassiveSpec.lua +++ b/src/Classes/PassiveSpec.lua @@ -1471,6 +1471,13 @@ function PassiveSpecClass:BuildNodePathsToRootNodes(roots) ::continueBuildPath:: end end + +-- Returns the radius index a jewel actually uses in this spec, accounting for +-- "Non-Unique Time-Lost Jewels have 40% increased radius" (Baryanic Leylines) +function PassiveSpecClass:GetJewelRadiusIndex(item) + return data.getTimeLostJewelRadiusIndex(item, self.hasTimeLostJewelRadiusIncrease) +end + -- Rebuilds dependencies and paths for all nodes function PassiveSpecClass:BuildAllDependsAndPaths() -- This table will keep track of which nodes have been visited during each path-finding attempt @@ -1493,6 +1500,7 @@ function PassiveSpecClass:BuildAllDependsAndPaths() end end wipeTable(intuitiveLeapLikeNodes) + self.hasTimeLostJewelRadiusIncrease = false for id, node in pairs(self.allocNodes) do if node.ascendancyName then -- avoid processing potentially replaceable nodes self.tree:ProcessStats(node) @@ -1501,6 +1509,9 @@ function PassiveSpecClass:BuildAllDependsAndPaths() t_insert(intuitiveLeapLikeNodes, radius) end end + if node.modList:Sum("INC", nil, "NonUniqueTimeLostJewelRadius") > 0 then + self.hasTimeLostJewelRadiusIncrease = true + end processed[id] = true end end diff --git a/src/Classes/PassiveTreeView.lua b/src/Classes/PassiveTreeView.lua index 8449966830..01f7262600 100644 --- a/src/Classes/PassiveTreeView.lua +++ b/src/Classes/PassiveTreeView.lua @@ -1154,8 +1154,10 @@ function PassiveTreeViewClass:Draw(build, viewPort, inputEvents) end else -- Jewel in socket is not Thread of Hope or similar + -- Use the increased radii instead of the base ones if the socketed jewel benefits from them + local isIncreased = jewel and build.spec:GetJewelRadiusIndex(jewel) ~= jewel.jewelRadiusIndex for index, data in ipairs(build.data.jewelRadius) do - if hoverNode.nodesInRadius[index][node.id] then + if (not data.increased) == (not isIncreased) and hoverNode.nodesInRadius[index][node.id] then -- Draw normal jewel radii if data.inner == 0 then SetDrawColor(data.col) @@ -1277,8 +1279,8 @@ 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 function drawJewelRadius(jewel, scrX, scrY, tint, jewelSpec) + local radData = build.data.jewelRadius[jewelSpec:GetJewelRadiusIndex(jewel)] local outerSize = radData.outer * data.gameConstants["PassiveTreeJewelDistanceMultiplier"] * scale local innerSize = radData.inner * data.gameConstants["PassiveTreeJewelDistanceMultiplier"] * scale * 1.06 SetDrawColor(tint[1], tint[2], tint[3], tint[4]) @@ -1326,6 +1328,9 @@ function PassiveTreeViewClass:Draw(build, viewPort, inputEvents) if node == hoverNode then local effectiveJewel = jewel or cJewel local isThreadOfHope = effectiveJewel and effectiveJewel.jewelRadiusLabel == "Variable" + -- Preview the increased radii instead of the base ones if the socketed jewel benefits from them + local jewelSpec = jewel and spec or self.compareSpec + local isIncreased = effectiveJewel and jewelSpec and jewelSpec:GetJewelRadiusIndex(effectiveJewel) ~= effectiveJewel.jewelRadiusIndex for _, radData in ipairs(build.data.jewelRadius) do local outerSize = radData.outer * data.gameConstants["PassiveTreeJewelDistanceMultiplier"] * scale local innerSize = radData.inner * data.gameConstants["PassiveTreeJewelDistanceMultiplier"] * scale @@ -1338,7 +1343,7 @@ function PassiveTreeViewClass:Draw(build, viewPort, inputEvents) end else -- Standard jewel: draw the full-disc radii (inner == 0) - if innerSize == 0 then + if innerSize == 0 and (not radData.increased) == (not isIncreased) then SetDrawColor(radData.col) DrawImage(self.ring, scrX - outerSize, scrY - outerSize, outerSize * 2, outerSize * 2) end @@ -1349,12 +1354,16 @@ function PassiveTreeViewClass:Draw(build, viewPort, inputEvents) local pHasRadius = jewel and jewel.jewelRadiusIndex local cHasRadius = cJewel and cJewel.jewelRadiusIndex local sameJewel = compareJewelsEqual(jewel, cJewel) + if sameJewel and pHasRadius and cHasRadius then + -- Identical jewels can have different radii when only one spec allocates Baryanic Leylines. + sameJewel = spec:GetJewelRadiusIndex(jewel) == self.compareSpec:GetJewelRadiusIndex(cJewel) + end 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) + drawJewelRadius(jewel, scrX, scrY, tint, spec) end if cHasRadius and not sameJewel then - drawJewelRadius(cJewel, scrX, scrY, JEWEL_RADIUS_TINT_COMPARE_ONLY) + drawJewelRadius(cJewel, scrX, scrY, JEWEL_RADIUS_TINT_COMPARE_ONLY, self.compareSpec) end end end @@ -1869,7 +1878,9 @@ function PassiveTreeViewClass:AddNodeTooltip(tooltip, node, build, incSmallPassi local isInRadius = false for id, socket in pairs(build.itemsTab.sockets) do if build.itemsTab.activeSocketList and socket.inactive == false or socket.inactive == nil then - isInRadius = isInRadius or (build.spec.nodes[id] and build.spec.nodes[id].nodesInRadius and build.spec.nodes[id].nodesInRadius[4][node.id] ~= nil) + -- index 4 is Very Large; its increased version covers the largest possible Time-Lost radius + local maxRadiusIndex = build.spec.hasTimeLostJewelRadiusIncrease and data.timeLostJewelIncreasedRadiusIndex[4] or 4 + isInRadius = isInRadius or (build.spec.nodes[id] and build.spec.nodes[id].nodesInRadius and build.spec.nodes[id].nodesInRadius[maxRadiusIndex][node.id] ~= nil) if isInRadius then break end end end diff --git a/src/Data/ModCache.lua b/src/Data/ModCache.lua index 701abf6fd2..0068132fca 100644 --- a/src/Data/ModCache.lua +++ b/src/Data/ModCache.lua @@ -8817,7 +8817,7 @@ c["Non-Keystone Passive Skills in Medium Radius of allocated Keystone Passive Sk c["Non-Minion Skills have 50% less Reservation Efficiency"]={{[1]={[1]={neg=true,skillType=6,type="SkillType"},flags=0,keywordFlags=0,name="ReservationEfficiency",type="MORE",value=-50}},nil} c["Non-Unique Life Flasks apply their Effects constantly"]={nil,"Non-Unique Life Flasks apply their Effects constantly "} c["Non-Unique Life Flasks apply their Effects constantly Recovery from Life Flasks cannot be Instant"]={nil,"Non-Unique Life Flasks apply their Effects constantly Recovery from Life Flasks cannot be Instant "} -c["Non-Unique Time-Lost Jewels have 40% increased radius"]={nil,"Non-Unique Time-Lost Jewels have 40% increased radius "} +c["Non-Unique Time-Lost Jewels have 40% increased radius"]={{[1]={flags=0,keywordFlags=0,name="NonUniqueTimeLostJewelRadius",type="INC",value=40}},nil} c["Non-instant Recovery from Mana Flasks also applies to Life"]={nil,"Non-instant Recovery from Mana Flasks also applies to Life "} c["Nova Spells have 20% less Area of Effect"]={{[1]={[1]={skillType=85,type="SkillType"},flags=0,keywordFlags=0,name="AreaOfEffect",type="MORE",value=-20}},nil} c["Oasis"]={{[1]={flags=0,keywordFlags=0,name="Keystone",type="LIST",value="Oasis"},[2]={flags=0,keywordFlags=0,name="Condition:HaveOasis",type="FLAG",value=true}},nil} diff --git a/src/Modules/CalcSetup.lua b/src/Modules/CalcSetup.lua index 76c1bd37ba..19f29fbfca 100644 --- a/src/Modules/CalcSetup.lua +++ b/src/Modules/CalcSetup.lua @@ -1134,6 +1134,9 @@ function calcs.initEnv(build, mode, override, specEnv) local nodesModsList = calcs.buildModListForNodeList(env, env.allocNodes, true, true) env.useAltGemQualityStats = nodesModsList:Flag(nil, "GemlingQuality") local canUseBonded = nodesModsList:Flag(nil, "CanUseBonded") + -- Check the environment's own node list rather than the spec's allocation, so that + -- simulated allocations (e.g. node hover tooltips) see the radius change too + local timeLostJewelRadiusIncrease = nodesModsList:Sum("INC", nil, "NonUniqueTimeLostJewelRadius") > 0 if allocatedNotableCount and allocatedNotableCount > 0 then modDB:NewMod("Multiplier:AllocatedNotable", "BASE", allocatedNotableCount) @@ -1285,22 +1288,23 @@ function calcs.initEnv(build, mode, override, specEnv) end if item and not (node and node.sinister) and ( item.jewelRadiusIndex or (override and override.extraJewelFuncs and #override.extraJewelFuncs > 0) ) then -- Jewel has a radius, add it to the list + local radiusIndex = data.getTimeLostJewelRadiusIndex(item, timeLostJewelRadiusIncrease) local funcList = (item.jewelData and item.jewelData.funcList) or { { type = "Self", func = defaultRadiusJewelFunc } } for _, func in ipairs(funcList) do t_insert(env.radiusJewelList, { - nodes = node.nodesInRadius and node.nodesInRadius[item.jewelRadiusIndex] or { }, + nodes = node.nodesInRadius and node.nodesInRadius[radiusIndex] or { }, func = func.func, type = func.type, item = item, nodeId = slot.nodeId, - attributes = node.attributesInRadius and node.attributesInRadius[item.jewelRadiusIndex] or { }, + attributes = node.attributesInRadius and node.attributesInRadius[radiusIndex] or { }, data = { }, -- store this to compare with cache later jewelHash = getHashFromString(item.modSource..item.raw) }) if func.type ~= "Self" and node.nodesInRadius then -- Add nearby unallocated nodes to the extra node list - for nodeId, node in pairs(node.nodesInRadius[item.jewelRadiusIndex]) do + for nodeId, node in pairs(node.nodesInRadius[radiusIndex]) do if not env.allocNodes[nodeId] then env.extraRadiusNodeList[nodeId] = env.spec.nodes[nodeId] end diff --git a/src/Modules/Data.lua b/src/Modules/Data.lua index 90546be5bf..79cc4e09b5 100644 --- a/src/Modules/Data.lua +++ b/src/Modules/Data.lua @@ -680,9 +680,32 @@ data.jewelRadii = { { inner = 1400, outer = 1700, col = "^xFFCC00", label = "Variable" }, { inner = 1650, outer = 1950, col = "^xFF6600", label = "Variable" }, { inner = 1800, outer = 2100, col = "^x0099FF", label = "Variable" }, + + -- 40% increased versions of the four base radii, used for non-Unique Time-Lost + -- jewels when "Non-Unique Time-Lost Jewels have 40% increased radius" is allocated + -- (Baryanic Leylines). `increased` keeps them out of the generic radius previews. + { inner = 0, outer = 1400, col = "^xBB6600", label = "Increased Small", increased = true }, + { inner = 0, outer = 1610, col = "^x66FFCC", label = "Increased Medium", increased = true }, + { inner = 0, outer = 1820, col = "^x2222CC", label = "Increased Large", increased = true }, + { inner = 0, outer = 2100, col = "^xC100FF", label = "Increased Very Large", increased = true }, } } +-- Maps a Time-Lost jewel's base radius index to its 40% increased counterpart above +data.timeLostJewelIncreasedRadiusIndex = { [1] = 13, [2] = 14, [3] = 15, [4] = 16 } + +-- Returns the radius index a jewel uses given whether the Time-Lost radius increase +-- (Baryanic Leylines) applies; only non-unique Time-Lost jewels are remapped +data.getTimeLostJewelRadiusIndex = function(item, timeLostJewelRadiusIncrease) + local radiusIndex = item.jewelRadiusIndex + if radiusIndex and timeLostJewelRadiusIncrease + and item.rarity ~= "UNIQUE" and item.rarity ~= "RELIC" + and item.baseName and item.baseName:find("Time%-Lost") then + return data.timeLostJewelIncreasedRadiusIndex[radiusIndex] or radiusIndex + end + return radiusIndex +end + data.jewelRadius = data.setJewelRadiiGlobally(latestTreeVersion) -- Stat descriptions diff --git a/src/Modules/ItemSlotHelper.lua b/src/Modules/ItemSlotHelper.lua index 4d9524c9ea..c051233492 100644 --- a/src/Modules/ItemSlotHelper.lua +++ b/src/Modules/ItemSlotHelper.lua @@ -18,6 +18,15 @@ function M.DrawViewer(itemsTab, nodeId, x, y, w, h) local viewer = itemsTab.socketViewer viewer.zoom = 17 + -- Zoom out if needed so the socketed jewel's radius ring (including the Time-Lost + -- radius increase from Baryanic Leylines) fits inside the preview: the ring's + -- diameter must fit min(w,h), with ~5% margin so the ring stroke isn't clipped + local _, jewel = itemsTab:GetSocketAndJewelForNodeID(nodeId) + if jewel and jewel.jewelRadiusIndex then + local radData = data.jewelRadius[itemsTab.build.spec:GetJewelRadiusIndex(jewel)] + local ringSize = radData.outer * data.gameConstants["PassiveTreeJewelDistanceMultiplier"] + viewer.zoom = math.min(viewer.zoom, itemsTab.build.spec.tree.size / (2 * ringSize) / 1.05) + end local viewPortSize = math.min(w, h) local scale = itemsTab.build.spec.tree.size / (viewPortSize * viewer.zoom) diff --git a/src/Modules/ModParser.lua b/src/Modules/ModParser.lua index 6d8d909dcb..e8b5dd42ac 100644 --- a/src/Modules/ModParser.lua +++ b/src/Modules/ModParser.lua @@ -5605,6 +5605,7 @@ local specialModList = { ["upgrades radius to medium"] = { mod("JewelData", "LIST", { key = "timeLostJewelRadiusOverride", value = 2 })}, ["upgrades radius to large"] = { mod("JewelData", "LIST", { key = "timeLostJewelRadiusOverride", value = 3 })}, ["upgrades radius to very large"] = { mod("JewelData", "LIST", { key = "timeLostJewelRadiusOverride", value = 4 })}, + ["non%-unique time%-lost jewels have (%d+)%% increased radius"] = function(num) return { mod("NonUniqueTimeLostJewelRadius", "INC", num) } end, ["primordial"] = { mod("Multiplier:PrimordialItem", "BASE", 1) }, ["spectres have a base duration of (%d+) seconds"] = { mod("SkillData", "LIST", { key = "duration", value = 6 }, { type = "SkillName", skillName = "Raise Spectre", includeTransfigured = true }) }, ["flasks applied to you have (%d+)%% increased effect"] = function(num) return { mod("FlaskEffect", "INC", num, { type = "ActorCondition", actor = "player"}) } end,