-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathProcDoc.lua
More file actions
2235 lines (2000 loc) · 91.1 KB
/
Copy pathProcDoc.lua
File metadata and controls
2235 lines (2000 loc) · 91.1 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
-- ProcDoc.lua
-- 1) SavedVariables initialization
-- Creates a frame to initialize persistent settings on VARIABLES_LOADED.
local initFrame = CreateFrame("Frame", "ProcDocDBInitFrame", UIParent)
initFrame:RegisterEvent("VARIABLES_LOADED")
initFrame:SetScript("OnEvent", function()
if event == "VARIABLES_LOADED" then
if not ProcDocDB then ProcDocDB = {} end
if not ProcDocDB.globalVars then ProcDocDB.globalVars = {} end
if not ProcDocDB.procsEnabled then ProcDocDB.procsEnabled = {} end
if not ProcDocDB.actionProcDurations then ProcDocDB.actionProcDurations = {} end
local gv = ProcDocDB.globalVars
-- Only set defaults if not present
if gv.minAlpha == nil then gv.minAlpha = 0.8 end
if gv.maxAlpha == nil then gv.maxAlpha = 1.0 end
if gv.minScale == nil then gv.minScale = 0.9 end
if gv.maxScale == nil then gv.maxScale = 1.0 end -- new default (old default was 1.25)
if gv.alphaStep == nil then gv.alphaStep = 0.01 end
if gv.pulseSpeed == nil then gv.pulseSpeed = 0.4 end
if gv.topOffset == nil then gv.topOffset = 70 end
if gv.sideOffset == nil then gv.sideOffset = 60 end
if gv.timerTextAlpha == nil then gv.timerTextAlpha = 0.85 end
if gv.isMuted == nil then gv.isMuted = false end
if gv.soundVolume == nil then gv.soundVolume = 1.0 end
if gv.disableTimers == nil then gv.disableTimers = false end
minAlpha = gv.minAlpha
maxAlpha = gv.maxAlpha
minScale = gv.minScale
maxScale = gv.maxScale
alphaStep = gv.alphaStep
pulseSpeed = gv.pulseSpeed
topOffset = gv.topOffset
sideOffset = gv.sideOffset
timerTextAlpha= gv.timerTextAlpha
initFrame:UnregisterEvent("VARIABLES_LOADED")
end
end)
-- Debug helper to print current DB values
local function ProcDoc_DumpDB()
if not ProcDocDB or not ProcDocDB.globalVars then
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000ProcDoc|r: DB not initialized yet.")
return
end
local gv = ProcDocDB.globalVars
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00ProcDoc DB Dump|r")
DEFAULT_CHAT_FRAME:AddMessage(" minAlpha="..tostring(gv.minAlpha)
.." maxAlpha="..tostring(gv.maxAlpha)
.." minScale="..tostring(gv.minScale)
.." maxScale="..tostring(gv.maxScale)
.." pulseSpeed="..tostring(gv.pulseSpeed))
DEFAULT_CHAT_FRAME:AddMessage(" topOffset="..tostring(gv.topOffset).." sideOffset="..tostring(gv.sideOffset)
.." isMuted="..tostring(gv.isMuted)
.." disableTimers="..tostring(gv.disableTimers))
end
-- 2) Main addon frame
local addonName = "ProcDoc"
local ProcDoc = CreateFrame("Frame", "ProcDocAlertFrame", UIParent)
-- 3) Proc data tables
local PROC_DATA = {
["WARLOCK"] = {
{
buffName = "Shadow Trance",
texture = "Interface\\Icons\\Spell_Shadow_Twilight",
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\WarlockShadowTrance.tga",
alertStyle = "SIDES",
},
},
["MAGE"] = {
{
buffName = "Clearcasting",
texture = "Interface\\Icons\\Spell_Shadow_ManaBurn",
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\DruidClearcasting.tga",
alertStyle = "TOP",
},
{
buffName = "Netherwind Focus",
texture = "Interface\\Icons\\Spell_Shadow_Teleport",
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\MageT2.tga",
alertStyle = "SIDES2",
},
{
buffName = "Temporal Convergence",
texture = "Interface\\Icons\\Spell_Nature_StormReach",
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\MageTemporalConvergence.tga",
alertStyle = "SIDES2",
},
{
buffName = "Flash Freeze",
texture = "Interface\\Icons\\Spell_Fire_FrostResistanceTotem",
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\MageFlashFreeze.tga",
alertStyle = "SIDES",
},
{
buffName = "Arcane Rupture",
texture = "Interface\\Icons\\Spell_Arcane_Blast",
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\MageArcaneRupture.tga",
alertStyle = "SIDES",
},
{
buffName = "Hot Streak",
texture = "Interface\\Icons\\Spell_Fire_Firestarter", -- generic fire icon (may differ on server)
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\WarriorRevenge.tga", -- sample texture for test button preview
alertStyle = "LEFT", -- preview only; live logic spawns LEFT/RIGHT/TOP2 as tiers advance
},
},
["DRUID"] = {
{
buffName = "Clearcasting",
texture = "Interface\\Icons\\Spell_Shadow_ManaBurn",
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\DruidClearcasting.tga",
alertStyle = "TOP",
},
{
buffName = "Nature's Grace",
texture = "Interface\\Icons\\Spell_Nature_NaturesBlessing",
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\DruidNaturesGrace.tga",
alertStyle = "SIDES",
},
{
buffName = "Tiger's Fury",
texture = "Interface\\Icons\\Ability_Mount_JungleTiger",
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\HunterMongooseBite.tga",
alertStyle = "SIDES2",
},
{
buffName = "Astral Boon",
texture = "Interface\\Icons\\Spell_Arcane_StarFire",
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\DruidAstralBoon.tga",
alertStyle = "TOP2",
},
{
buffName = "Natural Boon",
texture = "Interface\\Icons\\Spell_Nature_AbolishMagic",
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\DruidNaturalBoon.tga",
alertStyle = "TOP2",
},
{
buffName = "Arcane Eclipse",
texture = "Interface\\Icons\\Spell_Nature_WispSplode",
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\DruidArcaneEclipse.tga",
alertStyle = "SIDES2",
},
{
buffName = "Nature Eclipse",
texture = "Interface\\Icons\\Spell_Nature_AbolishMagic",
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\DruidNatureEclipse.tga",
alertStyle = "SIDES2",
},
},
["SHAMAN"] = {
{
buffName = "Clearcasting",
texture = "Interface\\Icons\\Spell_Shadow_ManaBurn",
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\DruidClearcasting.tga",
alertStyle = "TOP",
},
{
buffName = "Nature's Swiftness",
texture = "Interface\\Icons\\Spell_Nature_RavenForm",
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\DruidNaturesGrace.tga",
alertStyle = "SIDES",
},
{
buffName = "Stormstrike",
texture = "Interface\\Icons\\Ability_Shaman_StormStrike",
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\ShamanStormstrike.tga",
alertStyle = "TOP2",
},
{
buffName = "Flurry",
texture = "Interface\\Icons\\Ability_GhoulFrenzy",
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\HunterMongooseBite.tga",
alertStyle = "SIDES2",
},
},
["HUNTER"] = {
{
buffName = "Quick Shots",
texture = "Interface\\Icons\\Ability_Warrior_InnerRage",
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\HunterQuickShots.tga",
alertStyle = "SIDES2",
},
{
buffName = "Lock and Load",
texture = nil,
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\Lock_and_Load.blp",
alertStyle = "TOP2",
},
{
buffName = "Explosive Ammunition",
texture = nil,
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\impact.blp",
alertStyle = "TOP",
},
{
buffName = "Poisonous Ammunition",
texture = nil,
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\DruidNaturesGrace.tga",
alertStyle = "TOP_ROTATED",
},
{
buffName = "Enchanted Ammunition",
texture = nil,
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\serendipity.blp",
alertStyle = "TOP",
},
},
["WARRIOR"] = {
{
buffName = "Enrage",
texture = "Interface\\Icons\\Spell_Shadow_UnholyFrenzy",
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\WarriorEnrage.tga",
alertStyle = "SIDES",
},
},
["PRIEST"] = {
{
buffName = "Resurgence",
texture = "Interface\\Icons\\Spell_Holy_MindVision",
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\PriestResurgence.tga",
alertStyle = "SIDES",
},
{
buffName = "Enlightened",
texture = "Interface\\Icons\\Spell_Holy_PowerInfusion",
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\PriestEnlightened.tga",
alertStyle = "TOP",
},
{
buffName = "Searing Light",
texture = "Interface\\Icons\\Spell_Holy_SearingLightPriest",
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\PriestSearingLight.tga",
alertStyle = "SIDES2",
},
{
buffName = "Shadow Veil",
texture = "Interface\\Icons\\Spell_Shadow_GatherShadows",
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\PriestShadowVeil.tga",
alertStyle = "SIDES",
},
{
buffName = "Spell Blasting",
texture = "Interface\\Icons\\Spell_Lightning_LightningBolt01",
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\PriestSpellBlasting.tga",
alertStyle = "TOP2",
},
},
["PALADIN"] = {
{
buffName = "Daybreak",
texture = "Interface\\Icons\\Spell_Holy_AuraMastery",
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\PaladinDaybreak.tga",
alertStyle = "TOP",
},
{
-- Triggered by Crusader Strike. Reduces damage of the next blocked attack.
-- Lasts 10 seconds.
buffName = "Zealous Defence",
texture = nil,
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\Grand_Crusader.tga",
alertStyle = "SIDES2",
},
},
["ROGUE"] = {
{
buffName = "Remorseless",
texture = "Interface\\Icons\\Ability_FiegnDead",
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\RogueRemorseless.tga",
alertStyle = "SIDES",
},
{
buffName = "Tricks of the Trade",
texture = "Interface\\Icons\\INV_Misc_Key_03",
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\RogueTricksoftheTrade.tga",
alertStyle = "TOP",
},
},
}
local ACTION_PROCS = {
["ROGUE"] = {
{
buffName = "Riposte",
texture = "Interface\\Icons\\Ability_Warrior_Challange",
alertTexturePath= "Interface\\AddOns\\ProcDoc\\img\\RogueRiposte.tga",
alertStyle = "SIDES",
spellName = "Riposte"
},
{
buffName = "Surprise Attack",
texture = "Interface\\Icons\\Ability_Rogue_SurpriseAttack",
alertTexturePath= "Interface\\AddOns\\ProcDoc\\img\\RogueSuddenDeath.tga",
alertStyle = "SIDES2",
spellName = "Surprise Attack"
},
},
["WARRIOR"] = {
{
buffName = "Overpower",
texture = "Interface\\Icons\\Ability_MeleeDamage",
alertTexturePath= "Interface\\AddOns\\ProcDoc\\img\\WarriorOverpower.tga",
alertStyle = "TOP",
spellName = "Overpower"
},
{
buffName = "Execute",
texture = "Interface\\Icons\\inv_sword_48",
alertTexturePath= "Interface\\AddOns\\ProcDoc\\img\\WarriorExecute.tga",
alertStyle = "TOP2",
spellName = "Execute"
},
{
buffName = "Counterattack",
texture = "Interface\\Icons\\Ability_Warrior_Riposte",
alertTexturePath= "Interface\\AddOns\\ProcDoc\\img\\WarriorCounterattack.tga",
alertStyle = "LEFT",
spellName = "Counterattack"
},
{
buffName = "Revenge",
texture = "Interface\\Icons\\Ability_Warrior_Revenge",
alertTexturePath= "Interface\\AddOns\\ProcDoc\\img\\WarriorRevenge.tga",
alertStyle = "RIGHT",
spellName = "Revenge"
}
},
["MAGE"] = {
{
buffName = "Arcane Surge",
texture = "Interface\\Icons\\INV_Enchant_EssenceMysticalLarge",
alertTexturePath= "Interface\\AddOns\\ProcDoc\\img\\MageArcaneSurge.tga",
alertStyle = "TOP2",
spellName = "Arcane Surge"
},
},
["HUNTER"] = {
{
buffName = "Lacerate",
texture = "Interface\\Icons\\Spell_Lacerate_1c",
alertTexturePath= "Interface\\AddOns\\ProcDoc\\img\\HunterMongooseBite.tga",
alertStyle = "SIDES",
spellName = "Lacerate"
},
-- Kill Command behaves inconsistently: IsUsableAction returns 1 if it has been triggered, independently if it is on CD or not.
-- I decided to create an alternative method to check if it is in CD using the spellbook
-- If this server-side issue is fixed, this can be easily reverted
{
buffName = "Kill Command",
texture = "Interface\\Icons\\ability_hunter_killcommand",
alertTexturePath= "Interface\\AddOns\\ProcDoc\\img\\HunterBaitedShot.tga",
alertStyle = "TOP",
spellName = "Kill Command",
useSpellbookForCooldown = true,
}
},
["PALADIN"] = {
{
buffName = "Hammer of Wrath",
texture = "Interface\\Icons\\Ability_Thunderclap",
alertTexturePath= "Interface\\AddOns\\ProcDoc\\img\\PaladinHammer.tga",
alertStyle = "SIDES",
spellName = "Hammer of Wrath"
},
-- New: Spellbook-based cooldown procs (no action bar slot required)
{
buffName = "Judgement",
-- No action button texture lookup (use spellbook cooldown), so leave texture empty
texture = nil,
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\PaladinJudgement.tga",
alertStyle = "RIGHT",
spellName = "Judgement",
useSpellbook = true,
},
{
buffName = "Hammer of Justice",
texture = nil,
alertTexturePath = "Interface\\AddOns\\ProcDoc\\img\\PaladinJustice.tga",
alertStyle = "LEFT",
spellName = "Hammer of Justice",
useSpellbook = true,
},
},
}
local DEFAULT_ALERT_TEXTURE = "Interface\\AddOns\\ProcDoc\\img\\ProcDocAlert.tga"
-- Optional fixed durations (seconds) for action-based proc windows.
-- These emulate a timeout so the alert auto-clears even if the action remains usable.
local ACTION_PROC_DEFAULT_DURATIONS = {
["Overpower"] = 5,
["Riposte"] = 5,
["Counterattack"] = 5,
["Revenge"] = 5,
["Surprise Attack"] = 5,
["Arcane Surge"] = 4,
["Lacerate"] = 4,
["Kill Command"] = 4,
}
-- Hot Streak (Turtle WoW custom, stack-based visual) constants (Vanilla 1.12 compatible logic)
local HOT_STREAK_BUFF_NAME = "Hot Streak"
-- Re-use existing textures in the addon: WarriorEnrage for side build-up, MageArcaneRupture for final top burst.
local HOT_STREAK_SIDE_TEXTURE = "Interface\\AddOns\\ProcDoc\\img\\WarriorRevenge.tga"
local HOT_STREAK_TOP_TEXTURE = "Interface\\AddOns\\ProcDoc\\img\\MageHotStreak.tga"
local hotStreakLastTier = 0 -- 0,3,4,5 (we only care about tiers 3/4/5)
-- Ensure DB tables exist and load saved globals
local function ProcDoc_EnsureDB()
if not ProcDocDB then ProcDocDB = {} end
if not ProcDocDB.globalVars then ProcDocDB.globalVars = {} end
if not ProcDocDB.procsEnabled then ProcDocDB.procsEnabled = {} end
if not ProcDocDB.actionProcDurations then ProcDocDB.actionProcDurations = {} end
end
local function ProcDoc_LoadGlobalsFromDB()
ProcDoc_EnsureDB()
local gv = ProcDocDB.globalVars
-- Migrate any legacy fields
if gv.maxSize and not gv.maxScale then gv.maxScale = gv.maxSize; gv.maxSize = nil end
if gv.minAlpha ~= nil then minAlpha = gv.minAlpha end
if gv.maxAlpha ~= nil then maxAlpha = gv.maxAlpha end
if gv.minScale ~= nil then minScale = gv.minScale end
if gv.maxScale ~= nil then maxScale = gv.maxScale end
if gv.pulseSpeed ~= nil then pulseSpeed = gv.pulseSpeed end
if gv.topOffset ~= nil then topOffset = gv.topOffset end
if gv.sideOffset ~= nil then sideOffset = gv.sideOffset end
end
-- Initialize once SavedVariables are available in vanilla (VARIABLES_LOADED)
local ProcDocInitFrame = CreateFrame("Frame", "ProcDocInitFrame", UIParent)
ProcDocInitFrame:RegisterEvent("VARIABLES_LOADED")
ProcDocInitFrame:SetScript("OnEvent", function()
ProcDoc_LoadGlobalsFromDB()
end)
local function GetActionProcDuration(spellName)
if not spellName then return nil end
local overrides = ProcDocDB.actionProcDurations
if overrides and overrides[spellName] and overrides[spellName] > 0 then
return overrides[spellName]
end
return ACTION_PROC_DEFAULT_DURATIONS[spellName]
end
-- 4) Alert frame pool
local alertFrames = {}
local function CreateAlertFrame(style)
local alertObj = {}
alertObj.isActive = false
alertObj.isActionBased = false
alertObj.style = style
alertObj.textures = {}
-- Timer related fields
alertObj.sliceGroups = {} -- (legacy; unused after reverting wipe)
alertObj.timerTexts = {} -- per base texture index -> fontstring
alertObj.hasTimer = false
alertObj.procStartTime = nil
alertObj.procDuration = nil
alertObj.zeroShown = false
alertObj.pulseAlpha = minAlpha
alertObj.pulseDir = alphaStep
-- Decide frame size and positions based on style
if style == "TOP" or style == "TOP2" or style == "TOP_ROTATED" then
alertObj.baseWidth = 256
alertObj.baseHeight = 128
local tex = ProcDoc:CreateTexture(nil, "OVERLAY")
local offsetY = (style == "TOP2") and (topOffset + 50) or topOffset
tex:SetPoint("CENTER", UIParent, "CENTER", 0, offsetY)
tex:SetWidth(alertObj.baseWidth)
tex:SetHeight(alertObj.baseHeight)
tex:SetAlpha(0)
if style == "TOP_ROTATED" then
tex:SetTexCoord(0, 1, 1, 1, 0, 0, 1, 0)
end
tex:Hide()
table.insert(alertObj.textures, tex)
elseif style == "SIDES" or style == "SIDES2" then
alertObj.baseWidth = 128
alertObj.baseHeight = 256
local left = ProcDoc:CreateTexture(nil, "OVERLAY")
local right = ProcDoc:CreateTexture(nil, "OVERLAY")
local offsetX = (style == "SIDES2") and (sideOffset + 50) or sideOffset
left:SetPoint("CENTER", UIParent, "CENTER", -offsetX, topOffset - 150)
right:SetPoint("CENTER", UIParent, "CENTER", offsetX, topOffset - 150)
left:SetWidth(alertObj.baseWidth)
left:SetHeight(alertObj.baseHeight)
left:SetAlpha(0)
left:Hide()
right:SetWidth(alertObj.baseWidth)
right:SetHeight(alertObj.baseHeight)
right:SetTexCoord(1, 0, 0, 1)
right:SetAlpha(0)
right:Hide()
table.insert(alertObj.textures, left)
table.insert(alertObj.textures, right)
elseif style == "LEFT" then
-- Single left-side vertical texture (same positioning as the LEFT half of SIDES2, no mirrored partner)
alertObj.baseWidth = 128
alertObj.baseHeight = 256
local tex = ProcDoc:CreateTexture(nil, "OVERLAY")
local offsetX = sideOffset + 50 -- mimic SIDES2 left offset spacing
tex:SetPoint("CENTER", UIParent, "CENTER", -offsetX, topOffset - 150)
tex:SetWidth(alertObj.baseWidth)
tex:SetHeight(alertObj.baseHeight)
tex:SetAlpha(0)
tex:Hide()
table.insert(alertObj.textures, tex)
elseif style == "RIGHT" then
-- Single right-side vertical texture (same positioning as the RIGHT half of SIDES2, mirrored)
alertObj.baseWidth = 128
alertObj.baseHeight = 256
local tex = ProcDoc:CreateTexture(nil, "OVERLAY")
local offsetX = sideOffset + 50 -- mimic SIDES2 right offset spacing
tex:SetPoint("CENTER", UIParent, "CENTER", offsetX, topOffset - 150)
tex:SetWidth(alertObj.baseWidth)
tex:SetHeight(alertObj.baseHeight)
tex:SetTexCoord(1, 0, 0, 1) -- mirror horizontally
tex:SetAlpha(0)
tex:Hide()
table.insert(alertObj.textures, tex)
end
return alertObj
end
-- Acquire a frame for either buff-based OR action-based usage
local function AcquireAlertFrame(style, isActionBased)
for _, alertObj in ipairs(alertFrames) do
if (not alertObj.isActive)
and (alertObj.style == style)
and (alertObj.isActionBased == isActionBased)
then
return alertObj
end
end
local newAlert = CreateAlertFrame(style)
newAlert.isActionBased = isActionBased
table.insert(alertFrames, newAlert)
return newAlert
end
-- Re-anchor an alert object's textures according to its style and current saved offsets
local function ProcDoc_ReanchorAlert(alertObj)
if not alertObj or not alertObj.textures then return end
local style = alertObj.style
if style == "TOP" or style == "TOP2" or style == "TOP_ROTATED" then
local tex = alertObj.textures[1]
if tex then
tex:ClearAllPoints()
local offsetY = (style == "TOP2") and (topOffset + 50) or topOffset
tex:SetPoint("CENTER", UIParent, "CENTER", 0, offsetY)
end
elseif style == "SIDES" or style == "SIDES2" then
local left = alertObj.textures[1]
local right = alertObj.textures[2]
local offsetX = (style == "SIDES2") and (sideOffset + 50) or sideOffset
if left then
left:ClearAllPoints()
left:SetPoint("CENTER", UIParent, "CENTER", -offsetX, topOffset - 150)
end
if right then
right:ClearAllPoints()
right:SetPoint("CENTER", UIParent, "CENTER", offsetX, topOffset - 150)
end
elseif style == "LEFT" then
local tex = alertObj.textures[1]
if tex then
tex:ClearAllPoints()
tex:SetPoint("CENTER", UIParent, "CENTER", -(sideOffset + 50), topOffset - 150)
end
elseif style == "RIGHT" then
local tex = alertObj.textures[1]
if tex then
tex:ClearAllPoints()
tex:SetPoint("CENTER", UIParent, "CENTER", (sideOffset + 50), topOffset - 150)
end
end
end
--------------------------------------------
-- HELPER: Play proc sound with user volume
--------------------------------------------
local function ProcDoc_PlayAlertSound()
if ProcDocDB.globalVars.isMuted then
return
end
local desiredVolume = ProcDocDB.globalVars.soundVolume or 1.0
if desiredVolume < 0 then desiredVolume = 0 end
if desiredVolume > 1 then desiredVolume = 1 end
local oldVolume = GetCVar("SoundVolume")
SetCVar("SoundVolume", tostring(desiredVolume))
PlaySoundFile("Interface\\AddOns\\ProcDoc\\img\\SpellAlert.ogg", "SFX")
-- Immediately restore
SetCVar("SoundVolume", oldVolume)
end
-- 5) OnUpdate pulse logic (handles alpha/scale pulsing and optional timer text)
-- NOTE: Frequent use of the live GameTooltip for buff scanning will instantly hide
-- any tooltip the player is viewing (items, NPCs, etc.). To avoid this we create
-- our own hidden tooltip and never call GameTooltip:Hide() during scans.
if not ProcDocScanTooltip then
ProcDocScanTooltip = CreateFrame("GameTooltip","ProcDocScanTooltip",UIParent,"GameTooltipTemplate")
ProcDocScanTooltip:Hide()
end
local function OnUpdateHandler()
if maxAlpha <= minAlpha then maxAlpha = minAlpha + 0.01 end
if maxScale <= minScale then maxScale = minScale + 0.01 end
local now = GetTime()
-- Periodic capture of current buff remaining times so we can detect mid-duration refreshes
if not ProcDoc._lastBuffTimeScan then ProcDoc._lastBuffTimeScan = 0 end
if not ProcDoc.currentBuffTimes then ProcDoc.currentBuffTimes = {} end
if (now - ProcDoc._lastBuffTimeScan) > 0.30 then -- throttle ~3 times per second
local snapshot = {}
if GetPlayerBuffTimeLeft then
for i = 0, 31 do
local tex = GetPlayerBuffTexture(i)
if tex then
ProcDocScanTooltip:SetOwner(UIParent, "ANCHOR_NONE")
ProcDocScanTooltip:SetPlayerBuff(i)
local bName = (ProcDocScanTooltipTextLeft1 and ProcDocScanTooltipTextLeft1:GetText()) or ""
ProcDocScanTooltip:Hide()
if bName ~= "" then
local tl = GetPlayerBuffTimeLeft(i)
if tl and tl > 0 then
-- keep the largest remaining (some servers may duplicate icons)
if (not snapshot[bName]) or (tl > snapshot[bName]) then
snapshot[bName] = tl
end
end
end
end
end
end
ProcDoc.currentBuffTimes = snapshot
ProcDoc._lastBuffTimeScan = now
end
for _, alertObj in ipairs(alertFrames) do
if alertObj.isActive then
-- Pulse update
alertObj.pulseAlpha = alertObj.pulseAlpha + (alertObj.pulseDir * pulseSpeed)
if alertObj.pulseAlpha < minAlpha then
alertObj.pulseAlpha = minAlpha
alertObj.pulseDir = alphaStep
elseif alertObj.pulseAlpha > maxAlpha then
alertObj.pulseAlpha = maxAlpha
alertObj.pulseDir = -alphaStep
end
local scale = 1.0
local aRange = maxAlpha - minAlpha
if aRange > 0 then
local frac = (alertObj.pulseAlpha - minAlpha) / aRange
scale = minScale + frac * (maxScale - minScale)
end
-- Apply pulse to textures
for _, tex in ipairs(alertObj.textures) do
tex:SetAlpha(alertObj.pulseAlpha)
tex:SetWidth(alertObj.baseWidth * scale)
tex:SetHeight(alertObj.baseHeight * scale)
tex:Show()
end
-- Timer countdown text
if alertObj.hasTimer and alertObj.procDuration and alertObj.procStartTime then
-- Refresh detection: if the buff was re-applied (remaining time jumped up), reset timer
if (not alertObj.isActionBased) and alertObj.buffName and ProcDoc.currentBuffTimes then
local observedTL = ProcDoc.currentBuffTimes[alertObj.buffName]
if observedTL and observedTL > 0 then
local elapsedSoFar = now - alertObj.procStartTime
local remainingPrev = alertObj.procDuration - elapsedSoFar
if remainingPrev < 0 then remainingPrev = 0 end
-- If the newly observed time left exceeds previous remaining by >0.5s treat as refresh
if (observedTL - remainingPrev) > 0.5 then
alertObj.procStartTime = now
alertObj.procDuration = observedTL
alertObj.zeroShown = false
end
end
end
local elapsed = now - alertObj.procStartTime
local remaining = alertObj.procDuration - elapsed
local secs
if remaining <= 0 then
secs = 0
if alertObj.zeroShown then
-- Now hide after showing 0 previously
alertObj.isActive = false
alertObj.hasTimer = false
alertObj.procStartTime = nil
alertObj.procDuration = nil
alertObj.forceHide = true
for _, tex in ipairs(alertObj.textures) do tex:Hide() end
if alertObj.timerTexts then
for _, fs in ipairs(alertObj.timerTexts) do if fs then fs:Hide() end end
end
else
alertObj.zeroShown = true
end
else
-- Display remaining seconds (use floor so we reach 0)
secs = math.floor(remaining + 0.0001)
if secs < 0 then secs = 0 end
end
if alertObj.isActive and secs ~= nil then
for _, fs in ipairs(alertObj.timerTexts) do
if fs then
fs:SetText(secs)
if timerTextAlpha then fs:SetAlpha(timerTextAlpha) end
fs:Show()
end
end
end
else
-- No timer -> hide any stray timer texts
if alertObj.timerTexts then
for _, fs in ipairs(alertObj.timerTexts) do if fs then fs:Hide() end end
end
end
end
-- If frame is inactive but leftover timer text somehow visible, hide it
if (not alertObj.isActive) and alertObj.timerTexts then
for _, fs in ipairs(alertObj.timerTexts) do if fs then fs:Hide() end end
end
end
end
ProcDoc:SetScript("OnUpdate", OnUpdateHandler)
ProcDoc:SetWidth(1)
ProcDoc:SetHeight(1)
ProcDoc:SetPoint("CENTER", UIParent, "CENTER")
-- 6) Merge buff & action proc definitions for the player's class
local _, playerClass = UnitClass("player")
local normalProcs = PROC_DATA[playerClass] or {}
local actionProcs = ACTION_PROCS[playerClass] or {} -- e.g. Overpower
-- Merge them into one big “classProcs†table
local classProcs = {}
for _, p in ipairs(normalProcs) do
table.insert(classProcs, p)
end
for _, q in ipairs(actionProcs) do
table.insert(classProcs, q)
end
-- 7) Buff-based detection
-- Holds which buff names we’ve already played a sound for.
local knownBuffProcs = {}
local buffStackCounts = {}
local function CheckProcs()
-- Ensure we are using the latest saved globals each time we show/update alerts
if ProcDoc_LoadGlobalsFromDB then ProcDoc_LoadGlobalsFromDB() end
-- 1) Hide any old (buff-based) frames first
for _, alertObj in ipairs(alertFrames) do
if (not alertObj.isActionBased) then
alertObj.isActive = false
for _, tex in ipairs(alertObj.textures) do tex:Hide() end
if alertObj.timerTexts then
for _, fs in ipairs(alertObj.timerTexts) do if fs then fs:Hide() end end
end
if alertObj.stackText then alertObj.stackText:Hide() end
alertObj.hasTimer = false
alertObj.procStartTime = nil
alertObj.procDuration = nil
alertObj.buffName = nil
end
end
-- 2) Gather a list of all currently active buff-based procs
local activeBuffProcs = {}
local activeBuffNames = {} -- just the names, for quick “did it fall off?†checks
local hotStreakStacks = 0
buffStackCounts = {}
for i = 0, 31 do
local buffTexture = GetPlayerBuffTexture(i)
if buffTexture then
ProcDocScanTooltip:SetOwner(UIParent, "ANCHOR_NONE")
ProcDocScanTooltip:SetPlayerBuff(i)
local buffName = (ProcDocScanTooltipTextLeft1 and ProcDocScanTooltipTextLeft1:GetText()) or ""
local stackGuess = 0
-- Vanilla 1.12: stacks often appear as right text or in second left line; attempt simple numeric parse
if ProcDocScanTooltipTextRight1 and ProcDocScanTooltipTextRight1:GetText() then
local rn = tonumber(ProcDocScanTooltipTextRight1:GetText())
if rn then stackGuess = rn end
end
if stackGuess == 0 and ProcDocScanTooltipTextLeft2 and ProcDocScanTooltipTextLeft2:GetText() then
local ln2 = ProcDocScanTooltipTextLeft2:GetText()
local ln2n = tonumber(ln2)
if ln2n then stackGuess = ln2n end
end
-- Prefer GetPlayerBuffApplications for Astral Boon / Natural Boon (reliable charges)
if GetPlayerBuffApplications and (buffName == "Astral Boon" or buffName == "Natural Boon") then
local apiStacks = GetPlayerBuffApplications(i)
if apiStacks and apiStacks > 0 then
stackGuess = apiStacks
end
end
ProcDocScanTooltip:Hide()
for _, procInfo in ipairs(normalProcs) do
if ProcDocDB.procsEnabled[procInfo.buffName] ~= false then
if (procInfo.texture == nil or buffTexture == procInfo.texture) and (buffName == procInfo.buffName) then
table.insert(activeBuffProcs, procInfo)
activeBuffNames[procInfo.buffName] = true
-- Store stack count if any (>=1 valid)
if stackGuess and stackGuess >= 1 then
buffStackCounts[buffName] = stackGuess
end
end
end
end
-- Hot Streak detection (fuzzy, allow lowercase compare, handle possible rank suffixes)
if buffName ~= "" then
local lowerName = string.lower(buffName)
if lowerName == string.lower(HOT_STREAK_BUFF_NAME) or string.find(lowerName, "hot streak") then
local apiStacks
if GetPlayerBuffApplications then
apiStacks = GetPlayerBuffApplications(i)
end
local stacks = apiStacks or stackGuess
if not stacks or stacks < 1 then stacks = 1 end
if stacks > hotStreakStacks then hotStreakStacks = stacks end
end
end
end
end
-- 3) Show frames for each active buff
for _, procInfo in ipairs(activeBuffProcs) do
local style = procInfo.alertStyle or "SIDES"
local alertObj = AcquireAlertFrame(style, false)
alertObj.isActive = true
alertObj.pulseAlpha = minAlpha
alertObj.pulseDir = alphaStep
alertObj.hasTimer = false
alertObj.procStartTime = nil
alertObj.procDuration = nil
alertObj.buffName = procInfo.buffName
local path = procInfo.alertTexturePath or DEFAULT_ALERT_TEXTURE
for _, tex in ipairs(alertObj.textures) do
tex:SetTexture(path)
tex:SetAlpha(minAlpha)
tex:SetWidth(alertObj.baseWidth * minScale)
tex:SetHeight(alertObj.baseHeight * minScale)
tex:Show()
end
ProcDoc_ReanchorAlert(alertObj)
ProcDoc_ReanchorAlert(alertObj)
-- Timer numeric countdown (replaces prior vertical wipe implementation)
local timeLeft
if GetPlayerBuffTimeLeft then
for i = 0, 31 do
local bTex = GetPlayerBuffTexture(i)
if bTex then
ProcDocScanTooltip:SetOwner(UIParent, "ANCHOR_NONE")
ProcDocScanTooltip:SetPlayerBuff(i)
local bName = (ProcDocScanTooltipTextLeft1 and ProcDocScanTooltipTextLeft1:GetText()) or ""
ProcDocScanTooltip:Hide()
if bName == procInfo.buffName then
local tl = GetPlayerBuffTimeLeft(i)
if tl and tl > 0 then
timeLeft = tl
end
break
end
end
end
end
if (not ProcDocDB.globalVars.disableTimers) and timeLeft and timeLeft > 0 then
alertObj.hasTimer = true
alertObj.procStartTime = GetTime()
alertObj.procDuration = timeLeft
alertObj.zeroShown = false
-- Create/ensure timer font strings (one per base texture)
for idx, baseTex in ipairs(alertObj.textures) do
if not alertObj.timerTexts[idx] then
local parentFrame = baseTex:GetParent() or ProcDoc or UIParent
local fs = parentFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
local xShift = 0
if procInfo.buffName == "Astral Boon" then
xShift = -45 -- left side
elseif procInfo.buffName == "Natural Boon" then
xShift = 45 -- right side
end
fs:SetPoint("CENTER", baseTex, "CENTER", xShift, 0)
fs:SetTextColor(1, 1, 0, 1)
if timerTextAlpha then fs:SetAlpha(timerTextAlpha) end
fs:SetText("")
alertObj.timerTexts[idx] = fs
else
if procInfo.buffName == "Astral Boon" then
local fs = alertObj.timerTexts[idx]; fs:ClearAllPoints(); fs:SetPoint("CENTER", alertObj.textures[idx], "CENTER", -45, 0)
elseif procInfo.buffName == "Natural Boon" then
local fs = alertObj.timerTexts[idx]; fs:ClearAllPoints(); fs:SetPoint("CENTER", alertObj.textures[idx], "CENTER", 45, 0)
end
end
end
-- Stack displays (Astral Boon left, Natural Boon right; stacks above respective shifted timers)
if procInfo.buffName == "Astral Boon" or procInfo.buffName == "Natural Boon" then
local stacks = buffStackCounts[procInfo.buffName]
if stacks and stacks >= 1 then
local anchorFS = alertObj.timerTexts[1]
if anchorFS then
if not alertObj.stackText then
local parentFrame = anchorFS:GetParent() or ProcDoc or UIParent
local sfs = parentFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
sfs:SetTextColor(1,1,1,1)
if timerTextAlpha then sfs:SetAlpha(timerTextAlpha) end
alertObj.stackText = sfs
end
alertObj.stackText:ClearAllPoints()
-- Center directly above the timer (no horizontal offset)
alertObj.stackText:SetPoint("BOTTOM", anchorFS, "TOP", 0, 2)
alertObj.stackText:SetText(stacks)
alertObj.stackText:Show()
end
else
if alertObj.stackText then alertObj.stackText:Hide() end
end
elseif alertObj.stackText then
alertObj.stackText:Hide()
end
else
-- Ensure timer texts hidden if no duration
if alertObj.timerTexts then
for _, fs in ipairs(alertObj.timerTexts) do if fs then fs:Hide() end end
end
if alertObj.stackText then alertObj.stackText:Hide() end
end
-- 4) If this is a newly gained buff, play the sound (if not muted).
local bName = procInfo.buffName
if not knownBuffProcs[bName] then
if not ProcDocDB.globalVars.isMuted then
ProcDoc_PlayAlertSound()
end
knownBuffProcs[bName] = true
end
end
-- 5) Remove any old buff from knownBuffProcs that has fallen off
for bName in pairs(knownBuffProcs) do
if not activeBuffNames[bName] then
knownBuffProcs[bName] = nil
end
end
-- 6) Hot Streak tier visuals (3=LEFT, 4=LEFT+RIGHT, 5=LEFT+RIGHT+TOP)
if playerClass == "MAGE" and ProcDocDB.procsEnabled[HOT_STREAK_BUFF_NAME] ~= false then
local tier = 0
if hotStreakStacks >= 5 then tier = 5 elseif hotStreakStacks == 4 then tier = 4 elseif hotStreakStacks == 3 then tier = 3 end
-- Hide previous tier frames if tier dropped or returned to 0
if tier == 0 and hotStreakLastTier ~= 0 then
for _, alertObj in ipairs(alertFrames) do
if alertObj.isActive and (alertObj.style == "LEFT" or alertObj.style == "RIGHT" or alertObj.style == "TOP" or alertObj.style == "TOP2") then
-- Only hide those using our Hot Streak textures
local hide = false
for _, tex in ipairs(alertObj.textures) do
local tPath = tex:GetTexture()
if tPath == HOT_STREAK_SIDE_TEXTURE or tPath == HOT_STREAK_TOP_TEXTURE then hide = true break end
end
if hide then