Skip to content

Commit f850d16

Browse files
committed
Implement talent tree path restrictions (aka edge requirements)
1 parent 4d2d406 commit f850d16

File tree

2 files changed

+134
-47
lines changed

2 files changed

+134
-47
lines changed

TalentViewer.lua

Lines changed: 130 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -102,59 +102,129 @@ do
102102

103103
local emptyTable = {}
104104

105-
function TalentViewer_ClassTalentTalentsTabMixin:GetAndCacheNodeInfo(nodeID)
106-
local nodeInfo = LibTalentTree:GetLibNodeInfo(TalentViewer.treeId, nodeID)
107-
if not nodeInfo then nodeInfo = LibTalentTree:GetNodeInfo(TalentViewer.treeId, nodeID) end
108-
if nodeInfo.ID ~= nodeID then return nil end
109-
local isGranted = LibTalentTree:IsNodeGrantedForSpec(TalentViewer.selectedSpecId, nodeID)
110-
local isChoiceNode = #nodeInfo.entryIDs > 1
111-
local selectedEntryId = isChoiceNode and TalentViewer:GetSelectedEntryId(nodeID) or nil
112-
113-
local meetsEdgeRequirements = true
114-
local meetsGateRequirements = true
115-
if not TalentViewer.db.ignoreRestrictions then
116-
for _, conditionId in ipairs(nodeInfo.conditionIDs) do
117-
local condInfo = self:GetAndCacheCondInfo(conditionId)
118-
if condInfo.isGate and not condInfo.isMet then meetsGateRequirements = false end
105+
local nodeEdgesCache = {}
106+
local function getNodeEdges(nodeID)
107+
if not nodeEdgesCache[nodeID] then
108+
nodeEdgesCache[nodeID] = LibTalentTree:GetNodeEdges(TalentViewer.treeId, nodeID) or emptyTable
109+
end
110+
return nodeEdgesCache[nodeID]
111+
end
112+
113+
local incomingNodeEdgesCache = {}
114+
local function getIncomingNodeEdges(nodeID)
115+
local function getIncomingNodeEdgesCallback(nodeID)
116+
local incomingEdges = {}
117+
for _, treeNodeId in ipairs(C_Traits.GetTreeNodes(TalentViewer.treeId)) do
118+
local edges = getNodeEdges(treeNodeId)
119+
for _, edge in ipairs(edges) do
120+
if edge.targetNode == nodeID then
121+
table.insert(incomingEdges, treeNodeId)
122+
end
123+
end
119124
end
125+
return incomingEdges
120126
end
121127

122-
local isAvailable = meetsEdgeRequirements and meetsGateRequirements
128+
return GetOrCreateTableEntryByCallback(incomingNodeEdgesCache, nodeID, getIncomingNodeEdgesCallback)
129+
end
123130

124-
nodeInfo.activeRank = isGranted
125-
and nodeInfo.maxRanks
126-
or ((isChoiceNode and selectedEntryId and 1) or TalentViewer:GetActiveRank(nodeID))
127-
nodeInfo.currentRank = nodeInfo.activeRank
128-
nodeInfo.ranksPurchased = not isGranted and nodeInfo.currentRank or 0
129-
nodeInfo.isAvailable = isAvailable
130-
nodeInfo.canPurchaseRank = isAvailable and not isGranted and ((TalentViewer.purchasedRanks[nodeID] or 0) < nodeInfo.maxRanks)
131-
nodeInfo.canRefundRank = not isGranted and ((TalentViewer.purchasedRanks[nodeID] or 0) > 0)
132-
nodeInfo.meetsEdgeRequirements = meetsEdgeRequirements
133-
134-
for _, edge in ipairs(nodeInfo.visibleEdges) do
135-
edge.isActive = nodeInfo.activeRank == nodeInfo.maxRanks
131+
function TalentViewer_ClassTalentTalentsTabMixin:MarkEdgeRequirementCacheDirty(nodeID)
132+
local edges = getNodeEdges(nodeID)
133+
for _, edge in ipairs(edges) do
134+
self.edgeRequirementsCache[edge.targetNode] = nil
136135
end
136+
end
137137

