-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathLibTalentTree-1.0.lua
847 lines (729 loc) · 35.1 KB
/
LibTalentTree-1.0.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
-- the data for LibTalentTree will be loaded (and cached) from blizzard's APIs when the Lib loads
-- @curseforge-project-slug: libtalenttree@
local MAJOR, MINOR = "LibTalentTree-1.0", 24;
--- @class LibTalentTree-1.0
local LibTalentTree = LibStub:NewLibrary(MAJOR, MINOR);
if not LibTalentTree then return end -- No upgrade needed
--- Whether the current game version is compatible with this library. This is generally always true on retail, and always false on classic.
function LibTalentTree:IsCompatible()
return C_ClassTalents and C_ClassTalents.InitializeViewLoadout and true or false;
end
if not C_ClassTalents or not C_ClassTalents.InitializeViewLoadout then
setmetatable(LibTalentTree, {
__index = function()
error('LibTalentTree requires C_ClassTalents.InitializeViewLoadout to be available');
end,
});
return;
end
local MAX_LEVEL = 100; -- seems to not break if set too high, but can break things when set too low
local MAX_SUB_TREE_CURRENCY = 10; -- blizzard incorrectly reports 20 when asking for the maxQuantity of the currency
local HERO_TREE_REQUIRED_LEVEL = 71; -- while `C_ClassTalents.GetHeroTalentSpecsForClassSpec` returns this info, it's not immediately available on initial load
-- taken from ClassTalentUtil.GetVisualsForClassID
local CLASS_OFFSETS = {
[1] = { x = 30, y = 31, }, -- Warrior
[2] = { x = -60, y = -29, }, -- Paladin
[3] = { x = 0, y = -29, }, -- Hunter
[4] = { x = 30, y = -29, }, -- Rogue
[5] = { x = -30, y = -29, }, -- Priest
[6] = { x = 0, y = 1, }, -- DK
[7] = { x = 0, y = 1, }, -- Shaman
[8] = { x = 30, y = -29, }, -- Mage
[9] = { x = 0, y = 1, }, -- Warlock
[10] = { x = 0, y = -29, }, -- Monk
[11] = { x = 30, y = -29, }, -- Druid
[12] = { x = 30, y = -29, }, -- Demon Hunter
[13] = { x = 30, y = -29, }, -- Evoker
};
-- taken from ClassTalentTalentsTabTemplate XML
local BASE_PAN_OFFSET_X = 4;
local BASE_PAN_OFFSET_Y = -30;
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
local function deepCopy(original)
local copy;
if (type(original) == 'table') then
copy = {};
for key, value in next, original, nil do
copy[deepCopy(key)] = deepCopy(value);
end
setmetatable(copy, deepCopy(getmetatable(original)));
else
copy = original;
end
return copy;
end
local function mergeTables(target, source, keyToUse)
local lookup = {};
for _, value in pairs(target) do
if keyToUse then
lookup[value[keyToUse]] = true;
else
lookup[value] = true;
end
end
for _, value in pairs(source) do
if (keyToUse and not lookup[value[keyToUse]]) or (not keyToUse and not lookup[value]) then
table.insert(target, value);
end
end
end
local roundingFactor = 100;
local function round(coordinate)
return math.floor((coordinate / roundingFactor) + 0.5) * roundingFactor;
end
local function getGridLineFromCoordinate(start, spacing, halfwayEnabled, coordinate)
local bucketSpacing = halfwayEnabled and (spacing / 4) or (spacing / 2);
-- breaks out at 25, which is well above the expected max
for testLine = 1, 25, (halfwayEnabled and 0.5 or 1) do
local bucketStart = (start - spacing) + (spacing * testLine) - bucketSpacing;
local bucketEnd = bucketStart + (bucketSpacing * 2);
if coordinate >= bucketStart and coordinate < bucketEnd then
return testLine;
end
end
return nil;
end
LibTalentTree.cacheWarmupRegistery = LibTalentTree.cacheWarmupRegistery or {};
local forceBuildCache;
local cacheWarmedUp = false;
do
local function initCache()
LibTalentTree.cache = {
--- @type table<string, number> # className -> classID
classFileMap = {},
--- @type table<number, number> # specID -> classID
specMap = {},
--- @type table<number, number> # classID -> treeID
classTreeMap = {},
--- @type table<number, number> # nodeID -> treeID
nodeTreeMap = {},
--- @type table<number, number> # entryID -> treeID
entryTreeMap = {},
--- @type table<number, number> # entryID -> nodeID
entryNodeMap = {},
--- @type table<number, table<number, number>> # specID -> {entryIndex -> subTreeID}
specSubTreeMap = {},
--- @type table<number, table<number, boolean>> # subTreeID -> {specID -> true}
subTreeSpecMap = {},
--- @type table<number, number[]> # subTreeID -> nodeID[]
subTreeNodesMap = {},
--- @type table<number, treeCurrencyInfo[]> # treeID -> {currencyIndex|currencyID -> currencyInfo}
treeCurrencyMap = {},
--- @type table<number, libNodeInfo[]> # treeID -> {nodeID -> nodeInfo}
nodeData = {},
--- @type table<number, table<number, gateInfo>> # treeID -> {conditionID -> gateInfo}
gateData = {},
--- @type table<number, entryInfo[]> # treeID -> {entryID -> entryData}
entryData = {},
--- @type table<number, subTreeInfo> # subTreeID -> subTreeInfo
subTreeData = {},
};
for classID = 1, GetNumClasses() do
LibTalentTree.cache.classFileMap[select(2, GetClassInfo(classID))] = classID;
local specID = GetSpecializationInfoForClassID(classID, 1);
LibTalentTree.cache.classTreeMap[classID] = C_ClassTalents.GetTraitTreeForSpec(specID);
end
end
local level = MAX_LEVEL;
local configID = Constants.TraitConsts.VIEW_TRAIT_CONFIG_ID;
local initialSpecs = {
[1] = 1446,
[2] = 1451,
[3] = 1448,
[4] = 1453,
[5] = 1452,
[6] = 1455,
[7] = 1444,
[8] = 1449,
[9] = 1454,
[10] = 1450,
[11] = 1447,
[12] = 1456,
[13] = 1465,
};
local function buildPartialCache(classID)
local cache = LibTalentTree.cache;
local nodes;
local treeID = cache.classTreeMap[classID];
local nodeData = {};
local entryData = {};
local gateData = {};
cache.nodeData[treeID] = nodeData;
cache.entryData[treeID] = entryData;
cache.gateData[treeID] = gateData;
local numSpecs = GetNumSpecializationsForClassID(classID);
for specIndex = 1, (numSpecs + 1) do
local lastSpec = specIndex == (numSpecs + 1);
local specID = GetSpecializationInfoForClassID(classID, specIndex) or initialSpecs[classID];
cache.specMap[specID] = classID;
C_ClassTalents.InitializeViewLoadout(specID, level);
C_ClassTalents.ViewLoadout({});
nodes = nodes or C_Traits.GetTreeNodes(treeID);
local treeCurrencyInfo = C_Traits.GetTreeCurrencyInfo(configID, treeID, true);
local classCurrencyID = treeCurrencyInfo[1].traitCurrencyID;
cache.treeCurrencyMap[treeID] = cache.treeCurrencyMap[treeID] or treeCurrencyInfo;
cache.treeCurrencyMap[treeID][1].isClassCurrency = true;
cache.treeCurrencyMap[treeID][2].isSpecCurrency = true;
for _, currencyInfo in ipairs(treeCurrencyInfo) do
cache.treeCurrencyMap[treeID][currencyInfo.traitCurrencyID] = cache.treeCurrencyMap[treeID][currencyInfo.traitCurrencyID] or currencyInfo;
end
local treeInfo = C_Traits.GetTreeInfo(configID, treeID);
for _, gateInfo in ipairs(treeInfo.gates) do
local conditionID = gateInfo.conditionID;
local conditionInfo = C_Traits.GetConditionInfo(configID, conditionID);
gateData[conditionID] = {
currencyID = conditionInfo.traitCurrencyID,
spentAmountRequired = conditionInfo.spentAmountRequired,
};
end
for _, nodeID in pairs(nodes) do
cache.nodeTreeMap[nodeID] = treeID;
local nodeInfo = C_Traits.GetNodeInfo(configID, nodeID);
nodeData[nodeID] = nodeData[nodeID] or {};
local data = nodeData[nodeID];
data.grantedForSpecs = data.grantedForSpecs or {};
data.grantedForSpecs[specID] = false; -- true check is done only if the node is visible
if nodeInfo.isVisible then
data.posX = nodeInfo.posX;
data.posY = nodeInfo.posY;
data.type = nodeInfo.type;
data.maxRanks = nodeInfo.maxRanks;
data.flags = nodeInfo.flags;
data.entryIDs = nodeInfo.entryIDs;
data.subTreeID = nodeInfo.subTreeID;
data.isSubTreeSelection = nodeInfo.type == Enum.TraitNodeType.SubTreeSelection;
data.visibleEdges = data.visibleEdges or {}
mergeTables(data.visibleEdges, nodeInfo.visibleEdges, 'targetNode');
data.conditionIDs = data.conditionIDs or {}
mergeTables(data.conditionIDs, nodeInfo.conditionIDs);
data.groupIDs = data.groupIDs or {}
mergeTables(data.groupIDs, nodeInfo.groupIDs);
for entryIndex, entryID in pairs(nodeInfo.entryIDs) do
cache.entryTreeMap[entryID] = treeID;
cache.entryNodeMap[entryID] = nodeID;
if not entryData[entryID] then
local entryInfo = C_Traits.GetEntryInfo(configID, entryID);
entryData[entryID] = {
definitionID = entryInfo.definitionID,
type = entryInfo.type,
maxRanks = entryInfo.maxRanks,
subTreeID = entryInfo.subTreeID,
}
if entryInfo.subTreeID then
cache.specSubTreeMap[specID] = cache.specSubTreeMap[specID] or {};
cache.specSubTreeMap[specID][entryIndex] = entryInfo.subTreeID;
cache.subTreeSpecMap[entryInfo.subTreeID] = cache.subTreeSpecMap[entryInfo.subTreeID] or {};
cache.subTreeSpecMap[entryInfo.subTreeID][specID] = true;
-- I previously used C_ClassTalents.GetHeroTalentSpecsForClassSpec, but it returns nil on initial load
-- it's not actually required to retrieve the data though
local subTreeInfo = C_Traits.GetSubTreeInfo(configID, entryInfo.subTreeID);
if subTreeInfo then
subTreeInfo.requiredPlayerLevel = HERO_TREE_REQUIRED_LEVEL;
subTreeInfo.maxCurrency = MAX_SUB_TREE_CURRENCY;
subTreeInfo.isActive = false;
cache.subTreeData[entryInfo.subTreeID] = subTreeInfo;
cache.treeCurrencyMap[treeID][subTreeInfo.traitCurrencyID].subTreeID = entryInfo.subTreeID;
cache.treeCurrencyMap[treeID][subTreeInfo.traitCurrencyID].quantity = MAX_SUB_TREE_CURRENCY;
cache.treeCurrencyMap[treeID][subTreeInfo.traitCurrencyID].maxQuantity = MAX_SUB_TREE_CURRENCY;
end
end
end
end
for _, conditionID in pairs(data.conditionIDs) do
local cInfo = C_Traits.GetConditionInfo(configID, conditionID)
if cInfo and cInfo.isMet and cInfo.ranksGranted and cInfo.ranksGranted > 0 then
data.grantedForSpecs[specID] = true;
end
end
if nil == data.isClassNode then
data.isClassNode = false;
local nodeCost = C_Traits.GetNodeCost(configID, nodeID);
if not next(nodeCost) and data.grantedForSpecs[specID] then
data.isClassNode = true;
end
for _, cost in pairs(nodeCost) do
if cost.ID == classCurrencyID then
data.isClassNode = true;
break;
end
end
end
end
data.visibleForSpecs = data.visibleForSpecs or {};
data.visibleForSpecs[specID] = nodeInfo.isVisible;
if lastSpec then
if not data.posX then
nodeData[nodeID] = nil;
elseif data.subTreeID then
cache.subTreeNodesMap[data.subTreeID] = cache.subTreeNodesMap[data.subTreeID] or {};
table.insert(cache.subTreeNodesMap[data.subTreeID], nodeID);
end
end
end
end
for _, nodeInfo in pairs(nodeData) do
-- some subtree nodes incorrectly suggest they are visible for all specs, so we just correct that
if nodeInfo.subTreeID then
for specID, _ in pairs(nodeInfo.visibleForSpecs) do
nodeInfo.visibleForSpecs[specID] = cache.subTreeSpecMap[nodeInfo.subTreeID][specID] or false;
end
end
end
end
local frame = CreateFrame("Frame");
local function onCacheCompleted()
frame:SetScript("OnUpdate", nil);
forceBuildCache = nil;
cacheWarmedUp = true;
for _, callback in ipairs(LibTalentTree.cacheWarmupRegistery) do
securecallfunction(callback);
end
LibTalentTree.cacheWarmupRegistery = nil;
end
frame.currentClassID = 0;
frame.numClasses = GetNumClasses();
frame:SetScript("OnUpdate", function()
local _, latestMinor = LibStub:GetLibrary(MAJOR);
if latestMinor ~= MINOR then
frame:SetScript("OnUpdate", nil);
return;
end
local classID = frame.currentClassID + 1;
if classID == 1 then
initCache();
elseif classID > frame.numClasses then
onCacheCompleted();
return;
end
frame.currentClassID = classID;
-- buildPartialCache results in a significant amount of pointless taintlog entries when it's set to log level 11
-- so we just disable it temporarily
local backup = C_CVar.GetCVar('taintLog');
if backup and backup == '11' then C_CVar.SetCVar('taintLog', 0); end
buildPartialCache(classID);
if backup and backup == '11' then C_CVar.SetCVar('taintLog', backup); end
end);
forceBuildCache = function()
for classID = frame.currentClassID + 1, frame.numClasses do
buildPartialCache(classID);
end
onCacheCompleted();
end
end
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
--- @public
--- Register a callback to be called when the cache is fully built.
--- If you register the callback after the cache is built, it will be called immediately.
--- Using a function that requires the cache to be present, will force load the cache, which might result in a slight ms spike
--- @param callback fun() # called when all data is ready
function LibTalentTree:RegisterOnCacheWarmup(callback)
assert(type(callback) == 'function', 'callback must be a function');
if cacheWarmedUp then
securecallfunction(callback);
else
table.insert(self.cacheWarmupRegistery, callback);
end
end
--- @public
--- @param nodeID number # TraitNodeID
--- @return ( number | nil ) # TraitTreeID
function LibTalentTree:GetTreeIDForNode(nodeID)
assert(type(nodeID) == 'number', 'nodeID must be a number');
if forceBuildCache then forceBuildCache(); end;
return self.cache.nodeTreeMap[nodeID];
end
LibTalentTree.GetTreeIdForNode = LibTalentTree.GetTreeIDForNode;
--- @public
--- @param entryID number # TraitEntryID
--- @return ( number | nil ) # TraitTreeID
function LibTalentTree:GetTreeIDForEntry(entryID)
assert(type(entryID) == 'number', 'entryID must be a number');
if forceBuildCache then forceBuildCache(); end;
return self.cache.entryTreeMap[entryID];
end
LibTalentTree.GetTreeIdForEntry = LibTalentTree.GetTreeIDForEntry;
--- @public
--- @param entryID number # TraitEntryID
--- @return ( number | nil ) # TraitNodeID
function LibTalentTree:GetNodeIDForEntry(entryID)
assert(type(entryID) == 'number', 'entryID must be a number');
if forceBuildCache then forceBuildCache(); end;
return self.cache.entryNodeMap[entryID];
end
--- @public
--- @param treeID number # TraitTreeID, or TraitNodeID, if leaving the 2nd argument nil
--- @param nodeID number # TraitNodeID, can be omitted, by passing the nodeID as the first argument, the treeID is automatically determined
--- @return ( libNodeInfo | nil )
--- @overload fun(self: LibTalentTree-1.0, nodeID: number): libNodeInfo | nil
function LibTalentTree:GetLibNodeInfo(treeID, nodeID)
assert(type(treeID) == 'number', 'treeID must be a number');
if not nodeID then
nodeID = treeID;
--- @type number
treeID = self:GetTreeIDForNode(nodeID); ---@diagnostic disable-line: assign-type-mismatch
end
assert(type(nodeID) == 'number', 'nodeID must be a number');
if forceBuildCache then forceBuildCache(); end;
local nodeData = self.cache.nodeData;
local nodeInfo = nodeData[treeID] and nodeData[treeID][nodeID] and deepCopy(nodeData[treeID][nodeID]) or nil;
if (nodeInfo) then nodeInfo.ID = nodeID; end
return nodeInfo;
end
--- @public
--- @param treeID number # TraitTreeID, or TraitEntryID, if leaving the 2nd argument nil
--- @param nodeID number # TraitNodeID, can be omitted, by passing the nodeID as the first argument, the treeID is automatically determined
--- @return ( libNodeInfo ) # libNodeInfo is enriched and overwritten by C_Traits information if possible
--- @overload fun(self: LibTalentTree-1.0, nodeID: number): libNodeInfo
function LibTalentTree:GetNodeInfo(treeID, nodeID)
assert(type(treeID) == 'number', 'treeID must be a number');
if not nodeID then
nodeID = treeID;
--- @type number
treeID = self:GetTreeIDForNode(nodeID); ---@diagnostic disable-line: assign-type-mismatch
end
assert(type(nodeID) == 'number', 'nodeID must be a number');
if forceBuildCache then forceBuildCache(); end;
local cNodeInfo = C_ClassTalents.GetActiveConfigID()
and C_Traits.GetNodeInfo(C_ClassTalents.GetActiveConfigID(), nodeID)
or C_Traits.GetNodeInfo(Constants.TraitConsts.VIEW_TRAIT_CONFIG_ID or -3, nodeID);
local libNodeInfo = treeID and self:GetLibNodeInfo(treeID, nodeID);
if (not libNodeInfo) then return cNodeInfo; end
if (not cNodeInfo) then cNodeInfo = {}; end
if cNodeInfo.ID == nodeID then
return Mixin(libNodeInfo, cNodeInfo);
end
return Mixin(cNodeInfo, libNodeInfo);
end
--- @public
--- @param treeID number # TraitTreeID, or TraitEntryID, if leaving the 2nd argument nil
--- @param entryID number # TraitEntryID, can be omitted, by passing the entryID as the first argument, the treeID is automatically determined
--- @return ( entryInfo | nil )
--- @overload fun(self: LibTalentTree-1.0, entryID: number): entryInfo | nil
function LibTalentTree:GetEntryInfo(treeID, entryID)
assert(type(treeID) == 'number', 'treeID must be a number');
if not entryID then
entryID = treeID;
--- @type number
treeID = self:GetTreeIDForEntry(entryID); ---@diagnostic disable-line: assign-type-mismatch
end
assert(type(entryID) == 'number', 'entryID must be a number');
if forceBuildCache then forceBuildCache(); end;
local entryData = self.cache.entryData;
local entryInfo = entryData[treeID] and entryData[treeID][entryID] and deepCopy(entryData[treeID][entryID]) or nil;
if (entryInfo) then
entryInfo.isAvailable = true;
entryInfo.conditionIDs = {};
end
return entryInfo;
end
--- @public
--- @param class (string | number) # ClassID or ClassFilename - e.g. "DEATHKNIGHT" or 6 - See https://warcraft.wiki.gg/wiki/ClassID
--- @return ( number | nil ) # TraitTreeID
function LibTalentTree:GetClassTreeID(class)
assert(type(class) == 'string' or type(class) == 'number', 'class must be a string or number');
local classFileMap = self.cache.classFileMap;
local classTreeMap = self.cache.classTreeMap;
local classID = classFileMap[class] or class;
return classTreeMap[classID] or nil;
end
LibTalentTree.GetClassTreeId = LibTalentTree.GetClassTreeID;
--- @public
--- @param treeID (number) # a class' TraitTreeID
--- @return (number | nil) # ClassID or nil - See https://warcraft.wiki.gg/wiki/ClassID
function LibTalentTree:GetClassIDByTreeID(treeID)
treeID = tonumber(treeID); ---@diagnostic disable-line: cast-local-type
if not self.inverseClassMap then
local classTreeMap = self.cache.classTreeMap;
self.inverseClassMap = {};
for classID, mappedTreeID in pairs(classTreeMap) do
self.inverseClassMap[mappedTreeID] = classID;
end
end
return self.inverseClassMap[treeID];
end
LibTalentTree.GetClassIdByTreeId = LibTalentTree.GetClassIDByTreeID;
--- @public
--- @param specID number # See https://warcraft.wiki.gg/wiki/SpecializationID
--- @param nodeID number # TraitNodeID
--- @return boolean # whether the node is visible for the given spec
function LibTalentTree:IsNodeVisibleForSpec(specID, nodeID)
assert(type(specID) == 'number', 'specID must be a number');
assert(type(nodeID) == 'number', 'nodeID must be a number');
if forceBuildCache then forceBuildCache(); end;
local specMap = self.cache.specMap;
local class = specMap[specID];
assert(class, 'Unknown specID: ' .. specID);
local treeID = self:GetClassTreeID(class);
local nodeInfo = self:GetLibNodeInfo(treeID, nodeID);
if not nodeInfo then return false; end
return nodeInfo.visibleForSpecs[specID];
end
--- @public
--- @param specID number # See https://warcraft.wiki.gg/wiki/SpecializationID
--- @param nodeID number # TraitNodeID
--- @return boolean # whether the node is granted by default for the given spec
function LibTalentTree:IsNodeGrantedForSpec(specID, nodeID)
assert(type(specID) == 'number', 'specID must be a number');
assert(type(nodeID) == 'number', 'nodeID must be a number');
if forceBuildCache then forceBuildCache(); end;
local specMap = self.cache.specMap;
local class = specMap[specID];
assert(class, 'Unknown specID: ' .. specID);
local treeID = self:GetClassTreeID(class);
local nodeInfo = self:GetLibNodeInfo(treeID --[[@as number]], nodeID);
if not nodeInfo then return false; end
return nodeInfo.grantedForSpecs[specID];
end
--- @public
--- @param treeID number # TraitTreeID, or TraitNodeID, if leaving the 2nd parameter nil
--- @param nodeID number # TraitNodeID, can be omitted, by passing the nodeID as the first argument, the treeID is automatically determined
--- @return ( number|nil, number|nil ) # posX, posY - some trees have a global offset
--- @overload fun(self: LibTalentTree-1.0, nodeID: number): (number|nil, number|nil)
function LibTalentTree:GetNodePosition(treeID, nodeID)
assert(type(treeID) == 'number', 'treeID must be a number');
if not nodeID then
nodeID = treeID;
--- @type number
treeID = self:GetTreeIDForNode(nodeID); ---@diagnostic disable-line: assign-type-mismatch
end
assert(type(nodeID) == 'number', 'nodeID must be a number');
local nodeInfo = self:GetLibNodeInfo(treeID, nodeID);
if (not nodeInfo) then return nil, nil; end
return nodeInfo.posX, nodeInfo.posY;
end
local gridPositionCache = {};
--- @public
--- Returns an abstraction of the node positions into a grid of columns and rows.
--- Some specs may have nodes that sit between 2 columns, these columns end in ".5". This happens for example in the Druid and Demon Hunter trees.
---
--- The top row is 1, the bottom row is 10.
--- The first class column is 1, the last class column is 9.
--- The first spec column is 13. (if the client supports sub trees, otherwise it's 10)
---
--- Hero talents are placed in between the class and spec trees, in columns 10, 11, 12.
--- Hero talent subTrees are stacked to overlap, all subTrees on rows 1 - 5. You're responsible for adjusting this yourself.
---
--- The Hero talent selection node, is hardcoded to row 5.5 and column 10. Making it sit right underneath the sub trees themselves.
---
--- @param treeID number # TraitTreeID, or TraitNodeID, if leaving the 2nd parameter nil
--- @param nodeID number # TraitNodeID, can be omitted, by passing the nodeID as the first argument, the treeID is automatically determined
--- @return ( number|nil, number|nil ) # column, row
--- @overload fun(self: LibTalentTree-1.0, nodeID: number): (number|nil, number|nil)
function LibTalentTree:GetNodeGridPosition(treeID, nodeID)
assert(type(treeID) == 'number', 'treeID must be a number');
if not nodeID then
nodeID = treeID;
--- @type number
treeID = self:GetTreeIDForNode(nodeID); ---@diagnostic disable-line: assign-type-mismatch
end
assert(type(nodeID) == 'number', 'nodeID must be a number');
local classID = self:GetClassIDByTreeID(treeID);
if not classID then return nil, nil end
gridPositionCache[treeID] = gridPositionCache[treeID] or {};
if gridPositionCache[treeID][nodeID] then
return unpack(gridPositionCache[treeID][nodeID]);
end
local posX, posY = self:GetNodePosition(treeID, nodeID);
if (not posX or not posY) then return nil, nil; end
local offsetX = BASE_PAN_OFFSET_X - (CLASS_OFFSETS[classID] and CLASS_OFFSETS[classID].x or 0);
local offsetY = BASE_PAN_OFFSET_Y - (CLASS_OFFSETS[classID] and CLASS_OFFSETS[classID].y or 0);
local rawX, rawY = posX, posY;
posX = (round(posX) / 10) - offsetX;
posY = (round(posY) / 10) - offsetY;
local colSpacing = 60;
local row, col;
local nodeInfo = self:GetLibNodeInfo(treeID, nodeID);
local subTreeID = nodeInfo and nodeInfo.subTreeID;
if subTreeID then
local subTreeInfo = self:GetSubTreeInfo(subTreeID);
if subTreeInfo then
local topCenterPosX = subTreeInfo.posX;
local topCenterPosY = subTreeInfo.posY;
local offsetFromCenterX = rawX - topCenterPosX;
if (offsetFromCenterX > colSpacing) then
col = 12;
elseif (offsetFromCenterX < -colSpacing) then
col = 10;
else
col = 11;
end
local rowStart = topCenterPosY;
local rowSpacing = 2400 / 4; -- 2400 is generally the height of a sub tree, 4 is number of "gaps" between 5 rows
local halfRowEnabled = false;
row = getGridLineFromCoordinate(rowStart, rowSpacing, halfRowEnabled, rawY) or 0;
end
elseif nodeInfo and nodeInfo.isSubTreeSelection then
col = 10;
row = 5.5;
end
if not row or not col then
local colStart = 176;
local halfColEnabled = true;
local classColEnd = 656;
local specColStart = 956;
local subTreeOffset = 3 * colSpacing;
local classSpecGap = (specColStart - classColEnd) - subTreeOffset;
if (posX > (classColEnd + (classSpecGap / 2))) then
-- remove the gap between the class and spec trees
posX = posX - classSpecGap + colSpacing;
end
col = getGridLineFromCoordinate(colStart, colSpacing, halfColEnabled, posX);
local rowStart = 151;
local rowSpacing = 60;
local halfRowEnabled = false;
row = getGridLineFromCoordinate(rowStart, rowSpacing, halfRowEnabled, posY);
end
gridPositionCache[treeID][nodeID] = {col, row};
return col, row;
end
--- @public
--- @param treeID number # TraitTreeID, or TraitNodeID, if leaving the 2nd parameter nil
--- @param nodeID number # TraitNodeID, can be omitted, by passing the nodeID as the first argument, the treeID is automatically determined
--- @return ( nil | visibleEdge[] ) # The order might not match C_Traits
--- @overload fun(self: LibTalentTree-1.0, nodeID: number): nil | visibleEdge[]
function LibTalentTree:GetNodeEdges(treeID, nodeID)
assert(type(treeID) == 'number', 'treeID must be a number');
if not nodeID then
nodeID = treeID;
--- @type number
treeID = self:GetTreeIDForNode(nodeID); ---@diagnostic disable-line: assign-type-mismatch
end
assert(type(nodeID) == 'number', 'nodeID must be a number');
local nodeInfo = self:GetLibNodeInfo(treeID, nodeID);
if (not nodeInfo) then return nil; end
return nodeInfo.visibleEdges;
end
--- @public
--- @param treeID number # TraitTreeID, or TraitNodeID, if leaving the 2nd parameter nil
--- @param nodeID number # TraitNodeID, can be omitted, by passing the nodeID as the first argument, the treeID is automatically determined
--- @return ( boolean | nil ) # true if the node is a class node, false for spec nodes, nil if unknown
--- @overload fun(self: LibTalentTree-1.0, nodeID: number): boolean | nil
function LibTalentTree:IsClassNode(treeID, nodeID)
assert(type(treeID) == 'number', 'treeID must be a number');
if not nodeID then
nodeID = treeID;
--- @type number
treeID = self:GetTreeIDForNode(nodeID); ---@diagnostic disable-line: assign-type-mismatch
end
assert(type(nodeID) == 'number', 'nodeID must be a number');
local nodeInfo = self:GetLibNodeInfo(treeID, nodeID);
if (not nodeInfo) then return nil; end
return nodeInfo.isClassNode;
end
local gateCache = {}
--- @public
--- @param specID number # See https://warcraft.wiki.gg/wiki/SpecializationID
--- @return ( gateInfo[] ) # list of gates for the given spec, sorted by spending required
function LibTalentTree:GetGates(specID)
-- an optimization step is likely trivial in 10.1.0, but well.. effort, and this also works fine still :)
assert(type(specID) == 'number', 'specID must be a number');
if forceBuildCache then forceBuildCache(); end;
if (gateCache[specID]) then return deepCopy(gateCache[specID]); end
local specMap = self.cache.specMap;
local class = specMap[specID];
assert(class, 'Unknown specID: ' .. specID);
local treeID = self:GetClassTreeID(class);
local gates = {};
local nodesByConditions = {};
local gateData = self.cache.gateData;
local conditions = gateData[treeID];
local nodeData = self.cache.nodeData;
for nodeID, nodeInfo in pairs(nodeData[treeID]) do
if (nodeInfo.conditionIDs and #nodeInfo.conditionIDs > 0 and self:IsNodeVisibleForSpec(specID, nodeID)) then
for _, conditionID in pairs(nodeInfo.conditionIDs) do
if conditions[conditionID] then
nodesByConditions[conditionID] = nodesByConditions[conditionID] or {};
nodesByConditions[conditionID][nodeID] = nodeInfo;
end
end
end
end
for conditionID, gateInfo in pairs(conditions) do
local nodes = nodesByConditions[conditionID];
if (nodes) then
local minX, minY, topLeftNode = 9999999, 9999999, nil;
for nodeID, nodeInfo in pairs(nodes) do
local roundedX, roundedY = round(nodeInfo.posX), round(nodeInfo.posY);
if (roundedY < minY) then
minY = roundedY;
minX = roundedX;
topLeftNode = nodeID
elseif (roundedY == minY and roundedX < minX) then
minX = roundedX;
topLeftNode = nodeID
end
end
if (topLeftNode) then
table.insert(gates, {
topLeftNodeID = topLeftNode,
conditionID = conditionID,
spentAmountRequired = gateInfo.spentAmountRequired,
traitCurrencyID = gateInfo.currencyID,
});
end
end
end
table.sort(gates, function(a, b)
return a.spentAmountRequired < b.spentAmountRequired;
end);
gateCache[specID] = gates;
return deepCopy(gates);
end
--- @public
--- @param treeID number # TraitTreeID
--- @return treeCurrencyInfo[] # list of currencies for the given tree, first entry is class currency, second is spec currency, the rest are sub tree currencies. The list is additionally indexed by the traitCurrencyID.
function LibTalentTree:GetTreeCurrencies(treeID)
assert(type(treeID) == 'number', 'treeID must be a number');
if forceBuildCache then forceBuildCache(); end;
return deepCopy(self.cache.treeCurrencyMap[treeID]);
end
--- @public
--- @param subTreeID number # TraitSubTreeID
--- @return number[] # list of TraitNodeIDs that belong to the given sub tree
function LibTalentTree:GetSubTreeNodeIDs(subTreeID)
assert(type(subTreeID) == 'number', 'subTreeID must be a number');
if forceBuildCache then forceBuildCache(); end;
return deepCopy(self.cache.subTreeNodesMap[subTreeID]) or {};
end
LibTalentTree.GetSubTreeNodeIds = LibTalentTree.GetSubTreeNodeIDs;
--- @public
--- @param specID number # See https://warcraft.wiki.gg/wiki/SpecializationID
--- @return number[] # list of TraitSubTreeIDs that belong to the given spec
function LibTalentTree:GetSubTreeIDsForSpecID(specID)
assert(type(specID) == 'number', 'specID must be a number');
if forceBuildCache then forceBuildCache(); end;
return deepCopy(self.cache.specSubTreeMap[specID]) or {};
end
LibTalentTree.GetSubTreeIdsForSpecId = LibTalentTree.GetSubTreeIDsForSpecID;
--- @public
--- @param subTreeID number # TraitSubTreeID
--- @return ( subTreeInfo | nil )
function LibTalentTree:GetSubTreeInfo(subTreeID)
assert(type(subTreeID) == 'number', 'subTreeID must be a number');
if forceBuildCache then forceBuildCache(); end;
return deepCopy(self.cache.subTreeData[subTreeID]);
end
--- @public
--- @param specID number # See https://warcraft.wiki.gg/wiki/SpecializationID
--- @param subTreeID number # TraitSubTreeID
--- @return number?, number? # TraitNodeID, TraitEntryID; or nil if not found
function LibTalentTree:GetSubTreeSelectionNodeIDAndEntryIDBySpecID(specID, subTreeID)
assert(type(specID) == 'number', 'specID must be a number');
assert(type(subTreeID) == 'number', 'subTreeID must be a number');
local subTreeInfo = self:GetSubTreeInfo(subTreeID);
for _, selectionNodeID in ipairs(subTreeInfo and subTreeInfo.subTreeSelectionNodeIDs or {}) do
if self:IsNodeVisibleForSpec(specID, selectionNodeID) then
local nodeInfo = self:GetLibNodeInfo(selectionNodeID);
for _, entryID in ipairs(nodeInfo and nodeInfo.entryIDs or {}) do
local entryInfo = self:GetEntryInfo(entryID);
if entryInfo and entryInfo.subTreeID == subTreeID then
return selectionNodeID, entryID;
end
end
break;
end
end
return nil;
end