-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCore.lua
More file actions
1234 lines (1117 loc) · 54.2 KB
/
Core.lua
File metadata and controls
1234 lines (1117 loc) · 54.2 KB
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
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
-- Wick's TBC BIS Tracker
-- Core.lua - Addon initialization and state
WTBT = WTBT or {}
WTBT_UI = WTBT_UI or {}
-- Current state
WTBT.state = {
class = "Priest",
spec = "Holy",
phase = 1,
tab = "bis", -- "bis", "custom", "consumables", "softres"
subtab = "gear", -- "gear", "enchants", "gems" (BIS + Custom tabs only)
customList = nil, -- active custom list name
customSlotPending = nil, -- slot waiting for item input
}
-- Slot display order
WTBT.SLOTS = {
{ key = "Head", label = "Head" },
{ key = "Neck", label = "Neck" },
{ key = "Shoulder", label = "Shoulder" },
{ key = "Back", label = "Back" },
{ key = "Chest", label = "Chest" },
{ key = "Wrist", label = "Wrist" },
{ key = "Hands", label = "Hands" },
{ key = "Waist", label = "Waist" },
{ key = "Legs", label = "Legs" },
{ key = "Feet", label = "Feet" },
{ key = "Ring1", label = "Ring 1" },
{ key = "Ring2", label = "Ring 2" },
{ key = "Trinket1", label = "Trinket 1" },
{ key = "Trinket2", label = "Trinket 2" },
{ key = "MainHand", label = "Main Hand" },
{ key = "OffHand", label = "Off Hand" },
{ key = "Relic", label = "Ranged" },
}
-- Source type colors (r, g, b)
WTBT.SOURCE_COLORS = {
drop = { 0.87, 0.47, 0.47 },
craft = { 0.47, 0.67, 0.87 },
badge = { 0.87, 0.75, 0.47 },
rep = { 0.47, 0.87, 0.47 },
quest = { 0.87, 0.87, 0.47 },
pvp = { 0.70, 0.47, 0.87 },
token = { 0.47, 0.78, 0.73 },
custom = { 0.65, 0.65, 0.65 },
}
-- Classes and their specs
WTBT.CLASSES = {
{ name = "Priest", specs = { "Holy", "Discipline", "Shadow" }, color = {1.00, 1.00, 1.00} },
{ name = "Paladin", specs = { "Holy", "Protection", "Retribution", "Fire Resist", "Nature Resist" }, color = {0.96, 0.55, 0.73} },
{ name = "Druid", specs = { "Restoration", "Balance", "Feral" }, color = {1.00, 0.49, 0.04} },
{ name = "Shaman", specs = { "Restoration", "Enhancement", "Elemental" }, color = {0.00, 0.44, 0.87} },
{ name = "Mage", specs = { "Fire", "Frost", "Arcane" }, color = {0.25, 0.78, 0.92} },
{ name = "Warlock", specs = { "Destruction", "Affliction", "Demonology", "Fire Resist" }, color = {0.53, 0.53, 0.93} },
{ name = "Hunter", specs = { "Beast Mastery", "Marksmanship", "Survival" }, color = {0.67, 0.83, 0.45} },
{ name = "Rogue", specs = { "Combat", "Assassination", "Subtlety" }, color = {1.00, 0.96, 0.41} },
{ name = "Warrior", specs = { "Arms", "Fury", "Protection", "Fire Resist", "Nature Resist", "Frost Resist" }, color = {0.78, 0.61, 0.43} },
}
-- Spec aliases (some specs share BIS guides)
WTBT.SPEC_ALIASES = {
["Priest"] = { ["Discipline"] = "Holy" },
["Rogue"] = { ["Subtlety"] = "Combat" },
}
-- Inventory slot mapping for equipped item checks
WTBT.INV_SLOTS = {
Head = 1, Neck = 2, Shoulder = 3, Back = 15, Chest = 5,
Wrist = 9, Hands = 10, Waist = 6, Legs = 7, Feet = 8,
Ring1 = 11, Ring2 = 12, Trinket1 = 13, Trinket2 = 14,
MainHand = 16, OffHand = 17, Relic = 18,
}
-- Check if an item is currently equipped by the player
function WTBT:IsItemEquipped(itemId)
if not itemId then return false end
for _, invSlot in pairs(self.INV_SLOTS) do
if GetInventoryItemID("player", invSlot) == itemId then
return true
end
end
return false
end
-- Check if the panel is showing the player's own class
function WTBT:IsViewingOwnClass()
local _, englishClass = UnitClass("player")
local classLookup = {
PRIEST = "Priest", PALADIN = "Paladin", DRUID = "Druid",
SHAMAN = "Shaman", MAGE = "Mage", WARLOCK = "Warlock",
HUNTER = "Hunter", ROGUE = "Rogue", WARRIOR = "Warrior",
}
return classLookup[englishClass] == self.state.class
end
-- Helper: get BIS list for current state
function WTBT:GetCurrentBIS()
local data = WTBT_Data
if not data then return nil end
local classData = data[self.state.class]
if not classData then return nil end
-- Check for spec alias
local spec = self.state.spec
local aliases = WTBT.SPEC_ALIASES[self.state.class]
if aliases and aliases[spec] then
spec = aliases[spec]
end
local specData = classData[spec]
if not specData then return nil end
return specData[self.state.phase]
end
-- Settings (persisted via SavedVariables)
WTBT.settings = {
tooltipBIS = true, -- show "BIS" tag in item tooltips
hideWorldBoss = false, -- hide World Boss drops
hidePvP = false, -- hide PvP sourced items
miniDashEnabled = false, -- show floating mini dashboard for current zone/dungeon/quests
miniDashPos = nil, -- saved position of the mini dash
}
-- Build a fast lookup: itemId → { slot, rank, phase } for the player's class across all phases
-- Called once on load and when class/spec might change
function WTBT:BuildPlayerBISLookup()
self.playerBISLookup = {} -- itemId → { slot, rank, phaseName }
local _, englishClass = UnitClass("player")
local classLookup = {
PRIEST = "Priest", PALADIN = "Paladin", DRUID = "Druid",
SHAMAN = "Shaman", MAGE = "Mage", WARLOCK = "Warlock",
HUNTER = "Hunter", ROGUE = "Rogue", WARRIOR = "Warrior",
}
local className = classLookup[englishClass]
if not className then return end
local data = WTBT_Data
if not data or not data[className] then return end
-- Try all specs for this class and build lookups
local classInfo = nil
for _, c in ipairs(self.CLASSES) do
if c.name == className then classInfo = c; break end
end
if not classInfo then return end
for _, specName in ipairs(classInfo.specs) do
-- Resolve alias
local resolvedSpec = specName
local aliases = self.SPEC_ALIASES[className]
if aliases and aliases[specName] then
resolvedSpec = aliases[specName]
end
local specData = data[className][resolvedSpec]
if specData then
for phase = 1, 5 do
local phaseData = specData[phase]
if phaseData then
for slotKey, items in pairs(phaseData) do
if type(items) == "table" then
for rank, item in ipairs(items) do
if item.itemId and not self.playerBISLookup[item.itemId] then
self.playerBISLookup[item.itemId] = {
slot = slotKey,
rank = rank,
phase = phase,
spec = specName,
}
end
end
end
end
end
end
end
end
end
-- Check if an item is BIS for the player (returns info table or nil)
function WTBT:GetBISInfo(itemId)
if not itemId or not self.playerBISLookup then return nil end
return self.playerBISLookup[itemId]
end
-- ============================================================
-- TOOLTIP HOOK — show BIS info on any item tooltip in the game
-- ============================================================
local function OnTooltipSetItem(tooltip)
if not WTBT.settings.tooltipBIS then return end
local _, itemLink = tooltip:GetItem()
if not itemLink then return end
local itemId = tonumber(itemLink:match("item:(%d+)"))
if not itemId then return end
local bisInfo = WTBT:GetBISInfo(itemId)
if not bisInfo then return end
local rankText
if bisInfo.rank == 1 then
rankText = "|cff4FC778BIS|r"
else
rankText = "|cff4FC778Alt #" .. bisInfo.rank .. "|r"
end
local phaseText = "|cff6B598AP" .. bisInfo.phase .. "|r"
local specText = "|cffD5C7A1" .. bisInfo.spec .. "|r"
tooltip:AddLine(" ")
tooltip:AddLine("|cff4FC778[Wick's BIS]|r " .. rankText .. " for " .. specText .. " " .. phaseText)
tooltip:Show()
end
-- Event frame
local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("ADDON_LOADED")
eventFrame:RegisterEvent("GET_ITEM_INFO_RECEIVED")
eventFrame:RegisterEvent("PLAYER_LOGOUT")
eventFrame:RegisterEvent("BAG_UPDATE_DELAYED")
eventFrame:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
eventFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
eventFrame:RegisterEvent("PLAYER_REGEN_DISABLED")
local pendingRefresh = false
local function ScheduleRefresh()
if pendingRefresh then return end
if not (WTBT_UI.panel and WTBT_UI.panel:IsShown()) then return end
pendingRefresh = true
if C_Timer and C_Timer.After then
C_Timer.After(0.2, function()
pendingRefresh = false
WTBT_UI:Refresh()
end)
else
pendingRefresh = false
WTBT_UI:Refresh()
end
end
eventFrame:SetScript("OnEvent", function(self, event, arg1)
if event == "ADDON_LOADED" and arg1 == "WickidsTBCBISTracker" then
WTBT:Init()
elseif event == "PLAYER_LOGOUT" then
-- Persist last-viewed class/spec/phase/tab/list
WTBT.settings.lastClass = WTBT.state.class
WTBT.settings.lastSpec = WTBT.state.spec
WTBT.settings.lastPhase = WTBT.state.phase
WTBT.settings.lastTab = WTBT.state.tab
WTBT.settings.lastCustomList = WTBT.state.customList
WTBTCustomLists = WTBT.customLists
WTBTSavedSettings = WTBT.settings
WTBTSoftResData = WTBT.softResData
elseif event == "GET_ITEM_INFO_RECEIVED" then
ScheduleRefresh()
elseif event == "BAG_UPDATE_DELAYED"
or event == "PLAYER_EQUIPMENT_CHANGED"
or event == "PLAYER_REGEN_ENABLED"
or event == "PLAYER_REGEN_DISABLED" then
if WTBT.state.tab == "custom" then
ScheduleRefresh()
end
end
end)
function WTBT:Init()
-- Load saved settings
if WTBTSavedSettings then
for k, v in pairs(WTBTSavedSettings) do
self.settings[k] = v
end
end
-- Load soft reserve data
self.softResData = WTBTSoftResData or nil
-- Detect player's class
local _, englishClass = UnitClass("player")
local classLookup = {
PRIEST = "Priest", PALADIN = "Paladin", DRUID = "Druid",
SHAMAN = "Shaman", MAGE = "Mage", WARLOCK = "Warlock",
HUNTER = "Hunter", ROGUE = "Rogue", WARRIOR = "Warrior",
}
local playerClass = classLookup[englishClass] or "Priest"
-- Always start on the player's own class
self.state.class = playerClass
self.state.phase = self.settings.lastPhase or 1
self.state.tab = self.settings.lastTab or "bis"
-- Restore saved spec only if it belongs to the player's class
local defaultSpec = "Holy"
for _, c in ipairs(self.CLASSES) do
if c.name == playerClass then
defaultSpec = c.specs[1]
if self.settings.lastSpec then
-- Check if saved spec is valid for this class
for _, s in ipairs(c.specs) do
if s == self.settings.lastSpec then
defaultSpec = self.settings.lastSpec
break
end
end
end
break
end
end
self.state.spec = defaultSpec
-- Load custom lists (per-character, keyed by class/spec)
if not WTBTCustomLists then WTBTCustomLists = {} end
-- Migrate old flat format to class/spec structure
local needsMigration = false
for k, v in pairs(WTBTCustomLists) do
-- Old format: top-level keys are list names (strings)
-- New format: top-level keys are class names containing spec tables
if type(v) == "table" and v.Head == nil and v.Chest == nil then
-- Could be new format (class table) or old list with no slots filled
-- Check if value contains spec-like subtables
local hasSpecKey = false
for sk, sv in pairs(v) do
if type(sv) == "table" then
for ssk, ssv in pairs(sv) do
if type(ssv) == "table" and ssv.itemId then
-- Old format list with slot data
needsMigration = true
break
end
end
end
if needsMigration then break end
end
elseif type(v) == "table" then
-- Has slot keys directly = old format
needsMigration = true
end
if needsMigration then break end
end
if needsMigration then
local oldLists = WTBTCustomLists
WTBTCustomLists = {}
local playerClass = self.state.class or "Priest"
local playerSpec = self.state.spec or "Holy"
WTBTCustomLists[playerClass] = {}
WTBTCustomLists[playerClass][playerSpec] = oldLists
end
self.customLists = WTBTCustomLists
-- Backfill missing source info on existing custom list items
for cls, specTable in pairs(WTBTCustomLists) do
if type(specTable) == "table" then
for spec, lists in pairs(specTable) do
if type(lists) == "table" then
for listName, slots in pairs(lists) do
if type(slots) == "table" then
for slotKey, entry in pairs(slots) do
if type(entry) == "table" and entry.itemId and not entry.source then
local src, srcType = self:FindItemSource(entry.itemId)
if src then
entry.source = src
entry.sourceType = srcType
end
end
end
end
end
end
end
end
end
-- Restore active custom list for current class/spec
local csLists = self:GetClassSpecLists()
if self.settings.lastCustomList and csLists[self.settings.lastCustomList] then
self.state.customList = self.settings.lastCustomList
else
self.state.customList = nil
for name, _ in pairs(csLists) do
self.state.customList = name
break
end
end
WTBT_UI:Build()
WTBT_UI:Refresh()
WTBT_UI.panel:Hide() -- Must be AFTER refresh to stay hidden
-- Build BIS lookup for tooltip integration
self:BuildPlayerBISLookup()
-- Initialize mini dashboard
if WTBT_MiniDash and WTBT_MiniDash.Initialize then
WTBT_MiniDash:Initialize()
end
-- Hook GameTooltip for BIS tags
GameTooltip:HookScript("OnTooltipSetItem", OnTooltipSetItem)
-- Also hook ItemRefTooltip (shift-clicked items in chat)
if ItemRefTooltip then
ItemRefTooltip:HookScript("OnTooltipSetItem", OnTooltipSetItem)
end
-- Hook shopping tooltips (comparison tooltips)
if ShoppingTooltip1 then
ShoppingTooltip1:HookScript("OnTooltipSetItem", OnTooltipSetItem)
end
if ShoppingTooltip2 then
ShoppingTooltip2:HookScript("OnTooltipSetItem", OnTooltipSetItem)
end
-- Wardrobe cross-promo — shown once per session if not already installed.
if not (IsAddOnLoaded and IsAddOnLoaded("WicksWardrobe")) then
C_Timer.After(3, function()
print("|cff4FC778[Wick's BIS]|r View all your BIS and custom sets on your character in the new |cffD4C8A1Wick's Wardrobe|r addon. Get it at curseforge.com/wow/addons/wicks-wardrobe")
end)
end
-- Pre-cache all item info for current phase
local bisData = self:GetCurrentBIS()
if bisData then
for _, slotData in pairs(bisData) do
if type(slotData) == "table" then
for _, item in ipairs(slotData) do
if item.itemId then
GetItemInfo(item.itemId)
end
end
end
end
end
end
-- ============================================================
-- CUSTOM LISTS
-- ============================================================
-- Slot keys used for custom lists (same as BIS)
WTBT.CUSTOM_SLOTS = {
"Head","Neck","Shoulder","Back","Chest","Wrist","Hands","Waist","Legs","Feet",
"Ring1","Ring2","Trinket1","Trinket2",
"MainHand","OffHand","Relic",
}
-- Get the lists subtable for the currently selected class/spec
function WTBT:GetClassSpecLists()
local cls = self.state.class or "Priest"
local spec = self.state.spec or "Holy"
if not self.customLists[cls] then self.customLists[cls] = {} end
if not self.customLists[cls][spec] then self.customLists[cls][spec] = {} end
return self.customLists[cls][spec]
end
function WTBT:CreateCustomList(name)
if not name or name == "" then return false end
local csLists = self:GetClassSpecLists()
if csLists[name] then return false end
csLists[name] = {}
self.state.customList = name
return true
end
function WTBT:DeleteCustomList(name)
local csLists = self:GetClassSpecLists()
if not name or not csLists[name] then return false end
csLists[name] = nil
-- Switch to another list in this class/spec or nil
self.state.customList = nil
for n, _ in pairs(csLists) do
self.state.customList = n
break
end
return true
end
function WTBT:GetCustomListNames()
local csLists = self:GetClassSpecLists()
local names = {}
for n, _ in pairs(csLists) do
names[#names + 1] = n
end
table.sort(names)
return names
end
function WTBT:SetCustomSlot(listName, slotKey, itemId, source, sourceType)
local csLists = self:GetClassSpecLists()
local list = csLists[listName]
if not list then return false end
if itemId then
local entry = { itemId = itemId }
-- Auto-detect source from BIS data
if not source then
local foundSource, foundType = self:FindItemSource(itemId)
if foundSource then
entry.source = foundSource
entry.sourceType = foundType
end
else
entry.source = source
entry.sourceType = sourceType or "custom"
end
list[slotKey] = entry
GetItemInfo(itemId)
else
list[slotKey] = nil
end
return true
end
function WTBT:GetCustomSlot(listName, slotKey)
local csLists = self:GetClassSpecLists()
local list = csLists[listName]
if not list then return nil end
return list[slotKey]
end
-- ============================================================
-- IMPORT / EXPORT
-- ============================================================
-- Format: WTBT1~Class~Spec~Name~id1,id2,...,id17
-- Separator is ~ (not |) because WoW EditBoxes eat |t / |c / |r etc.
-- Slot order matches WTBT.CUSTOM_SLOTS. 0 means empty slot.
local function CleanExportName(s)
if not s then return "" end
s = s:gsub("~", "-")
s = s:gsub("[\r\n]", " ")
return strtrim(s)
end
function WTBT:ExportCustomList(listName)
if not listName then return nil, "No list selected." end
local csLists = self:GetClassSpecLists()
local list = csLists[listName]
if not list then return nil, "List not found." end
local cls = self.state.class or ""
local spec = self.state.spec or ""
local cleanName = CleanExportName(listName)
local ids = {}
for _, slotKey in ipairs(self.CUSTOM_SLOTS) do
local entry = list[slotKey]
local id = (entry and entry.itemId) or 0
ids[#ids + 1] = tostring(id)
end
return string.format("WTBT1~%s~%s~%s~%s", cls, spec, cleanName, table.concat(ids, ","))
end
-- Returns (ok, finalNameOrErr, parsedClass, parsedSpec)
function WTBT:ImportCustomList(encoded)
if not encoded then return false, "Empty string." end
encoded = strtrim(encoded)
if encoded == "" then return false, "Empty string." end
local prefix, cls, spec, name, idstr = encoded:match("^([^~]+)~([^~]+)~([^~]+)~(.-)~([^~]+)$")
if not prefix then return false, "Not a valid Wick BIS list." end
if prefix ~= "WTBT1" then return false, "Unknown format version: " .. prefix end
-- Validate class/spec
local validClass, validSpec = false, false
for _, c in ipairs(self.CLASSES) do
if c.name == cls then
validClass = true
for _, s in ipairs(c.specs) do
if s == spec then validSpec = true break end
end
break
end
end
if not validClass then return false, "Unknown class: " .. tostring(cls) end
if not validSpec then return false, "Unknown spec for " .. cls .. ": " .. tostring(spec) end
name = strtrim(name)
if name == "" then return false, "Missing list name." end
-- Parse ids (one trailing comma forces final empty match capture)
local ids = {}
for chunk in (idstr .. ","):gmatch("([^,]*),") do
local n = tonumber(chunk)
if not n then return false, "Invalid item id: '" .. tostring(chunk) .. "'" end
ids[#ids + 1] = n
end
if #ids ~= #self.CUSTOM_SLOTS then
return false, string.format("Expected %d slot ids, got %d.", #self.CUSTOM_SLOTS, #ids)
end
-- Resolve name collisions under the source class/spec (NOT current view)
if not self.customLists[cls] then self.customLists[cls] = {} end
if not self.customLists[cls][spec] then self.customLists[cls][spec] = {} end
local target = self.customLists[cls][spec]
local finalName = name
if target[finalName] then
local i = 2
while target[finalName .. " (" .. i .. ")"] do i = i + 1 end
finalName = finalName .. " (" .. i .. ")"
end
target[finalName] = {}
for i, slotKey in ipairs(self.CUSTOM_SLOTS) do
local id = ids[i]
if id and id > 0 then
local entry = { itemId = id }
local src, srcType = self:FindItemSource(id)
if src then
entry.source = src
entry.sourceType = srcType
else
entry.source = "Custom"
entry.sourceType = "custom"
end
target[finalName][slotKey] = entry
GetItemInfo(id)
end
end
return true, finalName, cls, spec
end
-- ============================================================
-- SIXTY UPGRADES IMPORT
-- ============================================================
-- Parses the JSON exported by sixtyupgrades.com and creates a
-- custom list under the currently selected class/spec.
-- Returns (ok, finalNameOrErr)
local SU_SLOT_MAP = {
HEAD = "Head",
NECK = "Neck",
SHOULDERS = "Shoulder",
BACK = "Back",
CHEST = "Chest",
WRISTS = "Wrist",
HANDS = "Hands",
WAIST = "Waist",
LEGS = "Legs",
FEET = "Feet",
FINGER_1 = "Ring1",
FINGER_2 = "Ring2",
TRINKET_1 = "Trinket1",
TRINKET_2 = "Trinket2",
MAIN_HAND = "MainHand",
OFF_HAND = "OffHand",
RANGED = "Relic",
RELIC = "Relic",
}
local SU_CLASS_MAP = {
PRIEST = "Priest", PALADIN = "Paladin", DRUID = "Druid",
SHAMAN = "Shaman", MAGE = "Mage", WARLOCK = "Warlock",
HUNTER = "Hunter", ROGUE = "Rogue", WARRIOR = "Warrior",
}
-- Minimal field extractor for the Sixty Upgrades JSON format.
-- Avoids a full JSON parser by targeting known structure.
local function SU_ParseJSON(raw)
local gameClass = raw:match('"gameClass"%s*:%s*"([^"]+)"')
local charBlock = raw:match('"character"%s*:%s*(%b{})')
local charName = charBlock and charBlock:match('"name"%s*:%s*"([^"]+)"')
local setName = raw:match('"name"%s*:%s*"([^"]+)"')
-- Locate the "items" array and scan it for {id, slot} pairs.
-- Each item entry is a flat object: {"name":"...","id":N,"gems":[...],"slot":"S"}
-- Strategy: find "items": then walk character-by-character tracking brace depth
-- to collect each top-level object in the array.
local items = {}
local itemsStart = raw:find('"items"%s*:%s*%[')
if itemsStart then
-- Advance past the opening [
local pos = raw:find("%[", itemsStart) + 1
local depth = 0
local objStart = nil
while pos <= #raw do
local ch = raw:sub(pos, pos)
if ch == "{" then
if depth == 0 then objStart = pos end
depth = depth + 1
elseif ch == "}" then
depth = depth - 1
if depth == 0 and objStart then
local block = raw:sub(objStart, pos)
local id = block:match('"id"%s*:%s*(%d+)')
local slot = block:match('"slot"%s*:%s*"([^"]+)"')
if id and slot and SU_SLOT_MAP[slot] then
items[#items + 1] = { id = tonumber(id), slot = slot }
end
objStart = nil
end
elseif ch == "]" and depth == 0 then
break -- end of items array
end
pos = pos + 1
end
end
return gameClass, charName, setName, items
end
function WTBT:ImportFromSixtyUpgrades(raw)
if not raw or strtrim(raw) == "" then return false, "Empty paste." end
local gameClass, charName, setName, items = SU_ParseJSON(raw)
if not gameClass then return false, "Could not find gameClass in JSON." end
local mappedClass = SU_CLASS_MAP[gameClass]
if not mappedClass then return false, "Unknown class in export: " .. gameClass end
-- Validate against the current view's class
local curClass = self.state.class or ""
if mappedClass ~= curClass then
return false, string.format(
"Export is for a %s but you are viewing %s. Switch class first.",
mappedClass, curClass)
end
if not items or #items == 0 then return false, "No items found in JSON." end
-- Build list name from set name + char name
local baseName = setName or "60U Import"
if charName and charName ~= "" then
baseName = charName .. " - " .. baseName
end
baseName = baseName:gsub("~", "-"):gsub("[\r\n]", " ")
local csLists = self:GetClassSpecLists()
local finalName = baseName
if csLists[finalName] then
local i = 2
while csLists[finalName .. " (" .. i .. ")"] do i = i + 1 end
finalName = finalName .. " (" .. i .. ")"
end
csLists[finalName] = {}
local list = csLists[finalName]
local filled = 0
for _, entry in ipairs(items) do
local slotKey = SU_SLOT_MAP[entry.slot]
if slotKey and entry.id > 0 then
local src, srcType = self:FindItemSource(entry.id)
list[slotKey] = {
itemId = entry.id,
source = src or "Sixty Upgrades",
sourceType = srcType or "custom",
}
GetItemInfo(entry.id)
filled = filled + 1
end
end
if filled == 0 then
csLists[finalName] = nil
return false, "No valid items could be mapped from the export."
end
return true, finalName
end
-- Returns counts: total (slots with non-zero itemId in list), ownedInBags
-- (in bags and not currently worn), worn (already equipped to the right slot).
function WTBT:GetCustomListEquipStatus(listName)
if not listName then return 0, 0, 0 end
local csLists = self:GetClassSpecLists()
local list = csLists[listName]
if not list then return 0, 0, 0 end
local total, ownedInBags, worn = 0, 0, 0
for _, slotKey in ipairs(self.CUSTOM_SLOTS) do
local entry = list[slotKey]
if entry and entry.itemId then
total = total + 1
local invSlot = self.INV_SLOTS[slotKey]
if invSlot then
local equipped = GetInventoryItemID("player", invSlot)
if equipped == entry.itemId then
worn = worn + 1
elseif (GetItemCount(entry.itemId) or 0) > 0 then
ownedInBags = ownedInBags + 1
end
end
end
end
return total, ownedInBags, worn
end
-- Equips every list item that's currently in bags and not already worn,
-- targeting the specific inv slot from the list (Ring1 vs Ring2, etc.).
-- Returns equipCount, errMsg. Caller should check InCombatLockdown beforehand
-- for friendlier UX, but this also no-ops in combat.
function WTBT:EquipAllOwned(listName)
if not listName then return 0, "No list selected." end
if InCombatLockdown() then return 0, "Cannot equip while in combat." end
local csLists = self:GetClassSpecLists()
local list = csLists[listName]
if not list then return 0, "List not found." end
local count = 0
for _, slotKey in ipairs(self.CUSTOM_SLOTS) do
local entry = list[slotKey]
if entry and entry.itemId then
local invSlot = self.INV_SLOTS[slotKey]
if invSlot then
local already = GetInventoryItemID("player", invSlot)
if already ~= entry.itemId and (GetItemCount(entry.itemId) or 0) > 0 then
EquipItemByName(entry.itemId, invSlot)
count = count + 1
end
end
end
end
return count
end
-- Create a new list pre-filled with the player's currently equipped gear.
-- Saves under current state.class/spec. Returns finalName, count (slots filled).
function WTBT:CreateListFromEquipped(baseName)
if not baseName or baseName == "" then baseName = "Equipped" end
local csLists = self:GetClassSpecLists()
local finalName = baseName
if csLists[finalName] then
local i = 2
while csLists[finalName .. " (" .. i .. ")"] do i = i + 1 end
finalName = finalName .. " (" .. i .. ")"
end
csLists[finalName] = {}
local list = csLists[finalName]
local count = 0
for slotKey, invSlot in pairs(self.INV_SLOTS) do
local id = GetInventoryItemID("player", invSlot)
if id then
local entry = { itemId = id }
local src, srcType = self:FindItemSource(id)
if src then
entry.source = src
entry.sourceType = srcType
else
entry.source = "Currently Equipped"
entry.sourceType = "custom"
end
list[slotKey] = entry
GetItemInfo(id)
count = count + 1
end
end
self.state.customList = finalName
return finalName, count
end
-- Fallback source lookup for items not in BIS data (template-only items)
local ITEM_SOURCE_FALLBACK = {
[23522] = { "Blacksmithing", "craft" },
[24263] = { "Tailoring", "craft" },
[25685] = { "Leatherworking", "craft" },
[25790] = { "Leatherworking", "craft" },
[25791] = { "Leatherworking", "craft" },
[27493] = { "Darkweaver Syth — Sethekk Halls", "drop" },
[27537] = { "Darkweaver Syth — Sethekk Halls", "drop" },
[27672] = { "Heroic Shadow Labyrinth", "drop" },
[27760] = { "Quest: Wants and Needs", "quest" },
[27813] = { "Heroic Botanica", "drop" },
[27839] = { "Heroic Shattered Halls", "drop" },
[27847] = { "Heroic Shattered Halls", "drop" },
[27867] = { "Heroic Shattered Halls", "drop" },
[27915] = { "Quest: Escape from Firewing Point", "quest" },
[27985] = { "Quest: Teron Gorefiend, I am...", "quest" },
[27986] = { "Quest: Blast the Infernals!", "quest" },
[28179] = { "Quest: City of Light", "quest" },
[28269] = { "Heroic Old Hillsbrad", "drop" },
[28386] = { "Heroic Mechanar", "drop" },
[28407] = { "Quest: How to Break Into the Arcatraz", "quest" },
[28415] = { "Heroic Mechanar", "drop" },
[28504] = { "Heroic Sethekk Halls", "drop" },
[29153] = { "The Consortium — Revered", "rep" },
[29165] = { "Keepers of Time — Exalted", "rep" },
[29242] = { "Heroic Old Hillsbrad", "drop" },
[29348] = { "Warchief Kargath — Heroic Shattered Halls", "drop" },
[31692] = { "Quest: Dimensius the All-Devouring", "quest" },
[32087] = { "Badge of Justice — 50 badges", "badge" },
-- Fire Resistance gear
[23513] = { "Blacksmithing (Aldor)", "craft" }, -- Flamebane Breastplate
[23514] = { "Blacksmithing (Aldor)", "craft" }, -- Flamebane Gloves
[23515] = { "Blacksmithing (Aldor)", "craft" }, -- Flamebane Bracers
[23516] = { "Blacksmithing (Aldor)", "craft" }, -- Flamebane Helm
[24092] = { "Jewelcrafting", "craft" }, -- Pendant of Frozen Flame
[29489] = { "Leatherworking", "craft" }, -- Enchanted Felscale Leggings
[29490] = { "Leatherworking", "craft" }, -- Enchanted Felscale Gloves
[29491] = { "Leatherworking", "craft" }, -- Enchanted Felscale Boots
[29495] = { "Leatherworking", "craft" }, -- Enchanted Clefthoof Leggings
[29496] = { "Leatherworking", "craft" }, -- Enchanted Clefthoof Gloves
[29497] = { "Leatherworking", "craft" }, -- Enchanted Clefthoof Boots
[30761] = { "Badge of Justice — 30 badges", "badge" }, -- Infernoweave Leggings
[30762] = { "Badge of Justice — 30 badges", "badge" }, -- Infernoweave Robe
[30763] = { "Badge of Justice — 20 badges", "badge" }, -- Infernoweave Boots
[30764] = { "Badge of Justice — 20 badges", "badge" }, -- Infernoweave Gloves
[30766] = { "Badge of Justice — 30 badges", "badge" }, -- Inferno Tempered Leggings
[30767] = { "Badge of Justice — 20 badges", "badge" }, -- Inferno Tempered Gauntlets
[30768] = { "Badge of Justice — 20 badges", "badge" }, -- Inferno Tempered Boots
[30769] = { "Badge of Justice — 30 badges", "badge" }, -- Inferno Tempered Chestguard
[30770] = { "Badge of Justice — 20 badges", "badge" }, -- Inferno Forged Boots
[30772] = { "Badge of Justice — 30 badges", "badge" }, -- Inferno Forged Leggings
[30773] = { "Badge of Justice — 30 badges", "badge" }, -- Inferno Forged Hauberk
[30774] = { "Badge of Justice — 20 badges", "badge" }, -- Inferno Forged Gloves
[30776] = { "Badge of Justice — 30 badges", "badge" }, -- Inferno Hardened Chestguard
[30778] = { "Badge of Justice — 30 badges", "badge" }, -- Inferno Hardened Leggings
[30779] = { "Badge of Justice — 20 badges", "badge" }, -- Inferno Hardened Boots
[30780] = { "Badge of Justice — 20 badges", "badge" }, -- Inferno Hardened Gloves
[30837] = { "Tailoring", "craft" }, -- Flameheart Bracers
[30838] = { "Tailoring", "craft" }, -- Flameheart Gloves
[30839] = { "Tailoring", "craft" }, -- Flameheart Vest
[31341] = { "Vendor — Blade's Edge Mountains", "drop" }, -- Wyrmcultist's Cloak
[31746] = { "Quest: Trial of the Naaru", "quest" }, -- Phoenix-fire Band
-- Frost Resistance gear (Hydross)
[22658] = { "Tailoring", "craft" }, -- Glacial Cloak
[22661] = { "Leatherworking", "craft" }, -- Polar Tunic
[22662] = { "Leatherworking", "craft" }, -- Polar Gloves
[22663] = { "Leatherworking", "craft" }, -- Polar Bracers
[22701] = { "Leatherworking", "craft" }, -- Polar Leggings
[24093] = { "Jewelcrafting", "craft" }, -- Pendant of Thawing
[31369] = { "Blacksmithing (Violet Eye)", "craft" }, -- Iceguard Breastplate
[31370] = { "Blacksmithing (Violet Eye)", "craft" }, -- Iceguard Leggings
[31371] = { "Blacksmithing (Violet Eye)", "craft" }, -- Iceguard Helm
[31398] = { "Jewelcrafting", "craft" }, -- The Frozen Eye
-- Nature Resistance gear (Hydross)
[22660] = { "Tailoring", "craft" }, -- Gaea's Embrace
[22759] = { "Leatherworking", "craft" }, -- Bramblewood Helm
[22760] = { "Leatherworking", "craft" }, -- Bramblewood Boots
[22761] = { "Leatherworking", "craft" }, -- Bramblewood Belt
[24095] = { "Jewelcrafting", "craft" }, -- Pendant of Withering
[31364] = { "Blacksmithing (Cenarion Expedition)", "craft" }, -- Wildguard Breastplate
[31367] = { "Blacksmithing (Cenarion Expedition)", "craft" }, -- Wildguard Leggings
[31368] = { "Blacksmithing (Cenarion Expedition)", "craft" }, -- Wildguard Helm
[31399] = { "Jewelcrafting", "craft" }, -- The Natural Ward
-- Non-crafted prebis replacements
[27775] = { "Grandmaster Vorpil — Shadow Labyrinth", "drop" }, -- Hallowed Pauldrons
[27911] = { "Epoch Hunter — Old Hillsbrad", "drop" }, -- Epoch's Whispering Cinch
[28204] = { "Pathaleon — Mechanar", "drop" }, -- Tunic of Assassination
[28230] = { "Murmur — Shadow Labyrinth", "drop" }, -- Hallowed Garments
[29241] = { "Heroic Mana-Tombs", "drop" }, -- Belt of Depravity
[29249] = { "Heroic Sethekk Halls", "drop" }, -- Bands of the Benevolent
[29250] = { "Heroic Sethekk Halls", "drop" }, -- Cord of Sanctification
[29356] = { "Aeonus — Heroic Black Morass", "drop" }, -- Quantum Blade
-- Mage Tank (Krosh)
[21859] = { "Tailoring", "craft" }, -- Imbued Netherweave Pants
[21860] = { "Tailoring", "craft" }, -- Imbued Netherweave Boots
[21861] = { "Tailoring", "craft" }, -- Imbued Netherweave Robe
[27465] = { "Warchief Kargath — Shattered Halls", "drop" }, -- Mana-Etched Gloves
[28190] = { "Aeonus — Black Morass", "drop" }, -- Scarab of the Infinite Cycle
[28191] = { "Epoch Hunter — Old Hillsbrad", "drop" }, -- Mana-Etched Vestments
[28193] = { "Aeonus — Black Morass", "drop" }, -- Mana-Etched Crown
-- Leveling 60-70 templates
[23517] = { "Blacksmithing", "craft" }, -- Felsteel Gloves
[24390] = { "Broggok — Blood Furnace", "drop" }, -- Auslese's Light Channeler
[24395] = { "Keli'dan — Blood Furnace", "drop" }, -- Mindfire Waistband
[24396] = { "Keli'dan — Blood Furnace", "drop" }, -- Vest of Vengeance
[24459] = { "Ghaz'an — Underbog", "drop" }, -- Cloak of Healing Rays
[25562] = { "Quest: Gurok the Usurper", "quest" }, -- Earthen Mark of Razing
[25634] = { "Quest: Gava'xi — Nagrand", "quest" }, -- Oshu'gun Relic
[25644] = { "Quest: The Ultimate Bloodsport — Nagrand", "quest" }, -- Blessed Book of Nagrand
[25645] = { "Quest: The Ultimate Bloodsport — Nagrand", "quest" }, -- Totem of the Plains
[25761] = { "Quest: Ring of Blood — Nagrand", "quest" }, -- Staff of Beasts
[25804] = { "Quest: Turning the Tide — Zangarmarsh", "quest" }, -- Naliko's Revenge
[25819] = { "Quest: Forge Camp: Annihilated — HFP", "quest" }, -- Breastplate of the Warbringer
[27411] = { "Exarch Maladaar — Auchenai Crypts", "drop" }, -- Slippers of Serenity
[27434] = { "Epoch Hunter — Old Hillsbrad", "drop" }, -- Mantle of Perenolde
[27474] = { "Warchief Kargath — Shattered Halls", "drop" }, -- Beast Lord Handguards
[27484] = { "The Maker — Blood Furnace", "drop" }, -- Libram of Avengement
[27518] = { "Quest: Sethekk Halls", "quest" }, -- Ivory Idol of the Moongoddess
[27771] = { "Hungarfen — Underbog", "drop" }, -- Doomplate Shoulderguards
[27804] = { "Kalithresh — Steamvault", "drop" }, -- Devilshark Cape
[27806] = { "Steamrigger — Steamvault", "drop" }, -- Fathomheart Gauntlets
[27868] = { "O'mrogg — Shattered Halls", "drop" }, -- Runesong Dagger
[27886] = { "Quest: Zangarmarsh", "quest" }, -- Idol of the Emerald Queen
[27887] = { "Hellmaw — Shadow Labyrinth", "drop" }, -- Platinum Shield of the Valorous
[27890] = { "Blackheart — Shadow Labyrinth", "drop" }, -- Wand of the Netherwing
[27892] = { "Blackheart — Shadow Labyrinth", "drop" }, -- Cloak of the Inciter
[27903] = { "Murmur — Shadow Labyrinth", "drop" }, -- Sonic Spear
[27905] = { "Murmur — Shadow Labyrinth", "drop" }, -- Greatsword of Horrid Dreams
[27907] = { "Black Stalker — Underbog", "drop" }, -- Mana-Etched Pantaloons
[27917] = { "Darkweaver Syth — Sethekk Halls", "drop" }, -- Libram of the Eternal Rest
[27918] = { "Darkweaver Syth — Sethekk Halls", "drop" }, -- Bands of Syth
[27925] = { "Talon King Ikiss — Sethekk Halls", "drop" }, -- Ravenclaw Band
[27981] = { "Talon King Ikiss — Sethekk Halls", "drop" }, -- Sethekk Oracle Cloak
[28031] = { "Quest: Bring Me The Egg! — Nagrand", "quest" }, -- Nomad's Woven Cloak
[28032] = { "Quest: Bring Me The Egg! — Nagrand", "quest" }, -- Delicate Green Poncho
[28034] = { "Temporus — Black Morass", "drop" }, -- Hourglass of the Unraveller
[28040] = { "Quest: Overlord — Hellfire Peninsula", "quest" }, -- Vengeance of the Illidari
[28041] = { "Quest: Overlord — Hellfire Peninsula", "quest" }, -- Bladefist's Breadth
[28042] = { "Quest: Overlord — Hellfire Peninsula", "quest" }, -- Regal Protectorate