138-
if #nodeInfo.entryIDs > 1 then
139-
local entryIndex
140-
for i, entryId in ipairs(nodeInfo.entryIDs) do
141-
if entryId == selectedEntryId then
142-
entryIndex = i
143-
break
138+
function TalentViewer_ClassTalentTalentsTabMixin:MeetsEdgeRequirements(nodeID)
139+
local function EdgeRequirementCallback(nodeID)
140+
local incomingEdges = getIncomingNodeEdges(nodeID)
141+
local hasActiveIncomingEdge = false
142+
local hasInactiveIncomingEdge = false
143+
for _, incomingNodeId in ipairs(incomingEdges) do
144+
local nodeInfo = LibTalentTree:GetLibNodeInfo(TalentViewer.treeId, incomingNodeId)
145+
if not nodeInfo then nodeInfo = LibTalentTree:GetNodeInfo(TalentViewer.treeId, incomingNodeId) end
146+
if nodeInfo and LibTalentTree:IsNodeVisibleForSpec(TalentViewer.selectedSpecId, incomingNodeId) then
147+
local isGranted = LibTalentTree:IsNodeGrantedForSpec(TalentViewer.selectedSpecId, incomingNodeId)
148+
local isChoiceNode = #nodeInfo.entryIDs > 1
149+
local selectedEntryId = isChoiceNode and TalentViewer:GetSelectedEntryId(incomingNodeId) or nil
150+
local activeRank = isGranted
151+
and nodeInfo.maxRanks
152+
or ((isChoiceNode and selectedEntryId and 1) or TalentViewer:GetActiveRank(incomingNodeId))
153+
local isEdgeActive = activeRank == nodeInfo.maxRanks
154+
155+
if not isEdgeActive then
156+
hasInactiveIncomingEdge = true
157+
else
158+
hasActiveIncomingEdge = true
159+
end
144160
end
145161
end
146-
nodeInfo.activeEntry = entryIndex and { entryID = nodeInfo.entryIDs[entryIndex], rank = nodeInfo.activeRank } or emptyTable
147-
else
148-
nodeInfo.activeEntry = { entryID = nodeInfo.entryIDs[1], rank = nodeInfo.activeRank }
162+
163+
return not hasInactiveIncomingEdge or hasActiveIncomingEdge
149164
end
150165

151-
nodeInfo.isVisible = LibTalentTree:IsNodeVisibleForSpec(TalentViewer.selectedSpecId, nodeID)
166+
return GetOrCreateTableEntryByCallback(self.edgeRequirementsCache, nodeID, EdgeRequirementCallback)
167+
end
168+
169+
function TalentViewer_ClassTalentTalentsTabMixin:GetAndCacheNodeInfo(nodeID)
170+
local function GetNodeInfoCallback(nodeID)
171+
local nodeInfo = LibTalentTree:GetLibNodeInfo(TalentViewer.treeId, nodeID)
172+
if not nodeInfo then nodeInfo = LibTalentTree:GetNodeInfo(TalentViewer.treeId, nodeID) end
173+
if nodeInfo.ID ~= nodeID then return nil end
174+
local isGranted = LibTalentTree:IsNodeGrantedForSpec(TalentViewer.selectedSpecId, nodeID)
175+
local isChoiceNode = #nodeInfo.entryIDs > 1
176+
local selectedEntryId = isChoiceNode and TalentViewer:GetSelectedEntryId(nodeID) or nil
177+
178+
local meetsEdgeRequirements = TalentViewer.db.ignoreRestrictions or self:MeetsEdgeRequirements(nodeID)
179+
local meetsGateRequirements = true
180+
if not TalentViewer.db.ignoreRestrictions then
181+
for _, conditionId in ipairs(nodeInfo.conditionIDs) do
182+
local condInfo = self:GetAndCacheCondInfo(conditionId)
183+
if condInfo.isGate and not condInfo.isMet then meetsGateRequirements = false end
184+
end
185+
end
186+
187+
local isAvailable = meetsGateRequirements
152188

