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 <localidentity2@gmail.com>
This commit is contained in:
Wery Arthur
2025-11-22 10:33:11 +01:00
committed by GitHub
parent 2e690c7d98
commit a9e37be167
4 changed files with 238 additions and 17 deletions

View File

@@ -663,6 +663,80 @@ function PassiveSpecClass:IsClassConnected(classId)
return false
end
-- Find and allocate the shortest path to connect to a target class's starting node
function PassiveSpecClass:ConnectToClass(classId)
local classData = self.tree.classes[classId]
if not classData then
return false
end
local targetStartNode = self.nodes[classData.startNodeId]
if not targetStartNode then
return false
end
local function isMainTreeNode(node)
return node
and not node.isProxy
and not node.ascendancyName
and node.type ~= "ClassStart"
and node.type ~= "AscendClassStart"
end
local visited = {}
local prev = {}
local queue = { targetStartNode }
visited[targetStartNode] = true
local head = 1
local foundNode = nil
while queue[head] and not foundNode do
local node = queue[head]
head = head + 1
if node ~= targetStartNode and node.alloc and node.connectedToStart and node.type ~= "ClassStart" and node.type ~= "AscendClassStart" then
foundNode = node
break
end
for _, linked in ipairs(node.linked) do
if isMainTreeNode(linked) and not visited[linked] then
visited[linked] = true
prev[linked] = node
queue[#queue + 1] = linked
end
end
end
if not foundNode then
return false
end
local pathBack = {}
local current = foundNode
while current do
t_insert(pathBack, current)
if current == targetStartNode then
break
end
current = prev[current]
end
if pathBack[#pathBack] ~= targetStartNode then
return false
end
local altPath = { pathBack[1] }
for idx = 2, #pathBack - 1 do
altPath[idx] = pathBack[idx]
local node = pathBack[idx]
if not node.alloc then
self:AllocNode(node, altPath)
end
end
return true
end
-- Clear the allocated status of all non-class-start nodes
function PassiveSpecClass:ResetNodes()
for id, node in pairs(self.nodes) do

View File

@@ -277,14 +277,125 @@ function PassiveTreeViewClass:Draw(build, viewPort, inputEvents)
spec:DeallocNode(hoverNode)
spec:AddUndoState()
build.buildFlag = true
elseif hoverNode.path then
-- Node is unallocated and can be allocated, so allocate it
if hoverNode.type == "Mastery" and hoverNode.masteryEffects then
build.treeTab:OpenMasteryPopup(hoverNode, viewPort)
else
spec:AllocNode(hoverNode, self.tracePath and hoverNode == self.tracePath[#self.tracePath] and self.tracePath)
spec:AddUndoState()
build.buildFlag = true
else
-- Check if the node belongs to a different ascendancy
if hoverNode.ascendancyName then
local isDifferentAscendancy = false
local targetAscendClassId = nil
local targetBaseClassId = nil
local targetBaseClass = nil
-- Check if this is a bloodline (secondary ascendancy) node
if hoverNode.isBloodline and spec.tree.alternate_ascendancies then
local isDifferentBloodline = not spec.curSecondaryAscendClass or hoverNode.ascendancyName ~= spec.curSecondaryAscendClass.id
if isDifferentBloodline then
-- Find the bloodline in alternate_ascendancies
for bloodlineId, bloodlineData in pairs(spec.tree.alternate_ascendancies) do
if bloodlineData.id == hoverNode.ascendancyName then
spec:SelectSecondaryAscendClass(bloodlineId)
spec:AddUndoState()
spec:SetWindowTitleWithBuildClass()
build.buildFlag = true
break
end
end
end
else
-- Regular ascendancy node (not bloodline)
-- Check if it's different from current primary or secondary ascendancy
if spec.curAscendClassId == 0 or hoverNode.ascendancyName ~= spec.curAscendClassBaseName then
if not (spec.curSecondaryAscendClass and hoverNode.ascendancyName == spec.curSecondaryAscendClass.id) then
isDifferentAscendancy = true
end
end
if isDifferentAscendancy then
-- First, check if it's in the current class (same-class switching)
for ascendClassId, ascendClass in pairs(spec.curClass.classes) do
if ascendClass.id == hoverNode.ascendancyName then
targetAscendClassId = ascendClassId
break
end
end
if targetAscendClassId then
-- Same-class switching - always allowed
spec:SelectAscendClass(targetAscendClassId)
spec:AddUndoState()
spec:SetWindowTitleWithBuildClass()
build.buildFlag = true
else
-- Cross-class switching - search all classes
for classId, classData in pairs(spec.tree.classes) do
for ascendClassId, ascendClass in pairs(classData.classes) do
if ascendClass.id == hoverNode.ascendancyName then
targetBaseClassId = classId
targetBaseClass = classData
targetAscendClassId = ascendClassId
break
end
end
if targetBaseClassId then break end
end
if targetBaseClassId then
local used = spec:CountAllocNodes()
local clickedAscendNodeId = hoverNode and hoverNode.id
local function allocateClickedAscendancy()
if not clickedAscendNodeId then
return
end
local targetNode = spec.nodes[clickedAscendNodeId]
if targetNode and not targetNode.alloc then
spec:AllocNode(targetNode)
end
end
-- Allow cross-class switching if: no regular points allocated OR tree is connected to target class
if used == 0 or spec:IsClassConnected(targetBaseClassId) then
spec:SelectClass(targetBaseClassId)
spec:SelectAscendClass(targetAscendClassId)
allocateClickedAscendancy()
spec:AddUndoState()
spec:SetWindowTitleWithBuildClass()
build.buildFlag = true
else
-- Tree has points but isn't connected to target class
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()
spec:SelectClass(targetBaseClassId)
spec:SelectAscendClass(targetAscendClassId)
allocateClickedAscendancy()
spec:AddUndoState()
spec:SetWindowTitleWithBuildClass()
build.buildFlag = true
end, "Connect Path", function()
if spec:ConnectToClass(targetBaseClassId) then
spec:SelectClass(targetBaseClassId)
spec:SelectAscendClass(targetAscendClassId)
allocateClickedAscendancy()
spec:AddUndoState()
spec:SetWindowTitleWithBuildClass()
build.buildFlag = true
end
end)
return
end
end
end
end
end
end
-- Normal node allocation (non-ascendancy or same ascendancy)
if hoverNode.path and not hoverNode.alloc then
if hoverNode.type == "Mastery" and hoverNode.masteryEffects then
build.treeTab:OpenMasteryPopup(hoverNode, viewPort)
else
spec:AllocNode(hoverNode, self.tracePath and hoverNode == self.tracePath[#self.tracePath] and self.tracePath)
spec:AddUndoState()
build.buildFlag = true
end
end
end
end

View File

@@ -251,6 +251,13 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin
self.spec:AddUndoState()
self.spec:SetWindowTitleWithBuildClass()
self.buildFlag = true
end, "Connect Path", function()
if self.spec:ConnectToClass(value.classId) then
self.spec:SelectClass(value.classId)
self.spec:AddUndoState()
self.spec:SetWindowTitleWithBuildClass()
self.buildFlag = true
end
end)
end
end

View File

@@ -1538,7 +1538,7 @@ function main:OpenMessagePopup(title, msg)
return self:OpenPopup(m_max(DrawStringWidth(16, "VAR", msg) + 30, 190), 70 + numMsgLines * 16, title, controls, "close")
end
function main:OpenConfirmPopup(title, msg, confirmLabel, onConfirm)
function main:OpenConfirmPopup(title, msg, confirmLabel, onConfirm, extraLabel, onExtra)
local controls = { }
local numMsgLines = 0
for line in string.gmatch(msg .. "\n", "([^\n]*)\n") do
@@ -1546,14 +1546,43 @@ function main:OpenConfirmPopup(title, msg, confirmLabel, onConfirm)
numMsgLines = numMsgLines + 1
end
local confirmWidth = m_max(80, DrawStringWidth(16, "VAR", confirmLabel) + 10)
controls.confirm = new("ButtonControl", nil, {-5 - m_ceil(confirmWidth/2), 40 + numMsgLines * 16, confirmWidth, 20}, confirmLabel, function()
main:ClosePopup()
onConfirm()
end)
t_insert(controls, new("ButtonControl", nil, {5 + m_ceil(confirmWidth/2), 40 + numMsgLines * 16, confirmWidth, 20}, "Cancel", function()
main:ClosePopup()
end))
return self:OpenPopup(m_max(DrawStringWidth(16, "VAR", msg) + 30, 190), 70 + numMsgLines * 16, title, controls, "confirm")
if extraLabel and onExtra then
-- Three button layout: Continue (left), Connect Path (center), Cancel (right)
local extraWidth = m_max(80, DrawStringWidth(16, "VAR", extraLabel) + 10)
local cancelWidth = 80
local spacing = 10
local totalWidth = confirmWidth + extraWidth + cancelWidth + (spacing * 2)
local leftEdge = -totalWidth / 2
local buttonY = 40 + numMsgLines * 16
local function placeButton(width, label, onClick, isConfirm)
local centerX = leftEdge + width / 2
local ctrl = new("ButtonControl", nil, {centerX, buttonY, width, 20}, label, function()
main:ClosePopup()
onClick()
end)
if isConfirm then
controls.confirm = ctrl
else
t_insert(controls, ctrl)
end
leftEdge = leftEdge + width + spacing
end
placeButton(confirmWidth, confirmLabel, onConfirm, true)
placeButton(extraWidth, extraLabel, onExtra)
placeButton(cancelWidth, "Cancel", function() end)
return self:OpenPopup(m_max(DrawStringWidth(16, "VAR", msg) + 30, totalWidth + 40), 70 + numMsgLines * 16, title, controls, "confirm")
else
-- Two button layout (original)
controls.confirm = new("ButtonControl", nil, {-5 - m_ceil(confirmWidth/2), 40 + numMsgLines * 16, confirmWidth, 20}, confirmLabel, function()
main:ClosePopup()
onConfirm()
end)
t_insert(controls, new("ButtonControl", nil, {5 + m_ceil(confirmWidth/2), 40 + numMsgLines * 16, confirmWidth, 20}, "Cancel", function()
main:ClosePopup()
end))
return self:OpenPopup(m_max(DrawStringWidth(16, "VAR", msg) + 30, 190), 70 + numMsgLines * 16, title, controls, "confirm")
end
end
function main:OpenNewFolderPopup(path, onClose)