-
Notifications
You must be signed in to change notification settings - Fork 74
Expand file tree
/
Copy pathItemUpgrades.lua
More file actions
2077 lines (1680 loc) · 77.6 KB
/
ItemUpgrades.lua
File metadata and controls
2077 lines (1680 loc) · 77.6 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
local addonName, addon = ...
local L = addon.locale.Get
if not (addon.game == "CLASSIC" or addon.game == "TBC" or addon.game == "CATA") then return end
local locale = GetLocale()
if not (locale == "enUS" or locale == "enGB" or locale == "frFR") then return end
local fmt, tinsert, ipairs, pairs, next, type, wipe, tonumber, strlower, smatch = string.format, table.insert, ipairs,
pairs, next, type, wipe, tonumber,
strlower, string.match
local GetItemInfo = C_Item and C_Item.GetItemInfo or _G.GetItemInfo
local GetItemInfoInstant = C_Item and C_Item.GetItemInfoInstant or _G.GetItemInfoInstant
local IsEquippedItem = C_Item and C_Item.IsEquippedItem or _G.IsEquippedItem
local GetItemStats = C_Item and C_Item.GetItemStats or _G.GetItemStats
local UnitLevel = _G.UnitLevel
local GetInventoryItemLink = _G.GetInventoryItemLink
local ItemArmorSubclass, ItemWeaponSubclass = Enum.ItemArmorSubclass, Enum.ItemWeaponSubclass
addon.itemUpgrades = addon:NewModule("ItemUpgrades", "AceEvent-3.0")
local session = {
isInitialized = false,
-- Loaded stat weights for class
-- Available spec weights, e.g. ele/enh or mageAoe/mageSingle
specWeights = {},
-- Active loaded stat weights
activeStatWeights = {},
-- Capturable regexes for tooltip parsing
statsRegexes = {},
-- Item stats cache
itemCache = {},
-- Track compatible
equippableSlots = {},
equippableArmor = {},
equippableWeapons = {},
weaponSlotToWeightKey = {},
-- TODO handle thread-safe?
comparisonTip = nil
}
-- TODO support spec awareness
-- Ignoring for now since overrides are rare and specific
local ITEM_WEIGHT_ADDITIONS = {
["DEATHKNIGHT"] = {},
["DRUID"] = {},
["HUNTER"] = {},
["MAGE"] = {},
["PALADIN"] = {},
["PRIEST"] = {},
["ROGUE"] = {},
["SHAMAN"] = {
-- [4908] = 12 -- Additional 12 EP for testing
},
["WARLOCK"] = {},
["WARRIOR"] = {}
}
local CLASS_MAP = {
["All"] = {
["Slot"] = {
["INVTYPE_HEAD"] = _G.INVSLOT_HEAD,
["INVTYPE_NECK"] = _G.INVSLOT_NECK,
["INVTYPE_SHOULDER"] = _G.INVSLOT_SHOULDER,
["INVTYPE_BODY"] = _G.INVSLOT_BODY,
["INVTYPE_CHEST"] = _G.INVSLOT_CHEST,
["INVTYPE_ROBE"] = _G.INVSLOT_CHEST,
["INVTYPE_WAIST"] = _G.INVSLOT_WAIST,
["INVTYPE_LEGS"] = _G.INVSLOT_LEGS,
["INVTYPE_FEET"] = _G.INVSLOT_FEET,
["INVTYPE_WRIST"] = _G.INVSLOT_WRIST,
["INVTYPE_HAND"] = _G.INVSLOT_HAND,
["INVTYPE_FINGER"] = {[_G.INVSLOT_FINGER1] = _G.INVSLOT_FINGER1, [_G.INVSLOT_FINGER2] = _G.INVSLOT_FINGER2},
["INVTYPE_TRINKET"] = {
[_G.INVSLOT_TRINKET1] = _G.INVSLOT_TRINKET1,
[_G.INVSLOT_TRINKET2] = _G.INVSLOT_TRINKET2
},
["INVTYPE_CLOAK"] = _G.INVSLOT_BACK,
["INVTYPE_HOLDABLE"] = _G.INVSLOT_OFFHAND,
["INVTYPE_WEAPONMAINHAND"] = _G.INVSLOT_MAINHAND,
["INVTYPE_WEAPON"] = _G.INVSLOT_MAINHAND,
["INVTYPE_2HWEAPON"] = _G.INVSLOT_MAINHAND
},
["ArmorType"] = {
[ItemArmorSubclass.Generic] = true, -- Trinkets, rings, necks
[ItemArmorSubclass.Cloth] = true -- Cloaks plus cloth armor
},
["WeaponType"] = {[ItemWeaponSubclass.Generic] = true}
},
["DEATHKNIGHT"] = {
["Slot"] = {
["INVTYPE_THROWN"] = _G.INVSLOT_RANGED,
["INVTYPE_RANGEDRIGHT"] = _G.INVSLOT_RANGED,
["INVTYPE_SHIELD"] = _G.INVSLOT_OFFHAND,
["INVTYPE_WEAPONOFFHAND"] = _G.INVSLOT_OFFHAND
},
["ArmorType"] = {
[ItemArmorSubclass.Leather] = true,
[ItemArmorSubclass.Mail] = true,
[ItemArmorSubclass.Plate] = true -- DK always 55+
},
["WeaponType"] = {
[ItemWeaponSubclass.Axe1H] = true,
[ItemWeaponSubclass.Axe2H] = true,
[ItemWeaponSubclass.Mace1H] = true,
[ItemWeaponSubclass.Mace2H] = true,
[ItemWeaponSubclass.Polearm] = function() return UnitLevel("player") >= 18 end,
[ItemWeaponSubclass.Sword1H] = true,
[ItemWeaponSubclass.Sword2H] = true,
[ItemWeaponSubclass.Unarmed] = true
}
},
["DRUID"] = {
["Slot"] = {},
["ArmorType"] = {[ItemArmorSubclass.Leather] = true},
["WeaponType"] = {
[ItemWeaponSubclass.Mace1H] = true,
[ItemWeaponSubclass.Mace2H] = true,
[ItemWeaponSubclass.Staff] = true,
[ItemWeaponSubclass.Unarmed] = true,
[ItemWeaponSubclass.Dagger] = true
}
},
["HUNTER"] = {
["Slot"] = {
["INVTYPE_THROWN"] = _G.INVSLOT_RANGED,
["INVTYPE_RANGEDRIGHT"] = _G.INVSLOT_RANGED,
["INVTYPE_RANGED"] = _G.INVSLOT_RANGED,
["INVTYPE_WEAPONOFFHAND"] = function()
return UnitLevel("player") >= 18 and _G.INVSLOT_OFFHAND or nil
end
},
["ArmorType"] = {
[ItemArmorSubclass.Leather] = true,
[ItemArmorSubclass.Mail] = function() return UnitLevel("player") >= 35 end
},
["WeaponType"] = {
[ItemWeaponSubclass.Axe1H] = true,
[ItemWeaponSubclass.Axe2H] = true,
[ItemWeaponSubclass.Bows] = true,
[ItemWeaponSubclass.Guns] = true,
[ItemWeaponSubclass.Polearm] = function() return UnitLevel("player") >= 18 end,
[ItemWeaponSubclass.Sword1H] = true,
[ItemWeaponSubclass.Sword2H] = true,
[ItemWeaponSubclass.Staff] = true,
[ItemWeaponSubclass.Unarmed] = true,
[ItemWeaponSubclass.Dagger] = true,
[ItemWeaponSubclass.Crossbow] = true
}
},
["MAGE"] = {
["Slot"] = {["INVTYPE_RANGEDRIGHT"] = _G.INVSLOT_RANGED},
["ArmorType"] = {},
["WeaponType"] = {
[ItemWeaponSubclass.Sword1H] = true,
[ItemWeaponSubclass.Dagger] = true,
[ItemWeaponSubclass.Staff] = true,
[ItemWeaponSubclass.Wand] = true
}
},
["PALADIN"] = {
["Slot"] = {["INVTYPE_SHIELD"] = _G.INVSLOT_OFFHAND},
["ArmorType"] = {
[ItemArmorSubclass.Shield] = true,
[ItemArmorSubclass.Leather] = true,
[ItemArmorSubclass.Mail] = true,
[ItemArmorSubclass.Plate] = function() return UnitLevel("player") >= 35 end
},
["WeaponType"] = {
[ItemWeaponSubclass.Axe1H] = true,
[ItemWeaponSubclass.Axe2H] = true,
[ItemWeaponSubclass.Mace1H] = true,
[ItemWeaponSubclass.Mace2H] = true,
[ItemWeaponSubclass.Polearm] = function() return UnitLevel("player") >= 18 end,
[ItemWeaponSubclass.Sword1H] = true,
[ItemWeaponSubclass.Sword2H] = true
}
},
["PRIEST"] = {
["Slot"] = {["INVTYPE_RANGEDRIGHT"] = _G.INVSLOT_RANGED},
["ArmorType"] = {},
["WeaponType"] = {
[ItemWeaponSubclass.Mace1H] = true,
[ItemWeaponSubclass.Dagger] = true,
[ItemWeaponSubclass.Staff] = true,
[ItemWeaponSubclass.Wand] = true
}
},
["ROGUE"] = {
["Slot"] = {
["INVTYPE_THROWN"] = _G.INVSLOT_RANGED,
["INVTYPE_RANGEDRIGHT"] = _G.INVSLOT_RANGED,
["INVTYPE_RANGED"] = _G.INVSLOT_RANGED,
["INVTYPE_WEAPONOFFHAND"] = function()
return UnitLevel("player") >= 10 and _G.INVSLOT_OFFHAND or nil
end
},
["ArmorType"] = {[ItemArmorSubclass.Leather] = true},
["WeaponType"] = {
[ItemWeaponSubclass.Bows] = true,
[ItemWeaponSubclass.Guns] = true,
[ItemWeaponSubclass.Mace1H] = true,
[ItemWeaponSubclass.Sword1H] = true,
[ItemWeaponSubclass.Unarmed] = true,
[ItemWeaponSubclass.Dagger] = true,
[ItemWeaponSubclass.Thrown] = true,
[ItemWeaponSubclass.Crossbow] = true
}
},
["SHAMAN"] = {
["Slot"] = {{["INVTYPE_SHIELD"] = _G.INVSLOT_OFFHAND}},
["ArmorType"] = {
[ItemArmorSubclass.Shield] = true,
[ItemArmorSubclass.Leather] = true,
[ItemArmorSubclass.Mail] = function() return UnitLevel("player") >= 35 end
},
["WeaponType"] = {
[ItemWeaponSubclass.Axe1H] = true,
[ItemWeaponSubclass.Axe2H] = true,
[ItemWeaponSubclass.Mace1H] = true,
[ItemWeaponSubclass.Mace2H] = true,
[ItemWeaponSubclass.Staff] = true,
[ItemWeaponSubclass.Unarmed] = true,
[ItemWeaponSubclass.Dagger] = true
}
},
["WARLOCK"] = {
["Slot"] = {["INVTYPE_RANGEDRIGHT"] = _G.INVSLOT_RANGED},
["ArmorType"] = {},
["WeaponType"] = {
[ItemWeaponSubclass.Sword1H] = true,
[ItemWeaponSubclass.Dagger] = true,
[ItemWeaponSubclass.Staff] = true,
[ItemWeaponSubclass.Wand] = true
}
},
["WARRIOR"] = {
["Slot"] = {
["INVTYPE_THROWN"] = _G.INVSLOT_RANGED,
["INVTYPE_RANGEDRIGHT"] = _G.INVSLOT_RANGED,
["INVTYPE_SHIELD"] = _G.INVSLOT_OFFHAND,
["INVTYPE_WEAPONOFFHAND"] = function()
return UnitLevel("player") >= 18 and _G.INVSLOT_OFFHAND or nil
end
},
["ArmorType"] = {
[ItemArmorSubclass.Shield] = true,
[ItemArmorSubclass.Leather] = true,
[ItemArmorSubclass.Mail] = true,
[ItemArmorSubclass.Plate] = function() return UnitLevel("player") >= 35 end
},
["WeaponType"] = {
[ItemWeaponSubclass.Axe1H] = true,
[ItemWeaponSubclass.Axe2H] = true,
[ItemWeaponSubclass.Bows] = true,
[ItemWeaponSubclass.Guns] = true,
[ItemWeaponSubclass.Mace1H] = true,
[ItemWeaponSubclass.Mace2H] = true,
[ItemWeaponSubclass.Polearm] = function() return UnitLevel("player") >= 18 end,
[ItemWeaponSubclass.Sword1H] = true,
[ItemWeaponSubclass.Sword2H] = true,
[ItemWeaponSubclass.Staff] = true,
[ItemWeaponSubclass.Unarmed] = true,
[ItemWeaponSubclass.Thrown] = true,
[ItemWeaponSubclass.Dagger] = true,
[ItemWeaponSubclass.Crossbow] = true
}
}
}
-- Map quasi-friendly key from GSheet/StatWeights to regex-friendly value
-- GSheet or pretty name = Regex formatting
local KEY_TO_TEXT = {
['ITEM_MOD_STRENGTH_SHORT'] = _G.ITEM_MOD_STRENGTH,
['ITEM_MOD_AGILITY_SHORT'] = _G.ITEM_MOD_AGILITY,
['ITEM_MOD_INTELLECT_SHORT'] = _G.ITEM_MOD_INTELLECT,
['ITEM_MOD_STAMINA_SHORT'] = _G.ITEM_MOD_STAMINA,
['ITEM_MOD_SPIRIT_SHORT'] = _G.ITEM_MOD_SPIRIT,
['ITEM_MOD_HEALTH_REGEN_SHORT'] = _G.ITEM_MOD_HEALTH_REGEN,
['ITEM_MOD_POWER_REGEN0_SHORT'] = _G.ITEM_MOD_MANA_REGENERATION,
['ITEM_MOD_SPELL_HEALING_DONE'] = _G.ITEM_MOD_SPELL_HEALING_DONE,
['ITEM_MOD_HIT_SPELL_RATING_SHORT'] = _G.ITEM_MOD_HIT_SPELL_RATING,
['ITEM_MOD_CRIT_SPELL_RATING_SHORT'] = _G.ITEM_MOD_CRIT_SPELL_RATING,
['ITEM_MOD_RANGED_ATTACK_POWER_SHORT'] = _G.ITEM_MOD_RANGED_ATTACK_POWER,
['ITEM_MOD_DEFENSE_SKILL_RATING_SHORT'] = _G.ITEM_MOD_DEFENSE_SKILL_RATING
-- Data in GetItemStats
-- ['ITEM_MOD_DAMAGE_PER_SECOND_SHORT'] = _G.DPS_TEMPLATE,
-- ['ITEM_MOD_SPELL_DAMAGE_DONE'] = { -- CANNOT BE TRUSTED, replaced by parsing STAT_SPELLDAMAGE
-- _G.ITEM_MOD_SPELL_POWER, _G.ITEM_MOD_SPELL_DAMAGE_DONE
-- },
-- Wrong global variable for text, unable to find corresponding easily
-- ['ITEM_MOD_HIT_RATING_SHORT'] = _G.ITEM_MOD_HIT_RATING,
-- ['ITEM_MOD_CRIT_RATING_SHORT'] = _G.ITEM_MOD_CRIT_RATING,
-- ['ITEM_MOD_DODGE_RATING_SHORT'] = _G.ITEM_MOD_DODGE_RATING,
-- ['ITEM_MOD_PARRY_RATING_SHORT'] = _G.ITEM_MOD_PARRY_RATING
-- ['ITEM_MOD_ATTACK_POWER_SHORT'] = _G.ITEM_MOD_ATTACK_POWER,
}
if addon.game == "CATA" then
KEY_TO_TEXT['ITEM_MOD_MASTERY_RATING_SHORT'] = _G.ITEM_MOD_MASTERY_RATING
KEY_TO_TEXT['ITEM_MOD_HIT_RANGED_RATING_SHORT'] = _G.ITEM_MOD_HIT_RANGED_RATING
KEY_TO_TEXT['ITEM_MOD_CRIT_RANGED_RATING_SHORT'] = _G.ITEM_MOD_CRIT_RANGED_RATING
KEY_TO_TEXT['ITEM_MOD_SPELL_PENETRATION_SHORT'] = _G.ITEM_MOD_SPELL_PENETRATION
KEY_TO_TEXT['ITEM_MOD_HEALTH_REGEN_SHORT'] = _G.ITEM_MOD_HEALTH_REGEN
KEY_TO_TEXT['ITEM_MOD_BLOCK_RATING_SHORT'] = _G.ITEM_MOD_BLOCK_RATING
KEY_TO_TEXT['ITEM_MOD_RESILIENCE_RATING_SHORT'] = _G.ITEM_MOD_RESILIENCE_RATING
end
-- Keys only obtained from tooltip text parsing
-- Explicitly set regex
local OUT_OF_BAND_KEYS = {
['ITEM_MOD_CR_SPEED_SHORT'] = _G.ITEM_MOD_CR_SPEED_SHORT .. "%s+(%d+%.%d+)",
['ITEM_MOD_CRIT_RATING_SHORT'] = "%s+Improves your chance to get a critical strike by (%d+)%%.",
['ITEM_MOD_HIT_RATING_SHORT'] = "%s+Improves your chance to hit by (%d+)%%.",
['ITEM_MOD_DODGE_RATING_SHORT'] = "%s+Increases your chance to dodge an attack by (%d+)%%.",
['ITEM_MOD_PARRY_RATING_SHORT'] = "%s+Increases your chance to parry an attack by (%d+)%%.",
['ITEM_MOD_ATTACK_POWER_SHORT'] = "%s+%+(%d+)%s+" .. ITEM_MOD_ATTACK_POWER_SHORT,
-- Stats cannot be trusted, explicitly parse
-- Overrides ITEM_MOD_SPELL_DAMAGE_DONE built-in
['STAT_SPELLDAMAGE'] = {_G.ITEM_MOD_SPELL_POWER, _G.ITEM_MOD_SPELL_DAMAGE_DONE}
}
local WEAPON_SLOT_MAP = {
['2H'] = {['Slot'] = {["INVTYPE_2HWEAPON"] = _G.INVSLOT_MAINHAND}},
['MH'] = {['Slot'] = {["INVTYPE_WEAPON"] = _G.INVSLOT_MAINHAND, ["INVTYPE_WEAPONMAINHAND"] = _G.INVSLOT_MAINHAND}},
['OH'] = {['Slot'] = {["INVTYPE_WEAPON"] = _G.INVSLOT_OFFHAND, ["INVTYPE_WEAPONOFFHAND"] = _G.INVSLOT_OFFHAND}},
['RANGED'] = {
["Slot"] = {
["INVTYPE_THROWN"] = _G.INVSLOT_RANGED,
["INVTYPE_RANGED"] = _G.INVSLOT_RANGED,
["INVTYPE_RANGEDRIGHT"] = _G.INVSLOT_RANGED
}
}
}
-- Used for dpsWeights post-processing
local SPEED_SUFFIX_SLOT_MAP = {
['2H'] = _G.INVSLOT_MAINHAND,
['MH'] = _G.INVSLOT_MAINHAND,
['OH'] = _G.INVSLOT_OFFHAND,
['RANGED'] = _G.INVSLOT_RANGED
}
local SPEED_SUFFIX_NAME_MAP = {
['2H'] = _G.INVTYPE_2HWEAPON,
['MH'] = _G.INVTYPE_WEAPONMAINHAND,
['OH'] = _G.INVTYPE_WEAPONOFFHAND,
['RANGED'] = _G.INVTYPE_RANGED
}
-- Turn GSheet suffix
local SPELL_KIND_MAP = {
-- SPELL_SCHOOL1_NAME = "STAT_SPELLDAMAGE_HOLY",
[SPELL_SCHOOL2_NAME] = "STAT_SPELLDAMAGE_FIRE",
[SPELL_SCHOOL3_NAME] = "STAT_SPELLDAMAGE_NATURE",
[SPELL_SCHOOL4_NAME] = "STAT_SPELLDAMAGE_FROST",
[SPELL_SCHOOL5_NAME] = "STAT_SPELLDAMAGE_SHADOW",
[SPELL_SCHOOL6_NAME] = "STAT_SPELLDAMAGE_ARCANE"
}
local SPELL_KIND_MATCH = "Increases damage done by (%a+) spells and effects by up to (%d+)."
if locale == 'frFR' then
SPELL_KIND_MATCH = "Augmente les dégâts infligés par les sorts et effets d?e?'? ?(%a+) de (%d+) au maximum."
-- Comma decimal delimiter
OUT_OF_BAND_KEYS['ITEM_MOD_CR_SPEED_SHORT'] = _G.ITEM_MOD_CR_SPEED_SHORT .. "%s+(%d+,%d+)"
OUT_OF_BAND_KEYS['ITEM_MOD_CRIT_RATING_SHORT'] = "%s+Augmente vos chances d'infliger un coup critique de (%d+)%%."
OUT_OF_BAND_KEYS['ITEM_MOD_HIT_RATING_SHORT'] = "%s+Augmente vos chances de toucher de (%d+)%%."
OUT_OF_BAND_KEYS['ITEM_MOD_DODGE_RATING_SHORT'] = "%s+Augmente vos chances d'esquiver une attaque de (%d+)%%."
OUT_OF_BAND_KEYS['ITEM_MOD_PARRY_RATING_SHORT'] = "%s+Augmente vos chances de parer une attaque de (%d+)%%."
end
local SPEC_MAP = {
["WARRIOR"] = {[1] = "Arms", [2] = "Fury", [3] = "Protection"},
["PALADIN"] = {[1] = "Holy", [2] = "Protection", [3] = "Retribution"},
["HUNTER"] = {[1] = "Beast Mastery", [2] = "Marksmanship", [3] = "Survival"},
["ROGUE"] = {[1] = "Assassination", [2] = "Combat", [3] = "Subtlety"},
["PRIEST"] = {[1] = "Discipline", [2] = "Holy", [3] = "Shadow"},
["SHAMAN"] = {[1] = "Elemental", [2] = "Enhancement", [3] = "Restoration"},
["MAGE"] = {[1] = "Arcane", [2] = "Fire", [3] = "Frost"},
["WARLOCK"] = {[1] = "Affliction", [2] = "Demonology", [3] = "Destruction"},
["DRUID"] = {[1] = "Balance", [2] = "Feral Combat", [4] = "Restoration"},
["DEATHKNIGHT"] = {[1] = "Blood", [2] = "Frost", [4] = "Unholy"}
}
-- Setup reverse lookup in session.weaponSlotToWeightKey
for weaponKey, d in pairs(WEAPON_SLOT_MAP) do
if not d.Slot then
addon.error("Incomplete WEAPON_SLOT_MAP slot data for", weaponKey)
return
end
for itemEquipLoc, _ in pairs(d.Slot) do
if not session.weaponSlotToWeightKey[itemEquipLoc] then session.weaponSlotToWeightKey[itemEquipLoc] = {} end
-- ['INVTYPE_WEAPON'] = { "MH", "OH" }
tinsert(session.weaponSlotToWeightKey[itemEquipLoc], weaponKey)
end
end
local function regexify(input)
-- Replace '%s' with '(%d+)' to match numbers
-- Remove leading control characters on stats
return input:gsub("%%[ds]", "(%%d%+)"):gsub("^%%c", '')
end
-- Maps regex global string with stat rating key
-- Turn descriptive text into number friendly regexes
local function KeyToRegex(keyString)
if session.statsRegexes[keyString] then return session.statsRegexes[keyString] end
local regex = KEY_TO_TEXT[keyString]
-- Return nil for keys without mappings
if not regex then return end
if type(regex) == "table" then
for i, _ in ipairs(regex) do regex[i] = regexify(regex[i]) end
else
regex = regexify(regex)
end
session.statsRegexes[keyString] = regex
return regex
end
local function prettyPrintRatio(ratio)
if not ratio then return "NaN" end
local percentage
if ratio == 1 then
return '100%'
elseif ratio > 0 then
percentage = ((ratio * 100) - 100)
elseif ratio == 0 then
return '0%'
else -- < 0
percentage = (ratio * 100)
end
return fmt("%.2f%%", percentage)
end
local function IsWeaponSlot(itemEquipLoc)
return itemEquipLoc == 'INVTYPE_WEAPON' or itemEquipLoc == 'INVTYPE_RANGED' or itemEquipLoc == 'INVTYPE_2HWEAPON' or
itemEquipLoc == 'INVTYPE_WEAPONMAINHAND' or itemEquipLoc == 'INVTYPE_WEAPONOFFHAND' or itemEquipLoc ==
'INVTYPE_THROWN' or itemEquipLoc == 'INVTYPE_RANGEDRIGHT'
end
local function IsMeleeSlot(itemEquipLoc)
return itemEquipLoc == 'INVTYPE_WEAPON' or itemEquipLoc == 'INVTYPE_2HWEAPON' or itemEquipLoc ==
'INVTYPE_WEAPONMAINHAND' or itemEquipLoc == 'INVTYPE_WEAPONOFFHAND'
end
local function enableTotalEPLines(itemData, lines)
if itemData.dpsWeights then -- IsWeaponSlot equivalent
for suffix, data in pairs(itemData.dpsWeights) do
tinsert(lines, fmt(" Total EP (%s): %.2f", SPEED_SUFFIX_NAME_MAP[suffix], data.totalWeight))
end
else -- Armor
tinsert(lines, fmt(" Total EP: %.2f", itemData.totalWeight))
end
end
local function TooltipSetItem(tooltip, ...)
if not addon.settings.profile.enableItemUpgrades or not addon.settings.profile.enableTips then return end
local _, itemLink = tooltip:GetItem()
if not itemLink then return end
-- print("TooltipSetItem", tooltip:GetName(), itemLink)
local itemData = addon.itemUpgrades:GetItemData(itemLink, tooltip)
if not (itemData and itemData.totalWeight) then return end
local lines = {}
-- Exclude addon text when looking at an equipped item
-- Unless enableTotalEP
if IsEquippedItem(itemLink) then
if addon.settings.profile.enableTotalEP then
enableTotalEPLines(itemData, lines)
if #lines > 0 then
tooltip:AddLine(fmt("%s - %s", addon.title, _G.ITEM_UPGRADE))
for _, line in ipairs(lines) do tooltip:AddLine(line) end
end
end
return
end
local statComparisons = addon.itemUpgrades:CompareItemWeight(itemLink, tooltip)
-- Effectively only used when an item downgrade
-- TODO when weapons have stats, this may be a problem
if not statComparisons or next(statComparisons) == nil then
if addon.settings.profile.enableTotalEP then
enableTotalEPLines(itemData, lines)
if #lines > 0 then
tooltip:AddLine(fmt("%s - %s SC", addon.title, _G.ITEM_UPGRADE))
for _, line in ipairs(lines) do tooltip:AddLine(line) end
end
end
return
end
local lineText
local equippedWeaponWeight, comparedWeaponWeight
for _, statsData in ipairs(statComparisons) do
lineText = nil
if IsWeaponSlot(statsData.itemEquipLoc) then
-- DpsWeights exists when weapon, but only if not Empty
for suffix, comparisonDpsData in pairs(statsData.DpsWeights or {}) do
-- Only compare weights if they are compatible
if itemData.dpsWeights[suffix] then
equippedWeaponWeight = statsData.TotalWeight + comparisonDpsData.totalWeight
comparedWeaponWeight = itemData.totalWeight + itemData.dpsWeights[suffix].totalWeight
if statsData['ItemLink'] == _G.EMPTY then
lineText = fmt(" %s (%s): +%.2f EP", _G.EMPTY, SPEED_SUFFIX_NAME_MAP[suffix],
comparedWeaponWeight)
else
lineText = fmt(" %s (%s): %s / +%.2f EP", statsData['ItemLink'], SPEED_SUFFIX_NAME_MAP[suffix],
prettyPrintRatio(comparedWeaponWeight / equippedWeaponWeight),
comparedWeaponWeight - equippedWeaponWeight)
end
if statsData['debug'] and addon.settings.profile.debug then
lineText = fmt("%s (%s)", lineText, statsData['debug'])
end
-- Limit dual wielding comparison output to weapon in the specific slot
-- E.g. Compare a new 1H against current 1H in MH slot, not against that 1H in MH and OH slot (4 lines)
if statsData.itemEquipLoc == 'INVTYPE_WEAPON' or statsData.itemEquipLoc == 'INVTYPE_WEAPONOFFHAND' then
if statsData['SlotCompared'] == _G.INVSLOT_MAINHAND and suffix == "MH" then
tinsert(lines, lineText)
elseif statsData['SlotCompared'] == _G.INVSLOT_OFFHAND and suffix == "OH" then
tinsert(lines, lineText)
-- else -- ignore cross-hand comparisons in tooltip
end
else
-- Add a comparison line for every statComparison, should be 1 except for 1H weapons
tinsert(lines, lineText)
end
end
end
else
if statsData['Ratio'] then
lineText = fmt(" %s: %s / +%.2f EP", statsData['ItemLink'] or _G.UNKNOWN,
prettyPrintRatio(statsData['Ratio']), statsData.WeightIncrease)
elseif statsData['ItemLink'] == _G.EMPTY then
lineText = fmt(" %s: +%s EP", _G.EMPTY, statsData.WeightIncrease)
end
if lineText then tinsert(lines, lineText) end
end
end
if #lines > 0 then
tooltip:AddLine(fmt("%s - %s", addon.title, _G.ITEM_UPGRADE))
if addon.settings.profile.enableTotalEP then enableTotalEPLines(itemData, lines) end
for _, line in ipairs(lines) do tooltip:AddLine(line) end
end
tooltip:Show()
end
function addon.itemUpgrades:UpdateSlotMap()
session.equippableSlots = CLASS_MAP["All"]["Slot"]
for k, v in pairs(CLASS_MAP[addon.player.class]["Slot"]) do
if type(v) == "function" then v = v() end
session.equippableSlots[k] = v
end
session.equippableArmor = CLASS_MAP["All"]["ArmorType"]
for k, v in pairs(CLASS_MAP[addon.player.class]["ArmorType"]) do
if type(v) == "function" then v = v() end
session.equippableArmor[k] = v
end
session.equippableWeapons = CLASS_MAP["All"]["WeaponType"]
for k, v in pairs(CLASS_MAP[addon.player.class]["WeaponType"]) do
if type(v) == "function" then v = v() end
session.equippableWeapons[k] = v
end
end
function addon.itemUpgrades:Setup()
-- Toggle functionality off
if not addon.settings.profile.enableItemUpgrades or not addon.settings.profile.enableTips then return end
if UnitLevel("player") == GetMaxPlayerLevel() then return end
self:UpdateSlotMap()
if not self:LoadStatWeights() then return end
if not self:ActivateSpecWeights() then return end
session.itemCache = {}
-- Only register events and hookScript once
if session.isInitialized then return end
self:RegisterEvent("PLAYER_LEVEL_UP")
local lookup
-- Only load stats coming from GSheet
for key, _ in pairs(session.activeStatWeights) do
-- print("Checking", key)
lookup = KeyToRegex(key)
if lookup then
-- print("Match loaded", lookup)
session.statsRegexes[key] = lookup
end
end
-- Add out-of-band (aka hackery) stat parsing
for key, regex in pairs(OUT_OF_BAND_KEYS) do session.statsRegexes[key] = regex end
-- Inventory
GameTooltip:HookScript("OnTooltipSetItem", TooltipSetItem)
-- Vendor?
ItemRefTooltip:HookScript("OnTooltipSetItem", TooltipSetItem)
-- Enable AH
ShoppingTooltip1:HookScript("OnTooltipSetItem", TooltipSetItem)
-- ShoppingTooltip2:HookScript("OnTooltipSetItem", TooltipSetItem)
session.isInitialized = true
self.AH:Setup()
end
-- Reset cache on levelup
function addon.itemUpgrades:PLAYER_LEVEL_UP()
if not addon.settings.profile.enableItemUpgrades then return end
addon.itemUpgrades:Setup()
end
function addon.itemUpgrades:LoadStatWeights()
if not addon.statWeights then return end
local newWeights = {}
local guideMode = addon.settings.profile.hardcore and "HARDCORE" or "SPEEDRUN"
-- TODO chance this doesn't evaluate properly on PLAYER_LEVEL_UP event
local playerLevel = UnitLevel("player")
for dbTitle, data in pairs(addon.statWeights) do
if data.MAX_LEVEL <= data.MIN_LEVEL then
addon.comms.PrettyPrint("Invalid min (%s) and max %s level for for %s", data.MIN_LEVEL, data.MAX_LEVEL,
dbTitle)
end
if strupper(data.Class) == addon.player.class and strupper(data.Kind) == guideMode and playerLevel >=
data.MIN_LEVEL and playerLevel <= data.MAX_LEVEL then
newWeights[data.Spec or data.Class] = data
-- print("Loaded statWeights", data.Title)
-- print("Loaded statWeights, level:", playerLevel, data.MIN_LEVEL, data.MAX_LEVEL)
end
end
for spec, data in pairs(newWeights) do
for kind, value in pairs(data) do
-- Optimization: remove all 0 stats
if tonumber(value) and value == 0 then
-- print("Removed", spec .. ':' .. kind)
data[kind] = nil
end
end
-- SoD
if addon.player.season == 3 and data['ITEM_MOD_SPIRIT_SHORT'] then
if addon.player.class == "PRIEST" then
data['ITEM_MOD_SPIRIT_SHORT'] = data['ITEM_MOD_SPIRIT_SHORT'] * 0.75
else
data['ITEM_MOD_SPIRIT_SHORT'] = data['ITEM_MOD_SPIRIT_SHORT'] * 0.5
end
end
end
session.specWeights = newWeights
return session.specWeights ~= nil
end
local function getSpec()
-- Classes with className as spec only have one (Rogue, Warrior), use that
if session.specWeights[addon.player.class] then return addon.player.class end
-- if addon.settings.profile.enableTalentGuides then
-- -- Difficult/impossible to map talent guide
-- -- RXPCData.activeTalentGuide == "Rogue - Hardcore Rogue 10-60"
-- end
-- Calculate most likely spec
local pointsSpent
local guessedSpec = {index = nil, count = 0}
for tabIndex = 1, _G.GetNumTalentTabs(false) do
-- id, name, description, icon, pointsSpent, background, previewPointsSpent, isUnlocked
local arg3
_, _, arg3, _, pointsSpent = _G.GetTalentTabInfo(tabIndex)
if type(arg3) == "number" then
pointsSpent = arg3
end
if pointsSpent > guessedSpec.count then
guessedSpec.index = tabIndex
guessedSpec.count = pointsSpent
end
end
local specName
-- No tabs found with > 0 talents, likely fresh character
if guessedSpec.index then
specName = SPEC_MAP[addon.player.class][guessedSpec.index]
addon.comms.PrettyDebug("ItemUpgrades, spec guessed as %s", specName)
end
-- If calculated spec has no weights, then class is unsupported
-- Likely exited earlier with Rogue/Warrior in this scenario then
if session.specWeights[specName] then return specName end
-- If no class-wide spec and no talents, then fallback to arbitrarily pick the first loaded spec
specName, _ = next(session.specWeights)
-- Returns first specName, or nil
return specName
end
-- Always run after LoadStatWeights
function addon.itemUpgrades:ActivateSpecWeights()
if not session.specWeights then return end
local spec = getSpec()
-- Uninitialized spec, so set to calculated value
if not addon.settings.profile.itemUpgradeSpec then
addon.settings.profile.itemUpgradeSpec = spec
elseif addon.settings.profile.itemUpgradeSpec ~= spec then
-- Handle spec name changes
if not session.specWeights[addon.settings.profile.itemUpgradeSpec] then
addon.settings.profile.itemUpgradeSpec = spec
end
-- Chosen talents don't match itemUpgradeSpec
-- Leave alone as is, don't spam user if there's a mismatch
addon.comms.PrettyDebug("ItemUpgrades selected spec (%s) differs from calculated spec (%s)",
addon.settings.profile.itemUpgradeSpec, spec)
end
if not addon.settings.profile.itemUpgradeSpec then return end
addon.comms.PrettyDebug("Activating spec weights for %s", addon.settings.profile.itemUpgradeSpec)
session.activeStatWeights = session.specWeights[addon.settings.profile.itemUpgradeSpec]
if not session.activeStatWeights then return end
session.activeStatWeights.extraWeight = ITEM_WEIGHT_ADDITIONS[addon.player.class] or {}
return session.activeStatWeights ~= nil
end
function addon.itemUpgrades:GetSpecWeights()
local options = {}
for k, _ in pairs(session.specWeights) do options[k] = k end
-- No current support for class, spec, etc
if next(options) == nil then return end
return options
end
-- ITEM_SET_NAME = "%s (%d/%d)";
local SET_BONUS_MATCH = "(%w+)%s+%((%d+)/(%d+)%)"
local function GetTooltipLines(tooltip, baseItemData)
local textLines = {}
-- print("GetTooltipLines, tooltip", tooltip:GetName(), tooltip:NumLines())
-- Something went wrong
if not tooltip or not tooltip.NumLines or tooltip:NumLines() == 0 then return end
local regions = {tooltip:GetRegions()}
local rText
local setMatch = {}
for _, r in ipairs(regions) do
if r:IsObjectType("FontString") and r:GetText() then
rText = r:GetText()
-- print("GetTooltipLines, regions", rText)
-- Set bonus, so stop gathering lines past set bonus
if baseItemData and baseItemData.setID then
-- print("GetTooltipLines, checking for set bonus line '" .. rText .. "'")
setMatch = {smatch(rText, SET_BONUS_MATCH)}
if setMatch[1] and setMatch[2] and setMatch[3] then
-- print("GetTooltipLines, aborting at set bonuses", rText)
break
end
end
tinsert(textLines, rText)
end
end
return textLines
end
local function GetComparisonTip()
if session.comparisonTip then return session.comparisonTip end
session.comparisonTip = CreateFrame("GameTooltip", "RXPItemUpgradesComparison", nil, "GameTooltipTemplate")
session.comparisonTip:SetOwner(WorldFrame, "ANCHOR_NONE")
session.comparisonTip:AddFontStrings(session.comparisonTip:CreateFontString("$parentTextLeft1", nil,
"GameTooltipText"),
session.comparisonTip:CreateFontString("$parentTextRight1", nil,
"GameTooltipText"))
return session.comparisonTip
end
local function IsUsableForClass(itemSubTypeID, itemEquipLoc)
if type(itemSubTypeID) ~= "number" then
addon.error("IsUsableForClass, itemSubTypeID number required")
return
end
if type(itemEquipLoc) ~= "string" then
addon.error("IsUsableForClass, itemEquipLoc string required")
return
end
if IsWeaponSlot(itemEquipLoc) then
if not session.equippableWeapons[itemSubTypeID] then return end
else
if not session.equippableArmor[itemSubTypeID] then return end
end
-- A fully trained class will get to here, but that's not the case when leveling
-- TODO exclude untrained weapon types
-- https://www.wowhead.com/classic/spells/proficiencies?filter=22;1;0
return true
end
local function CalculateDPSWeight(itemData, stats, itemEquipLoc)
-- Example:
-- itemData = {
-- ['itemEquipLoc'] = 'INVTYPE_RANGED',
-- ...
-- }
-- stats = {
-- ['ITEM_MOD_DAMAGE_PER_SECOND_SHORT'] = 12.3456789,
-- ...
-- }
local dpsWeights = {}
-- This only happens if an empty slot comparison
-- Fake a 0 weight base item to preserve upstream comparison logic
if itemEquipLoc and not itemData then
for _, keySuffix in ipairs(session.weaponSlotToWeightKey[itemEquipLoc] or {}) do
dpsWeights[keySuffix] = {['totalWeight'] = 0.00, ['speedWeight'] = 0.00}
end
return dpsWeights
end
-- Shield gets here from being INVTYPE_OFFHAND
if itemData.itemEquipLoc == "INVTYPE_SHIELD" then return end
if not stats or not stats['ITEM_MOD_CR_SPEED_SHORT'] then
addon.comms.PrettyDebug("itemUpgrades CalculateDPSWeight, Speed property required %s",
itemData and itemData['itemLink'])
return nil
end
itemEquipLoc = itemData.itemEquipLoc
local speedWeightKey, speedWeightModifier, dpsWeight, speedKindWeight, dpsWeightModifier
-- Look through weaponSlotToWeightKey for all kinds associated with itemEquipLoc
-- - which then gives the WEAPON_SLOT_MAP key for weight lookup
-- weaponSlotToWeightKey['INVTYPE_WEAPON'] = { "MH", "OH" }
for _, keySuffix in ipairs(session.weaponSlotToWeightKey[itemEquipLoc] or {}) do
if itemEquipLoc == 'INVTYPE_RANGED' or itemEquipLoc == 'INVTYPE_THROWN' or itemEquipLoc == 'INVTYPE_RANGEDRIGHT' then
dpsWeightModifier = session.activeStatWeights['ITEM_MOD_DAMAGE_PER_SECOND_SHORT_RANGED']
else
dpsWeightModifier = session.activeStatWeights['ITEM_MOD_DAMAGE_PER_SECOND_SHORT']
end
if dpsWeightModifier and dpsWeightModifier > 0 then
dpsWeight = stats['ITEM_MOD_DAMAGE_PER_SECOND_SHORT'] * dpsWeightModifier
else
dpsWeight = stats['ITEM_MOD_DAMAGE_PER_SECOND_SHORT'] -- * 1
end
-- Lookup speed weight key with kind suffix (MH, OH, RANGED, 2H)
speedWeightKey = 'ITEM_MOD_CR_SPEED_SHORT_' .. keySuffix
speedWeightModifier = session.activeStatWeights[speedWeightKey]
-- Prevent multiplication by 0
if not speedWeightModifier or speedWeightModifier == 0 then speedWeightModifier = 1 end
-- Exclude Off-hand comparison before trained
if keySuffix == "OH" and not session.equippableSlots['INVTYPE_WEAPONOFFHAND'] then
-- print("Untrained OH")
elseif speedWeightModifier then
speedKindWeight = stats['ITEM_MOD_CR_SPEED_SHORT'] * speedWeightModifier
-- (DPS * 1_DPS_WEIGHT) + (SPEED * WEAPON_WEIGHT)
dpsWeights[keySuffix] = {['totalWeight'] = dpsWeight + speedKindWeight, ['speedWeight'] = speedKindWeight}
else -- Weapon speed irrelevant, likely a caster
dpsWeights[keySuffix] = {['totalWeight'] = dpsWeight, ['speedWeight'] = 0.00}
end
end
return dpsWeights
end
local function CalculateSpellWeight(stats, tooltipTextLines)
-- Example:
-- stats = {
-- ['ITEM_MOD_SPELL_DAMAGE_DONE'] = 12, -- Always 1 lower than tooltip shows
-- ...
-- }
-- No spellpower weights for class
if not (session.activeStatWeights['STAT_SPELLDAMAGE'] or session.activeStatWeights['ITEM_MOD_SPELL_POWER']) then
return 0
end
local schoolStatWeight, totalStatWeight = 0, 0
local schoolKey, schoolName, spellPower
-- Check all tooltip lines for regex matches
for _, line in ipairs(tooltipTextLines) do
-- print("CalculateSpellWeight (", line, ")")
schoolName, spellPower = smatch(line, SPELL_KIND_MATCH)
if schoolName then
schoolKey = SPELL_KIND_MAP[strlower(schoolName)]
-- print("Matched schoolName", strlower(schoolName), schoolKey, spellPower)
if session.activeStatWeights[schoolKey] and session.activeStatWeights[schoolKey] > 0 then
-- ITEM_MOD_SPELL_DAMAGE_DONE cannot be trusted, byRef add parsed stats
stats[schoolKey] = spellPower
schoolStatWeight = spellPower * session.activeStatWeights[schoolKey]
totalStatWeight = totalStatWeight + schoolStatWeight
end
end
end
-- Not a magic school, return default weighting
-- ITEM_MOD_SPELL_DAMAGE_DONE cannot be trusted, e.g. 40 Shadow + 40 Frost == 78 ITEM_MOD_SPELL_DAMAGE_DONE
if totalStatWeight == 0 and stats['STAT_SPELLDAMAGE'] then -- Legacy block
-- print("STAT_SPELLDAMAGE: Not a magic school", stats['STAT_SPELLDAMAGE'])
-- ITEM_MOD_SPELL_DAMAGE_DONE cannot be trusted without validation
-- Set spellPower stat to built-in stat after verifying no school
stats['STAT_SPELLDAMAGE'] = stats['ITEM_MOD_SPELL_DAMAGE_DONE'] + 1