153-
return nodeInfo
189+
nodeInfo.activeRank = isGranted
190+
and nodeInfo.maxRanks
191+
or ((isChoiceNode and selectedEntryId and 1) or TalentViewer:GetActiveRank(nodeID))
192+
nodeInfo.currentRank = nodeInfo.activeRank
193+
nodeInfo.ranksPurchased = not isGranted and nodeInfo.currentRank or 0
194+
nodeInfo.isAvailable = isAvailable
195+
nodeInfo.canPurchaseRank = isAvailable and meetsEdgeRequirements and not isGranted and ((TalentViewer.purchasedRanks[nodeID] or 0) < nodeInfo.maxRanks)
196+
nodeInfo.canRefundRank = not isGranted
197+
nodeInfo.meetsEdgeRequirements = meetsEdgeRequirements
198+
199+
for _, edge in ipairs(nodeInfo.visibleEdges) do
200+
edge.isActive = nodeInfo.activeRank == nodeInfo.maxRanks
201+
end
202+
203+
if #nodeInfo.entryIDs > 1 then
204+
local entryIndex
205+
for i, entryId in ipairs(nodeInfo.entryIDs) do
206+
if entryId == selectedEntryId then
207+
entryIndex = i
208+
break
209+
end
210+
end
211+
nodeInfo.activeEntry = entryIndex and { entryID = nodeInfo.entryIDs[entryIndex], rank = nodeInfo.activeRank } or nil
212+
else
213+
nodeInfo.activeEntry = { entryID = nodeInfo.entryIDs[1], rank = nodeInfo.activeRank }
214+
end
215+
if not isChoiceNode and nodeInfo.activeRank ~= nodeInfo.maxRanks then
216+
nodeInfo.nextEntry = { entryID = nodeInfo.entryIDs[1], rank = nodeInfo.activeRank + 1 }
217+
end
218+
219+
nodeInfo.isVisible = LibTalentTree:IsNodeVisibleForSpec(TalentViewer.selectedSpecId, nodeID)
220+
221+
return nodeInfo
222+
end
223+
return GetOrCreateTableEntryByCallback(self.nodeInfoCache, nodeID, GetNodeInfoCallback);
154224
end
155225

156226
function TalentViewer_ClassTalentTalentsTabMixin:GetAndCacheCondInfo(condID)
157-
local function GetCondInfoCallback()
227+
local function GetCondInfoCallback(condID)
158228
local condInfo = C_Traits.GetConditionInfo(C_ClassTalents.GetActiveConfigID(), condID)
159229
if condInfo.isGate then
160230
local gates = LibTalentTree:GetGates(self:GetSpecID())
@@ -203,10 +273,15 @@ do
203273
end
204274
end
205275
end
276+
function talentButton:CanRefundRank()
277+
-- remove this method override if/when "cascaded refunds" are implemented
278+
return self.nodeInfo.canRefundRank and self.nodeInfo.ranksPurchased and (self.nodeInfo.ranksPurchased > 0);
279+
end
206280

207281
function talentButton:PurchaseRank()
208282
self:PlaySelectSound();
209283
TalentViewer:PurchaseRank(self:GetNodeID());
284+
talentFrame:MarkEdgeRequirementCacheDirty(self:GetNodeID());
210285
talentFrame:MarkNodeInfoCacheDirty(self:GetNodeID())
211286
talentFrame:UpdateTreeCurrencyInfo()
212287
--self:CheckTooltip();
@@ -215,6 +290,7 @@ do
215290
function talentButton:RefundRank()
216291
self:PlayDeselectSound();
217292
TalentViewer:RefundRank(self:GetNodeID());
293+
talentFrame:MarkEdgeRequirementCacheDirty(self:GetNodeID());
218294
talentFrame:MarkNodeInfoCacheDirty(self:GetNodeID())
219295
talentFrame:UpdateTreeCurrencyInfo()
220296
--self:CheckTooltip();
@@ -225,6 +301,7 @@ do
225301

226302
function TalentViewer_ClassTalentTalentsTabMixin:SetSelection(nodeID, entryID)
227303
TalentViewer:SetSelection(nodeID, entryID)
304+
self:MarkEdgeRequirementCacheDirty(nodeID);
228305
self:MarkNodeInfoCacheDirty(nodeID)
229306
self:UpdateTreeCurrencyInfo()
230307
end
@@ -307,6 +384,8 @@ do
307384
function TalentViewer_ClassTalentTalentsTabMixin:OnLoad()
308385
ClassTalentTalentsTabMixin.OnLoad(self)
309386

