Skip to content

Commit a9e37be

Browse files
arthurweryLocalIdentity
andauthored
Add ascendancy click switching with connect-path option (#9292)
* Add click-to-switch functionality for ascendancies and bloodlines - Click any ascendancy node to automatically switch to that ascendancy - Same-class switching (e.g., Juggernaut → Berserker): always allowed - Cross-class switching: allowed if no regular points allocated or tree is connected to target class - Bloodline switching: always allowed (independent of base class and tree) - Show warning when trying to cross-class switch with points but no connection * Optimizations and auto pathing -Optimizations -Connect path option when trying to swap to unconnected class. * fix: keep existing allocations when auto-connecting classes * Fix pathing + Title update The pathing code didn't check to make sure it was linked to a node that was connected to the start Also changing Ascendancies was not updating the window title with the new ascendancy --------- Co-authored-by: LocalIdentity <[email protected]>
1 parent 2e690c7 commit a9e37be

File tree

4 files changed

+238
-17
lines changed

4 files changed

+238
-17
lines changed

src/Classes/PassiveSpec.lua

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,80 @@ function PassiveSpecClass:IsClassConnected(classId)
663663
return false
664664
end
665665

666+
-- Find and allocate the shortest path to connect to a target class's starting node
667+
function PassiveSpecClass:ConnectToClass(classId)
668+
local classData = self.tree.classes[classId]
669+
if not classData then
670+
return false
671+
end
672+
local targetStartNode = self.nodes[classData.startNodeId]
673+
if not targetStartNode then
674+
return false
675+
end
676+
677+
local function isMainTreeNode(node)
678+
return node
679+
and not node.isProxy
680+
and not node.ascendancyName
681+
and node.type ~= "ClassStart"
682+
and node.type ~= "AscendClassStart"
683+
end
684+
685+
local visited = {}
686+
local prev = {}
687+
local queue = { targetStartNode }
688+
visited[targetStartNode] = true
689+
local head = 1
690+
local foundNode = nil
691+
692+
while queue[head] and not foundNode do
693+
local node = queue[head]
694+
head = head + 1
695+
696+
if node ~= targetStartNode and node.alloc and node.connectedToStart and node.type ~= "ClassStart" and node.type ~= "AscendClassStart" then
697+
foundNode = node
698+
break
699+
end
700+
701+
for _, linked in ipairs(node.linked) do
702+
if isMainTreeNode(linked) and not visited[linked] then
703+
visited[linked] = true
704+
prev[linked] = node
705+
queue[#queue + 1] = linked
706+
end
707+
end
708+
end
709+
710+
if not foundNode then
711+
return false
712+
end
713+
714+
local pathBack = {}
715+
local current = foundNode
716+
while current do
717+
t_insert(pathBack, current)
718+
if current == targetStartNode then
719+
break
720+
end
721+
current = prev[current]
722+
end
723+
724+
if pathBack[#pathBack] ~= targetStartNode then
725+
return false
726+
end
727+
728+
local altPath = { pathBack[1] }
729+
for idx = 2, #pathBack - 1 do
730+
altPath[idx] = pathBack[idx]
731+
local node = pathBack[idx]
732+
if not node.alloc then
733+
self:AllocNode(node, altPath)
734+
end
735+
end
736+
737+
return true
738+
end
739+
666740
-- Clear the allocated status of all non-class-start nodes
667741
function PassiveSpecClass:ResetNodes()
668742
for id, node in pairs(self.nodes) do

src/Classes/PassiveTreeView.lua

Lines changed: 119 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -277,14 +277,125 @@ function PassiveTreeViewClass:Draw(build, viewPort, inputEvents)
277277
spec:DeallocNode(hoverNode)
278278
spec:AddUndoState()
279279
build.buildFlag = true
280-
elseif hoverNode.path then
281-
-- Node is unallocated and can be allocated, so allocate it
282-
if hoverNode.type == "Mastery" and hoverNode.masteryEffects then
283-
build.treeTab:OpenMasteryPopup(hoverNode, viewPort)
284-
else
285-
spec:AllocNode(hoverNode, self.tracePath and hoverNode == self.tracePath[#self.tracePath] and self.tracePath)
286-
spec:AddUndoState()
287-
build.buildFlag = true
280+
else
281+
-- Check if the node belongs to a different ascendancy
282+
if hoverNode.ascendancyName then
283+
local isDifferentAscendancy = false
284+
local targetAscendClassId = nil
285+
local targetBaseClassId = nil
286+
local targetBaseClass = nil
287+
288+
-- Check if this is a bloodline (secondary ascendancy) node
289+
if hoverNode.isBloodline and spec.tree.alternate_ascendancies then
290+
local isDifferentBloodline = not spec.curSecondaryAscendClass or hoverNode.ascendancyName ~= spec.curSecondaryAscendClass.id
291+
292+
if isDifferentBloodline then
293+
-- Find the bloodline in alternate_ascendancies
294+
for bloodlineId, bloodlineData in pairs(spec.tree.alternate_ascendancies) do
295+
if bloodlineData.id == hoverNode.ascendancyName then
296+
spec:SelectSecondaryAscendClass(bloodlineId)
297+
spec:AddUndoState()
298+
spec:SetWindowTitleWithBuildClass()
299+
build.buildFlag = true
300+
break
301+
end
302+
end
303+
end
304+
else
305+
-- Regular ascendancy node (not bloodline)
306+
-- Check if it's different from current primary or secondary ascendancy
307+
if spec.curAscendClassId == 0 or hoverNode.ascendancyName ~= spec.curAscendClassBaseName then
308+
if not (spec.curSecondaryAscendClass and hoverNode.ascendancyName == spec.curSecondaryAscendClass.id) then
309+
isDifferentAscendancy = true
310+
end
311+
end
312+
313+
if isDifferentAscendancy then
314+
-- First, check if it's in the current class (same-class switching)
315+
for ascendClassId, ascendClass in pairs(spec.curClass.classes) do
316+
if ascendClass.id == hoverNode.ascendancyName then
317+
targetAscendClassId = ascendClassId
318+
break
319+
end
320+
end
321+
322+
if targetAscendClassId then
323+
-- Same-class switching - always allowed
324+
spec:SelectAscendClass(targetAscendClassId)
325+
spec:AddUndoState()
326+
spec:SetWindowTitleWithBuildClass()
327+
build.buildFlag = true
328+
else
329+
-- Cross-class switching - search all classes
330+
for classId, classData in pairs(spec.tree.classes) do
331+
for ascendClassId, ascendClass in pairs(classData.classes) do
332+
if ascendClass.id == hoverNode.ascendancyName then
333+
targetBaseClassId = classId
334+
targetBaseClass = classData
335+
targetAscendClassId = ascendClassId
336+
break
337+
end
338+
end
339+
if targetBaseClassId then break end
340+
end
341+
342+
if targetBaseClassId then
343+
local used = spec:CountAllocNodes()
344+
local clickedAscendNodeId = hoverNode and hoverNode.id
345+
local function allocateClickedAscendancy()
346+
if not clickedAscendNodeId then
347+
return
348+
end
349+
local targetNode = spec.nodes[clickedAscendNodeId]
350+
if targetNode and not targetNode.alloc then
351+
spec:AllocNode(targetNode)
352+
end
353+
end
354+
355+
-- Allow cross-class switching if: no regular points allocated OR tree is connected to target class
356+
if used == 0 or spec:IsClassConnected(targetBaseClassId) then
357+
spec:SelectClass(targetBaseClassId)
358+
spec:SelectAscendClass(targetAscendClassId)
359+
allocateClickedAscendancy()
360+
spec:AddUndoState()
361+
spec:SetWindowTitleWithBuildClass()
362+
build.buildFlag = true
363+
else
364+
-- Tree has points but isn't connected to target class
365+
main:OpenConfirmPopup("Class Change", "Changing class to "..targetBaseClass.name.." will reset your passive tree.\nThis can be avoided by connecting one of the "..targetBaseClass.name.." starting nodes to your tree.", "Continue", function()
366+
spec:SelectClass(targetBaseClassId)
367+
spec:SelectAscendClass(targetAscendClassId)
368+
allocateClickedAscendancy()
369+
spec:AddUndoState()
370+
spec:SetWindowTitleWithBuildClass()
371+
build.buildFlag = true
372+
end, "Connect Path", function()
373+
if spec:ConnectToClass(targetBaseClassId) then
374+
spec:SelectClass(targetBaseClassId)
375+
spec:SelectAscendClass(targetAscendClassId)
376+
allocateClickedAscendancy()
377+
spec:AddUndoState()
378+
spec:SetWindowTitleWithBuildClass()
379+
build.buildFlag = true
380+
end
381+
end)
382+
return
383+
end
384+
end
385+
end
386+
end
387+
end
388+
end
389+
390+
-- Normal node allocation (non-ascendancy or same ascendancy)
391+
if hoverNode.path and not hoverNode.alloc then
392+
if hoverNode.type == "Mastery" and hoverNode.masteryEffects then
393+
build.treeTab:OpenMasteryPopup(hoverNode, viewPort)
394+
else
395+
spec:AllocNode(hoverNode, self.tracePath and hoverNode == self.tracePath[#self.tracePath] and self.tracePath)
396+
spec:AddUndoState()
397+
build.buildFlag = true
398+
end
288399
end
289400
end
290401
end

src/Modules/Build.lua

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,13 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin
251251
self.spec:AddUndoState()
252252
self.spec:SetWindowTitleWithBuildClass()
253253
self.buildFlag = true
254+
end, "Connect Path", function()
255+
if self.spec:ConnectToClass(value.classId) then
256+
self.spec:SelectClass(value.classId)
257+
self.spec:AddUndoState()
258+
self.spec:SetWindowTitleWithBuildClass()
259+
self.buildFlag = true
260+
end
254261
end)
255262
end
256263
end

src/Modules/Main.lua

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1538,22 +1538,51 @@ function main:OpenMessagePopup(title, msg)
15381538
return self:OpenPopup(m_max(DrawStringWidth(16, "VAR", msg) + 30, 190), 70 + numMsgLines * 16, title, controls, "close")
15391539
end
15401540

1541-
function main:OpenConfirmPopup(title, msg, confirmLabel, onConfirm)
1541+
function main:OpenConfirmPopup(title, msg, confirmLabel, onConfirm, extraLabel, onExtra)
15421542
local controls = { }
15431543
local numMsgLines = 0
15441544
for line in string.gmatch(msg .. "\n", "([^\n]*)\n") do
15451545
t_insert(controls, new("LabelControl", nil, {0, 20 + numMsgLines * 16, 0, 16}, line))
15461546
numMsgLines = numMsgLines + 1
15471547
end
15481548
local confirmWidth = m_max(80, DrawStringWidth(16, "VAR", confirmLabel) + 10)
1549-
controls.confirm = new("ButtonControl", nil, {-5 - m_ceil(confirmWidth/2), 40 + numMsgLines * 16, confirmWidth, 20}, confirmLabel, function()
1550-
main:ClosePopup()
1551-
onConfirm()
1552-
end)
1553-
t_insert(controls, new("ButtonControl", nil, {5 + m_ceil(confirmWidth/2), 40 + numMsgLines * 16, confirmWidth, 20}, "Cancel", function()
1554-
main:ClosePopup()
1555-
end))
1556-
return self:OpenPopup(m_max(DrawStringWidth(16, "VAR", msg) + 30, 190), 70 + numMsgLines * 16, title, controls, "confirm")
1549+
1550+
if extraLabel and onExtra then
1551+
-- Three button layout: Continue (left), Connect Path (center), Cancel (right)
1552+
local extraWidth = m_max(80, DrawStringWidth(16, "VAR", extraLabel) + 10)
1553+
local cancelWidth = 80
1554+
local spacing = 10
1555+
local totalWidth = confirmWidth + extraWidth + cancelWidth + (spacing * 2)
1556+
local leftEdge = -totalWidth / 2
1557+
local buttonY = 40 + numMsgLines * 16
1558+
local function placeButton(width, label, onClick, isConfirm)
1559+
local centerX = leftEdge + width / 2
1560+
local ctrl = new("ButtonControl", nil, {centerX, buttonY, width, 20}, label, function()
1561+
main:ClosePopup()
1562+
onClick()
1563+
end)
1564+
if isConfirm then
1565+
controls.confirm = ctrl
1566+
else
1567+
t_insert(controls, ctrl)
1568+
end
1569+
leftEdge = leftEdge + width + spacing
1570+
end
1571+
placeButton(confirmWidth, confirmLabel, onConfirm, true)
1572+
placeButton(extraWidth, extraLabel, onExtra)
1573+
placeButton(cancelWidth, "Cancel", function() end)
1574+
return self:OpenPopup(m_max(DrawStringWidth(16, "VAR", msg) + 30, totalWidth + 40), 70 + numMsgLines * 16, title, controls, "confirm")
1575+
else
1576+
-- Two button layout (original)
1577+
controls.confirm = new("ButtonControl", nil, {-5 - m_ceil(confirmWidth/2), 40 + numMsgLines * 16, confirmWidth, 20}, confirmLabel, function()
1578+
main:ClosePopup()
1579+
onConfirm()
1580+
end)
1581+
t_insert(controls, new("ButtonControl", nil, {5 + m_ceil(confirmWidth/2), 40 + numMsgLines * 16, confirmWidth, 20}, "Cancel", function()
1582+
main:ClosePopup()
1583+
end))
1584+
return self:OpenPopup(m_max(DrawStringWidth(16, "VAR", msg) + 30, 190), 70 + numMsgLines * 16, title, controls, "confirm")
1585+
end
15571586
end
15581587

15591588
function main:OpenNewFolderPopup(path, onClose)

0 commit comments

Comments
 (0)