387+
self.edgeRequirementsCache = {}
388+
310389
local setAmountOverride = function(self, amount)
311390
local requiredLevel = self.isClassCurrency and 8 or 9;
312391
local spent = (self.isClassCurrency and MAX_LEVEL_CLASS_CURRENCY_CAP or MAX_LEVEL_SPEC_CURRENCY_CAP) - amount;
@@ -447,6 +526,7 @@ function TalentViewer:ResetTree()
447526
wipe(self.purchasedRanks)
448527
wipe(self.selectedEntries)
449528
wipe(self.currencySpending)
529+
wipe(self:GetTalentFrame().edgeRequirementsCache)
450530
TalentViewer_DF.Talents:SetTalentTreeID(self.treeId, true);
451531
TalentViewer_DF.Talents:UpdateClassVisuals()
452532
TalentViewer_DF.Talents:UpdateSpecBackground();
@@ -526,7 +606,7 @@ end
526606
function TalentViewer:OnInitialize()
527607
local defaults = {
528608
ldbOptions = { hide = false },
529-
ignoreRestrictions = true,
609+
ignoreRestrictions = false,
530610
}
531611

532612
TalentTreeViewerDB = TalentTreeViewerDB or {}
@@ -725,12 +805,19 @@ function TalentViewer:InitCheckbox()
725805
self.ignoreRestrictionsCheckbox = TalentViewer_DF.Talents.IgnoreRestrictions
726806
local checkbox = self.ignoreRestrictionsCheckbox
727807
checkbox.Text:SetText('Ignore Restrictions')
728-
checkbox.tooltip = 'Ignore restrictions when selecting talents'
729808
if self.db then
730809
checkbox:SetChecked(self.db.ignoreRestrictions)
731810
end
732-
checkbox:SetScript('OnClick', function(checkbox)
733-
self.db.ignoreRestrictions = checkbox:GetChecked()
811+
checkbox:SetScript('OnEnter', function(self)
812+
GameTooltip:SetOwner(self, "ANCHOR_RIGHT");
813+
GameTooltip_AddNormalLine(GameTooltip, 'Ignore restrictions when selecting talents');
814+
GameTooltip:Show();
815+
end)
816+
checkbox:SetScript('OnLeave', function(self)
817+
GameTooltip:Hide();
818+
end)
819+
checkbox:SetScript('OnClick', function(button)
820+
self.db.ignoreRestrictions = button:GetChecked()
734821
self:GetTalentFrame():UpdateTreeCurrencyInfo()
735822
end)
736823
end

TalentViewerUI.xml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@
193193
</Frame>
194194

195195
<Button parentKey="TV_DropDownButton" inherits="UIPanelButtonTemplate" text="Select another Specialization">
196-
<Size x="220" y="22"/>
196+
<Size x="220" y="25"/>
197197
<Anchors>
198198
<Anchor point="LEFT" relativeKey="$parent.BottomBar" relativePoint="LEFT" x="48" y="0"/>
199199
</Anchors>
@@ -261,7 +261,7 @@
261261
</Button>
262262

263263
<Button parentKey="ImportButton" inherits="UIPanelButtonNoTooltipTemplate, UIButtonTemplate" text="Import">
264-
<Size x="130" y="22"/>
264+
<Size x="130" y="25"/>
265265
<Anchors>
266266
<Anchor point="RIGHT" relativeKey="$parent.BottomBar" relativePoint="CENTER" x="-5" y="3"/>
267267
</Anchors>
@@ -270,7 +270,7 @@
270270
</Scripts>
271271
</Button>
272272
<Button parentKey="ExportButton" inherits="UIPanelButtonNoTooltipTemplate, UIButtonTemplate" text="Export">
273-
<Size x="130" y="22"/>
273+
<Size x="130" y="25"/>
274274
<Anchors>
275275
<Anchor point="LEFT" relativeKey="$parent.BottomBar" relativePoint="CENTER" x="5" y="3"/>
276276
</Anchors>
@@ -279,7 +279,7 @@
279279
</Scripts>
280280
</Button>
281281
<CheckButton parentKey="IgnoreRestrictions" inherits="UICheckButtonTemplate">
282-
<Size x="22" y="22"/>
282+
<Size x="25" y="25"/>
283283
<Anchors>
284284
<Anchor point="RIGHT" relativeKey="$parent.ImportButton" relativePoint="LEFT" x="-110"/>
285285
</Anchors>

0 commit comments

Comments
 (0)