-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathPurity.lua
More file actions
4688 lines (4003 loc) · 186 KB
/
Copy pathPurity.lua
File metadata and controls
4688 lines (4003 loc) · 186 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
-- Purity AddOn - Core (Final Roster Version)
BINDING_HEADER_PURITY = "Purity";
BINDING_NAME_PURITY_TOGGLE = "Toggle Purity Window";
local addonName, Purity = ...
Purity.Version = "12.0.5d"
if not Purity_GlobalSettings then Purity_GlobalSettings = {} end
Purity.BLOODMAGE_CLASS_OVERRIDES = {
["PALADIN"] = { name = "Oath Breaker", colorHex = "FF0000" }
}
function Purity:EnforceDefaultClassColors()
if RAID_CLASS_COLORS and RAID_CLASS_COLORS.PALADIN then
RAID_CLASS_COLORS.PALADIN.r = 0.96
RAID_CLASS_COLORS.PALADIN.g = 0.55
RAID_CLASS_COLORS.PALADIN.b = 0.73
end
end
function Purity:GetThematicClassName(className)
local db = self:GetDB()
local mod = Purity.GlobalModules and Purity.GlobalModules.BLOOD_MAGE_BARGAIN
local rank = (mod and mod.GetOathbreakerRank) and mod:GetOathbreakerRank() or 0
if db.activeChallengeID == "BLOOD_MAGE_BARGAIN" and rank > 0 and self.BLOODMAGE_CLASS_OVERRIDES[className] then
return self.BLOODMAGE_CLASS_OVERRIDES[className].name
end
return className:sub(1,1) .. className:sub(2):lower()
end
function Purity:UpdateCharacterFrameClassName()
local db = self:GetDB()
local _, playerClass = UnitClass("player")
if playerClass ~= "PALADIN" then return end
local mod = Purity.GlobalModules and Purity.GlobalModules.BLOOD_MAGE_BARGAIN
local rank = (mod and mod.GetOathbreakerRank) and mod:GetOathbreakerRank() or 0
local levelTextFrame = _G["CharacterLevelText"]
if not levelTextFrame then return end
local originalText = levelTextFrame:GetText()
if not originalText then return end
if db and db.activeChallengeID == "BLOOD_MAGE_BARGAIN" and rank > 0 then
levelTextFrame:SetText(string.gsub(originalText, "Paladin", "Oath Breaker"))
else
levelTextFrame:SetText(string.gsub(originalText, "Oath Breaker", "Paladin"))
end
end
function Purity:GetThematicClassColor(className, playerData)
if not className then return nil end
local defaultPaladinColor = "F58CBA"
local db = self:GetDB()
local mod = Purity.GlobalModules and Purity.GlobalModules.BLOOD_MAGE_BARGAIN
local rank = (mod and mod.GetOathbreakerRank) and mod:GetOathbreakerRank() or 0
if className == "PALADIN" and db and db.activeChallengeID == "BLOOD_MAGE_BARGAIN" and rank > 0 then
return self.BLOODMAGE_CLASS_OVERRIDES["PALADIN"].colorHex
elseif className == "PALADIN" then
return defaultPaladinColor
end
return nil
end
Purity.hasSentInitialPing = false
Purity.purityChannelID = nil
Purity.ADDON_PREFIX = "PURITYCOMMS"
Purity.roster = {}
local securePingFrame = CreateFrame("Frame", "PuritySecurePingFrame")
securePingFrame:RegisterForDrag("LeftButton")
function securePingFrame:SendPing(channelID)
if channelID then
SendChatMessage("!purity_ping", "CHANNEL", nil, channelID)
end
end
Purity_Warning = "NOTICE: The integrity of this character's challenge data is paramount. Any manual modification will be detected and will result in the forfeiture of your run."
Purity_PerCharacterDB = Purity_PerCharacterDB
Purity.isTrainerHooked = false
Purity.ClassModules = {}
Purity.GlobalModules = {}
Purity.selectedChallenge = nil
Purity.hasUIBeenCreated = false
-- The Core Shadow Vault
local secureCoreState = {
isActive = false,
status = "Not Participating",
weaponInfractions = 0,
physicalStrikes = 0
}
function Purity:SyncSecureStateFromDB()
local db = self:GetDB()
secureCoreState.status = db.status or "Not Participating"
secureCoreState.weaponInfractions = db.weaponInfractions or 0
secureCoreState.physicalStrikes = db.physicalStrikes or 0
secureCoreState.isActive = true
end
local _, _, _, interfaceVersion = GetBuildInfo()
local isMoP = (interfaceVersion >= 50000)
local MAX_PLAYER_LEVEL = 60 -- Default to Classic Era / SoD / Hardcore
if interfaceVersion >= 20000 and interfaceVersion < 30000 then
MAX_PLAYER_LEVEL = 70 -- TBC Classic
elseif interfaceVersion >= 30000 and interfaceVersion < 40000 then
MAX_PLAYER_LEVEL = 80 -- WotLK Classic
elseif interfaceVersion >= 40000 and interfaceVersion < 50000 then
MAX_PLAYER_LEVEL = 85 -- Cata Classic
elseif interfaceVersion >= 50000 then
MAX_PLAYER_LEVEL = 90 -- MoP Classic
end
local isMonitoring = false
local weaponTimer = nil
local purityRuntimeTicker = nil
local purityPlayedTimeTicker = nil
local uptimeMonitorTicker = nil
local activeClassModule = nil
local monitorFrame = nil
local trainerKey = "a7K9!zPq@3rT$5wX&8nMbVcFgHjL"
local Base64 = {}
local BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
Purity.DRINK_LIST = {
["Bottle of Dalaran Noir"] = true,
["Cheap Beer"] = true,
["Holiday Spirits"] = true,
["Rhapsody Malt"] = true,
["Thunder Ale"] = true,
["Moonglow"] = true,
["Evermurky"] = true,
["Flask of Stormwind Tawny"] = true,
["Skin of Dwarven Stout"] = true,
["Southshore Stout"] = true,
["Steamwheedle Fizzy Spirits"] = true,
["Wizbang's Special Brew"] = true,
["Cherry Grog"] = true,
["Cuergo's Gold"] = true,
["Flagon of Dwarven Honeymead"] = true,
["Greatfather's Winter Ale"] = true,
["Jug of Badlands Bourbon"] = true,
["Junglevine Wine"] = true,
["Molasses Firewater"] = true,
["Volatile Rum"] = true,
["Cuergo's Gold with Worm"] = true,
["Dark Dwarven Lager"] = true,
["Darkmoon Special Reserve"] = true,
}
function Base64.encode(data)
local result = {}
local len = #data
for i = 1, len, 3 do
local b1 = data:byte(i)
local b2 = data:byte(i + 1) or 0
local b3 = data:byte(i + 2) or 0
local combined = bit.bor(bit.lshift(b1, 16), bit.lshift(b2, 8), b3)
table.insert(result, BASE64_CHARS:sub(bit.rshift(combined, 18) + 1, bit.rshift(combined, 18) + 1))
table.insert(result, BASE64_CHARS:sub(bit.band(bit.rshift(combined, 12), 0x3F) + 1, bit.band(bit.rshift(combined, 12), 0x3F) + 1))
table.insert(result, BASE64_CHARS:sub(bit.band(bit.rshift(combined, 6), 0x3F) + 1, bit.band(bit.rshift(combined, 6), 0x3F) + 1))
table.insert(result, BASE64_CHARS:sub(bit.band(combined, 0x3F) + 1, bit.band(combined, 0x3F) + 1))
end
local encoded_string = table.concat(result)
local padding = len % 3
if padding == 1 then
encoded_string = encoded_string:sub(1, #encoded_string - 2) .. "=="
elseif padding == 2 then
encoded_string = encoded_string:sub(1, #encoded_string - 1) .. "="
end
return encoded_string
end
local HIDE_RTP_CHAT_MSG_BUFFER = 0
local HIDE_RTP_CHAT_MSG_BUFFER_MAX = 5
-- Filter the CHAT_MSG_SYSTEM event directly
ChatFrame_AddMessageEventFilter("CHAT_MSG_SYSTEM", function(frame, event, message, ...)
if HIDE_RTP_CHAT_MSG_BUFFER > 0 then
-- Get the localized global strings and strip the "%s" variable
-- "Total time played: %s" -> "Total time played: "
local totalPrefix = string.gsub(TIME_PLAYED_TOTAL, "%%s", "")
local levelPrefix = string.gsub(TIME_PLAYED_LEVEL, "%%s", "")
-- Check if the message starts with "Total time played"
if string.find(message, totalPrefix, 1, true) then
-- Hide this line, but don't decrement buffer yet (the "Level" line comes next)
return true
end
-- Check if the message starts with "Time played this level"
if string.find(message, levelPrefix, 1, true) then
-- This is the second message, so we decrement the buffer now
HIDE_RTP_CHAT_MSG_BUFFER = HIDE_RTP_CHAT_MSG_BUFFER - 1
if HIDE_RTP_CHAT_MSG_BUFFER < 0 then HIDE_RTP_CHAT_MSG_BUFFER = 0 end
return true -- Hide message
end
end
return false, message, ...
end)
Purity.ChallengeCoefficients = {
["Path of the Unburdened"] = 5.00,
["The Ringbearer's Vow"] = 4.90,
["Path of Resilience"] = 4.86,
["The Blood Mage's Bargain"] = 4.65,
["The Glass Heart (Extreme)"] = 4.50,
["Quiver of Purity"] = 4.45,
["Path of Humility"] = 4.40,
["Brand of Purity"] = 4.35,
["Fisherman's Folly"] = 4.32,
["Shroud of Purity"] = 3.95,
["Libram of Purity"] = 4.25,
["Tether of Purity"] = 3.80,
["Conduit of Purity"] = 4.10,
["Crackling Tome of Purity"] = 4.05,
["Astrolabe of Purity"] = 4.00,
["Flame of Purity"] = 3.91,
["Sacrament of Purity"] = 3.85,
["Pact of Purity"] = 3.85,
["Bond of Purity"] = 3.80,
["Testament of Purity"] = 3.77,
["The Drunken Master"] = 3.65,
["The Glass Heart (Hard)"] = 3.50,
["Oath of Purity"] = 3.23,
["Contract of Purity"] = 3.20,
["Covenant of Purity"] = 3.00,
["Bulwark of Purity"] = 2.75,
["Communion of Purity"] = 2.68,
["Grimoire of Purity"] = 2.25,
["Burnt Tome of Purity"] = 2.27,
["Frozen Tome of Purity"] = 2.14,
["Foil of Purity"] = 2.00
}
Purity.HardcoreRealms = {
-- NA Realms
["Doomhowl"] = true,
["Defias Pillager"] = true,
["Skull Rock"] = true,
-- EU Realms
["Soulseeker"] = true,
["Nek'Rosh"] = true,
["Stitches"] = true
}
function Purity:IsOnCommunityHardcoreChallenge()
if not Hardcore_Character or not Hardcore_GetSecurityStatus then
return false
end
if Hardcore_Character.guid ~= UnitGUID("player") then
return false
end
if Hardcore_GetSecurityStatus() ~= "OK" then
return false
end
if Hardcore_Character.deaths and next(Hardcore_Character.deaths) == nil then
return true
end
return false
end
function Purity:IsHardcoreStatusValid()
if not Hardcore_Character or not Hardcore_GetSecurityStatus then
return false, "Hardcore addon not detected."
end
if Hardcore_GetSecurityStatus() ~= "OK" then
return false, "Hardcore data security check failed."
end
local status = Hardcore_Character.verification_status
if status == "FAIL" then
return false, "Hardcore challenge is marked as 'FAIL'."
end
return true
end
function Purity_TogglePanel()
if Purity and Purity.mainInterfaceFrame then
if Purity.mainInterfaceFrame:IsShown() then
Purity.mainInterfaceFrame:Hide()
else
Purity.mainInterfaceFrame:Show()
Purity:selectTab("status")
end
end
end
function Purity:UpdateAllModifierStatuses()
local db = self:GetDB()
if not db then return end
local wasHardcore, wasSelfFound, wasSSF = db.isHardcoreRun, db.isSelfFoundRun, db.isSSFRun
local isNowHardcore, isNowSelfFound, isNowSSF = false, false, false
local realmName = GetRealmName()
if realmName and Purity.HardcoreRealms[realmName] then
isNowHardcore = true
end
if self:IsOnCommunityHardcoreChallenge() then
isNowSSF = true
isNowHardcore = true
end
-- Iterate through all buffs
for i = 1, 40 do
local name, _, _, _, _, _, _, _, _, spellId = UnitAura("player", i)
if not name then
break
end
if spellId == 431567 then
isNowSelfFound = true
isNowHardcore = true
elseif spellId == 364001 then
isNowHardcore = true
end
end
if wasHardcore ~= isNowHardcore or wasSelfFound ~= isNowSelfFound or wasSSF ~= isNowSSF then
db.isHardcoreRun = isNowHardcore
db.isSelfFoundRun = isNowSelfFound
db.isSSFRun = isNowSSF
if db.isOptedIn then
local prefix = "|cffFFFF00Purity:|r "
if wasHardcore ~= isNowHardcore then
if isNowHardcore then
print(prefix .. "Hardcore status detected. |cff00FF00(Mode Enabled)|r")
else
print(prefix .. "Hardcore status lost. |cffFF0000(Mode Disabled)|r")
end
end
if wasSelfFound ~= isNowSelfFound then
if isNowSelfFound then
print(prefix .. "Self-Found status detected. |cff00FF00(Mode Enabled)|r")
else
print(prefix .. "Self-Found status lost. |cffFF0000(Mode Disabled)|r")
end
end
if wasSSF ~= isNowSSF then
if isNowSSF then
print(prefix .. "Community SSF status detected. |cff00FF00(Mode Enabled)|r")
else
print(prefix .. "Community SSF status lost. |cffFF0000(Mode Disabled)|r")
end
end
end
end
end
function Purity:StartModifierMonitor()
if self.modifierTicker then self.modifierTicker:Cancel() end
self.modifierTicker = C_Timer.NewTicker(5, function()
self:UpdateAllModifierStatuses()
end)
end
function Purity:GetCurrentChallengeInfo()
local db = self:GetDB()
if not db or not db.challengeTitle then return nil, 0 end
-- Helper to get coeff for a specific ID
local function GetCoeff(name)
return Purity.ChallengeCoefficients[name] or 1.0
end
local mainKey = db.challengeTitle
local activeChallenge = self:GetActiveChallengeObject()
local specifier = nil
-- 1. Retrieve the specifier (e.g. "HARD", "EXTREME", "Fire", etc.)
if activeChallenge and activeChallenge.GetChallengeSpecifier then
specifier = activeChallenge:GetChallengeSpecifier()
end
-- 2. Modify mainKey based on the specifier to match Purity.ChallengeCoefficients keys
if mainKey == "The Ascetic's Path" and specifier then
if specifier == "EASY" then mainKey = "Path of Humility"
elseif specifier == "MEDIUM" then mainKey = "Path of Resilience"
elseif specifier == "HARD" then mainKey = "Path of the Unburdened" end
elseif mainKey == "Tome of Purity" and specifier then
if specifier:upper() == "FIRE" then mainKey = "Burnt Tome of Purity"
elseif specifier:upper() == "FROST" then mainKey = "Frozen Tome of Purity"
elseif specifier:upper() == "ARCANE" then mainKey = "Crackling Tome of Purity" end
elseif mainKey == "The Glass Heart" and specifier then
-- Converts "HARD" -> "The Glass Heart (Hard)"
local formattedSpec = specifier:sub(1,1):upper()..specifier:sub(2):lower()
mainKey = string.format("The Glass Heart (%s)", formattedSpec)
end
local mainCoeff = GetCoeff(mainKey)
local finalCoeff = mainCoeff
local displayName = mainKey
-- MoP Logic: Weighted Average for Death Knights
if db.dkDestinyID then
local destinyCoeff = GetCoeff(db.dkDestinyID)
-- Weighted Average: (Vow + Destiny) / 2
finalCoeff = (mainCoeff + destinyCoeff) / 2
displayName = mainKey .. " + " .. db.dkDestinyID
end
return displayName, finalCoeff
end
function Purity:CalculateTotalCoefficient()
local _, baseCoeff = self:GetCurrentChallengeInfo()
-- [Insert your existing GameplayModifiers logic here (Hardcore/SSF)] --
local multiplier = 1.0
local modifiers = self:GetGameplayModifiers()
if modifiers.isSSF then multiplier = 4.0
elseif modifiers.isSelfFound then multiplier = 3.0
elseif modifiers.isHardcore then multiplier = 2.0
end
return baseCoeff * multiplier
end
function Purity:GetTotalCoefficient()
return self:CalculateTotalCoefficient()
end
function Purity:GetGameplayModifiers()
local db = self:GetDB()
local modifiers = {
isHardcore = false,
isSelfFound = false,
isSSF = false
}
local isHardcoreStatusValid = self:IsHardcoreStatusValid()
local isHC = db.isHardcoreRun
local isSF = db.isSelfFoundRun
local isSSF = db.isSSFRun and isHardcoreStatusValid
if isSSF then
modifiers.isSSF = true
modifiers.isHardcore = true
elseif isSF then
modifiers.isSelfFound = true
modifiers.isHardcore = true
elseif isHC then
modifiers.isHardcore = true
end
return modifiers
end
function Purity:GetDB()
if Purity_PerCharacterDB == nil then
Purity:InitializeDatabase()
end
return Purity_PerCharacterDB
end
function Purity:GetActiveChallengeObject()
local db = self:GetDB()
if not db.isOptedIn then
return nil
end
local challengeID = db.activeChallengeID
local moduleType = db.activeChallengeModuleType
if moduleType == "Global" and Purity.GlobalModules and Purity.GlobalModules[challengeID] then
return Purity.GlobalModules[challengeID]
elseif moduleType == "Class" and activeClassModule then
if activeClassModule.challenges then
for key, challengeData in pairs(activeClassModule.challenges) do
if challengeData.challengeName == challengeID then
return challengeData
end
end
return nil
else
if activeClassModule.challengeName == challengeID then
return activeClassModule
end
end
end
return nil
end
function Purity:SilentRequestTimePlayed()
-- Increment the buffer so the filter knows to hide the next set of messages
HIDE_RTP_CHAT_MSG_BUFFER = HIDE_RTP_CHAT_MSG_BUFFER + 1
if HIDE_RTP_CHAT_MSG_BUFFER > HIDE_RTP_CHAT_MSG_BUFFER_MAX then
HIDE_RTP_CHAT_MSG_BUFFER = HIDE_RTP_CHAT_MSG_BUFFER_MAX
end
RequestTimePlayed()
end
function Purity:FormatHex(n)
local hex = ""
for i = 7, 0, -1 do
local nibble = bit.band(bit.rshift(n, i * 4), 0xF)
hex = hex .. string.format("%x", nibble)
end
return hex
end
function Purity:CreateBackground(parent, r, g, b, a)
local bg = parent:CreateTexture(nil, "BACKGROUND", nil, -8)
bg:SetAllPoints(parent)
bg:SetColorTexture(r or 0.05, g or 0.05, b or 0.1, a or 0.9)
return bg
end
function Purity:MarkDBDirty()
end
function Purity:InitializeDatabase()
if Purity_PerCharacterDB == nil then
Purity_PerCharacterDB = {}
end
local defaults = {
isOptedIn = false, status = "Not Participating",
startDate = "N/A", completionDate = "N/A", addonRuntime = 0,
totalPlayedTime = 0, finalUptime = nil, verificationCode = nil,
hasBeenNotifiedOfLevelCap = false,
weaponInfractions = 0,
activeChallengeID = nil,
challengeTitle = nil,
playerGUID = nil,
dataSignature = nil,
uptimeGrace = 0,
addonRuntimeAtLastPlayedSync = 0,
physicalStrikes = 0,
activeChallengeModuleType = nil,
fishingFishedItemLinks = {},
uptimeIsUnverified = false,
failureReason = "N/A",
isHardcoreRun = false,
isSelfFoundRun = false,
isSSFRun = false,
challengeStats = {},
bloodBarIsSeparate = false,
dkDestinyID = nil,
sequenceID = 0,
bloodLogVisible = false,
glassLogVisible = false,
}
for key, value in pairs(defaults) do
if Purity_PerCharacterDB[key] == nil then
Purity_PerCharacterDB[key] = value
end
end
if Purity_PerCharacterDB.isOptedIn == false and Purity_PerCharacterDB.status == "Passing" then
Purity_PerCharacterDB.status = "Not Participating"
end
end
local FCT_INCOMING_DAMAGE_EVENTS = {
"DAMAGE",
"DAMAGE_CRIT",
"SPELL_DAMAGE",
"SPELL_DAMAGE_CRIT",
"PERIODIC_DAMAGE"
}
function Purity:DisableDefaultIncomingDamageText()
local isLoaded = (C_AddOns and C_AddOns.IsAddOnLoaded) and C_AddOns.IsAddOnLoaded("Blizzard_CombatText") or (type(IsAddOnLoaded) == "function" and IsAddOnLoaded("Blizzard_CombatText"))
-- Ensure the Blizzard addon is awake so we can edit its table
if not isLoaded then
if C_AddOns and C_AddOns.LoadAddOn then
C_AddOns.LoadAddOn("Blizzard_CombatText")
else
LoadAddOn("Blizzard_CombatText")
end
end
if COMBAT_TEXT_TYPE_INFO then
for _, event in ipairs(FCT_INCOMING_DAMAGE_EVENTS) do
if COMBAT_TEXT_TYPE_INFO[event] then
COMBAT_TEXT_TYPE_INFO[event].show = false
end
end
end
end
function Purity:RestoreDefaultIncomingDamageText()
local isLoaded = (C_AddOns and C_AddOns.IsAddOnLoaded) and C_AddOns.IsAddOnLoaded("Blizzard_CombatText") or (type(IsAddOnLoaded) == "function" and IsAddOnLoaded("Blizzard_CombatText"))
if isLoaded and COMBAT_TEXT_TYPE_INFO then
for _, event in ipairs(FCT_INCOMING_DAMAGE_EVENTS) do
if COMBAT_TEXT_TYPE_INFO[event] then
COMBAT_TEXT_TYPE_INFO[event].show = true
end
end
end
end
function Purity:InternalResetChallenge()
local db = Purity:GetDB()
Purity:RestoreDefaultIncomingDamageText()
db.isOptedIn = false
db.status = "Not Participating"
if secureCoreState then
secureCoreState.status = "Not Participating"
secureCoreState.isActive = false
end
db.startDate = "N/A"
db.completionDate = "N/A"
db.addonRuntime = 0
db.totalPlayedTime = 0
db.finalUptime = nil
db.verificationCode = nil
db.hasBeenNotifiedOfLevelCap = false
db.weaponInfractions = 0
db.activeChallengeID = nil
db.challengeTitle = nil
db.playerGUID = nil
db.dataSignature = nil
db.uptimeSignature = nil
db.uptimeGrace = 0
db.addonRuntimeAtLastPlayedSync = 0 -- ADDED THIS LINE
db.physicalStrikes = 0
db.activeChallengeModuleType = nil
db.uptimeIsUnverified = false
db.addonRuntime_lastHash = 0
db.totalPlayedTime_lastHash = 0
db.failureReason = "N/A"
db.isHardcoreRun = false
db.isSelfFoundRun = false
db.isSSFRun = false
db.challengeStats = {}
if db.fishingFishedItemLinks then
wipe(db.fishingFishedItemLinks)
end
isMonitoring = false
if purityRuntimeTicker then purityRuntimeTicker:Cancel(); purityRuntimeTicker = nil end
if purityPlayedTimeTicker then purityPlayedTimeTicker:Cancel(); purityPlayedTimeTicker = nil end
if self.communityHCTicker then self.communityHCTicker:Cancel(); self.communityHCTicker = nil end
if uptimeMonitorTicker then uptimeMonitorTicker:Cancel(); uptimeMonitorTicker = nil end
if weaponTimer then weaponTimer:Cancel(); weaponTimer = nil; Purity.weaponWarningFrame:Hide() end
end
function Purity:ResetChallenge()
Purity:InternalResetChallenge()
if Purity.mainInterfaceFrame and Purity.mainInterfaceFrame:IsShown() then
Purity.mainInterfaceFrame:Hide()
end
if UnitLevel("player") == 1 then
Purity.optInFrame:Show()
end
end
function Purity:DisplayRules()
local currentDB = Purity:GetDB()
local activeChallenge = self:GetActiveChallengeObject()
if not activeChallenge then
Purity.rulesPane.title:SetText("No Active Challenge")
if Purity.rulesPane.scrollChild and Purity.rulesPane.scrollChild.lines then
for _, line in ipairs(Purity.rulesPane.scrollChild.lines) do line:Hide() end
end
return
end
Purity.rulesPane.title:SetText(currentDB.challengeTitle or activeChallenge.challengeName)
local rules = activeChallenge:GetRulesText()
local scrollChild = Purity.rulesPane.scrollChild
if not scrollChild.lines then scrollChild.lines = {} end
for _, line in ipairs(scrollChild.lines) do
line:Hide()
end
local yOffset = -10
local defaultLineSpacing = 15
local emptyLineSpacing = 10
local lineIndex = 1
for _, lineText in ipairs(rules) do
local line = scrollChild.lines[lineIndex]
if not line then
line = scrollChild:CreateFontString(nil, "ARTWORK", "GameFontNormal")
line:SetJustifyH("LEFT")
table.insert(scrollChild.lines, line)
end
line:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 10, yOffset)
line:SetPoint("TOPRIGHT", scrollChild, "TOPRIGHT", -10, yOffset)
line:SetText(lineText)
line:Show()
local stringHeight = line:GetStringHeight()
if lineText == " " then
yOffset = yOffset - emptyLineSpacing
else
yOffset = yOffset - stringHeight - defaultLineSpacing
end
lineIndex = lineIndex + 1
end
scrollChild:SetHeight(math.abs(yOffset) + 20)
Purity.rulesPane.scrollFrame:SetVerticalScroll(0)
end
function Purity:BuildChallengeTypeMap()
self.ChallengeTypeMap = {}
-- Process Global Modules
if self.GlobalModules then
for _, module in pairs(self.GlobalModules) do
if module.challengeName == "The Ascetic's Path" then
self.ChallengeTypeMap["Path of Humility"] = "Global"
self.ChallengeTypeMap["Path of Resilience"] = "Global"
self.ChallengeTypeMap["Path of the Unburdened"] = "Global"
elseif module.challengeName == "The Glass Heart" then
self.ChallengeTypeMap["The Glass Heart (Hard)"] = "Global"
self.ChallengeTypeMap["The Glass Heart (Extreme)"] = "Global"
elseif module.challengeName then
self.ChallengeTypeMap[module.challengeName] = "Global"
end
end
end
-- Process Class-Specific Modules
if self.ClassModules then
for className, classModule in pairs(self.ClassModules) do
local friendlyClassName = className:sub(1,1) .. className:sub(2):lower()
if classModule.challenges then
for _, challengeData in pairs(classModule.challenges) do
if challengeData.challengeName == "Tome of Purity" then
self.ChallengeTypeMap["Crackling Tome of Purity"] = friendlyClassName
self.ChallengeTypeMap["Burnt Tome of Purity"] = friendlyClassName
self.ChallengeTypeMap["Frozen Tome of Purity"] = friendlyClassName
else
self.ChallengeTypeMap[challengeData.challengeName] = friendlyClassName
end
end
elseif classModule.challengeName then
if classModule.challengeName == "Tome of Purity" then
self.ChallengeTypeMap["Crackling Tome of Purity"] = friendlyClassName
self.ChallengeTypeMap["Burnt Tome of Purity"] = friendlyClassName
self.ChallengeTypeMap["Frozen Tome of Purity"] = friendlyClassName
else
self.ChallengeTypeMap[classModule.challengeName] = friendlyClassName
end
end
end
end
end
function Purity:DisplayCompletionStats()
local db = self:GetDB()
if not db or (not db.challengeStats and not db.fishingFishedItemLinks) then return end
local stats = db.challengeStats or {}
local challenge = db.challengeTitle
local message
if challenge == "Sacrament of Purity" and stats.lifeTapCasts then
message = string.format("Fun fact: During your challenge, you cast Life Tap %d times!", stats.lifeTapCasts)
elseif challenge == "Grimoire of Purity" and stats.immolateCasts then
message = string.format("Fun fact: During your demonic studies, you cast Immolate %d times!", stats.immolateCasts)
elseif challenge == "Brand of Purity" and stats.chargeInterceptCasts then
message = string.format("Fun fact: During your challenge, you Charged or Intercepted %d times!", stats.chargeInterceptCasts)
elseif challenge == "Bulwark of Purity" and stats.blocks then
message = string.format("Fun fact: As an ardent protector, you successfully blocked %d attacks!", stats.blocks)
elseif db.activeChallengeID == "Tome of Purity" and stats.primarySpellCasts then
message = string.format("Fun fact: During your studies, you cast your primary spell %d times!", stats.primarySpellCasts)
elseif challenge == "Conduit of Purity" then
local charge = stats.chargeAccumulatedCombat or 0
message = string.format("Fun fact: Through constant motion, you generated %d Static Charge during combat!", math.floor(charge))
elseif challenge == "Testament of Purity" and stats.smiteCasts then
message = string.format("Fun fact: To uphold your testament, you cast Smite %d times!", stats.smiteCasts)
elseif challenge == "Covenant of Purity" and stats.mindFlayCasts then
message = string.format("Fun fact: Embracing the shadows, you channeled Mind Flay %d times!", stats.mindFlayCasts)
elseif challenge == "Oath of Purity" and stats.holyLightCasts then
message = string.format("Fun fact: As a selfless guardian, you cast Holy Light %d times!", stats.holyLightCasts)
elseif challenge == "Libram of Purity" and stats.exorcismCasts then
message = string.format("Fun fact: In your crusade against the undead, you cast Exorcism %d times!", stats.exorcismCasts)
elseif challenge == "Communion of Purity" and stats.lightningBoltCasts then
message = string.format("Fun fact: In communion with the elements, you cast Lightning Bolt %d times!", stats.lightningBoltCasts)
elseif challenge == "Flame of Purity" and stats.fireSpellCasts then
message = string.format("Fun fact: Your inner flame burned bright, leading you to cast %d fire spells!", stats.fireSpellCasts)
elseif challenge == "Pact of Purity" and stats.shapeshiftCasts then
message = string.format("Fun fact: To protect the wilds, you shapeshifted into Bear Form %d times!", stats.shapeshiftCasts)
elseif challenge == "Astrolabe of Purity" and stats.celestialCasts then
message = string.format("Fun fact: To maintain celestial balance, you wove %d solar and lunar spells!", stats.celestialCasts)
elseif challenge == "Contract of Purity" and stats.sinisterStrikeCasts then
message = string.format("Fun fact: As an honorable duelist, you used Sinister Strike %d times!", stats.sinisterStrikeCasts)
elseif challenge == "Foil of Purity" and stats.riposteCasts then
message = string.format("Fun fact: With your fencer's grace, you successfully Riposted %d times!", stats.riposteCasts)
elseif challenge == "Bond of Purity" and stats.mendPetCasts then
message = string.format("Fun fact: To maintain your bond, you mended your pet %d times!", stats.mendPetCasts)
elseif challenge == "Quiver of Purity" and stats.aimedShotCasts then
message = string.format("Fun fact: As a lone wolf, you took aim and fired %d Aimed Shots!", stats.aimedShotCasts)
elseif challenge == "Fisherman's Folly" then
local fishCount = stats.totalCatches or 0
local trunkCount = stats.trunksFished or 0
message = string.format("Fun fact: During your folly, you had %d successful catches, including %d trunks!", fishCount, trunkCount) message = string.format("Fun fact: During your folly, you had %d successful catches, including %d trunks!", fishCount, trunkCount)
elseif challenge == "The Glass Heart" and stats.lowestGlassHP then
message = string.format("Fun fact: You walked the razor's edge! The closest your Glass Heart came to shattering was at %.1f%% integrity.", stats.lowestGlassHP)
elseif challenge == "The Ascetic's Path" and stats.forbiddenItemsSold then
message = string.format("Fun fact: On your path of self-denial, you sold %d items that you were forbidden to equip!", stats.forbiddenItemsSold)
end
if message then
print("|cffFFFF00Purity:|r " .. message)
end
end
function Purity:DisplayRankings()
local pane = self.rankingsPane
if not (pane and pane.scrollFrame and pane.scrollChild) then return end
local scrollChild = pane.scrollChild
local scrollFrame = pane.scrollFrame
-- Clear existing lines from the scroll child
if scrollChild.lines then
for _, line in ipairs(scrollChild.lines) do
line:Hide()
end
end
scrollChild.lines = {}
local goldColor = "|cffffd100"
local whiteColor = "|cffffffff"
local darkColor = "|cff261a0d"
local greenColor = "|cff00FF00"
local yOffset = -15
local lineSpacing = 22
local totalHeight = 20
-- [SECTION 1] GAMEPLAY MODIFIERS (The missing info)
local function AddHeader(text)
local h = scrollChild:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
h:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 10, yOffset)
h:SetText(goldColor .. text .. "|r")
table.insert(scrollChild.lines, h)
yOffset = yOffset - 25
totalHeight = totalHeight + 25
end
local function AddModLine(name, value)
local label = scrollChild:CreateFontString(nil, "ARTWORK", "GameFontNormal")
label:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 20, yOffset)
label:SetText(whiteColor .. name .. "|r")
local val = scrollChild:CreateFontString(nil, "ARTWORK", "GameFontNormal")
val:SetPoint("TOPRIGHT", scrollChild, "TOPRIGHT", -20, yOffset)
val:SetText(greenColor .. value .. "|r")
table.insert(scrollChild.lines, label)
table.insert(scrollChild.lines, val)
yOffset = yOffset - 18
totalHeight = totalHeight + 18
end
AddHeader("Gameplay Multipliers")
AddModLine("Hardcore (Soul of Iron)", "x2.0")
AddModLine("Self-Found (Official Buff)", "x3.0")
AddModLine("SSF (Hardcore AddOn)", "x4.0")
-- Add a separator line
yOffset = yOffset - 10
local separator = scrollChild:CreateTexture(nil, "ARTWORK")
separator:SetHeight(1)
separator:SetColorTexture(1, 1, 1, 0.2)
separator:SetPoint("LEFT", 10, 0)
separator:SetPoint("RIGHT", -10, 0)
separator:SetPoint("TOP", scrollChild, "TOP", 0, yOffset)
table.insert(scrollChild.lines, separator) -- Add to lines table so it gets hidden on refresh
yOffset = yOffset - 20
totalHeight = totalHeight + 30
-- [SECTION 2] CHALLENGE RANKINGS
AddHeader("Challenge Base Coefficients")
local sortedChallenges = {}
if not self.ChallengeCoefficients then return end
for name, coeff in pairs(self.ChallengeCoefficients) do
table.insert(sortedChallenges, {name = name, coeff = coeff})
end
table.sort(sortedChallenges, function(a, b)
return a.coeff > b.coeff
end)
for i, challengeData in ipairs(sortedChallenges) do
local rankText = string.format("%d.", i)
local challengeName = challengeData.name
local coefficientText = string.format("%.2f", challengeData.coeff)
local challengeType = (self.ChallengeTypeMap and self.ChallengeTypeMap[challengeName]) or ""
local challengeNameText = challengeName
if challengeType ~= "" then
local typeColor
local classUpper = string.upper(challengeType)
if classUpper == "SHAMAN" then
typeColor = "|cff0070DD"
elseif classUpper == "PALADIN" then
typeColor = "|cfff48cba"
else
local classInfo = RAID_CLASS_COLORS[classUpper]
if classInfo and challengeType ~= "Global" then
typeColor = string.format("|cff%02x%02x%02x", classInfo.r*255, classInfo.g*255, classInfo.b*255)
else
typeColor = "|cffb0b0b0" -- Grey fallback for "Global" or unknown
end
end
challengeNameText = string.format("%s (%s%s|r)", challengeName, typeColor, challengeType)
end
local rankLine = scrollChild:CreateFontString(nil, "ARTWORK", "GameFontNormal")
rankLine:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 20, yOffset)
rankLine:SetText(goldColor .. rankText .. "|r")
table.insert(scrollChild.lines, rankLine)
local coeffLine = scrollChild:CreateFontString(nil, "ARTWORK", "GameFontNormal")
coeffLine:SetPoint("TOPRIGHT", scrollChild, "TOPRIGHT", -20, yOffset)
coeffLine:SetText(goldColor .. coefficientText .. "|r")
table.insert(scrollChild.lines, coeffLine)
local nameLine = scrollChild:CreateFontString(nil, "ARTWORK", "GameFontNormal")
nameLine:SetPoint("LEFT", rankLine, "RIGHT", 15, 0)
nameLine:SetPoint("RIGHT", coeffLine, "LEFT", -10, 0) -- Prevents overlap
nameLine:SetJustifyH("LEFT")
nameLine:SetText(darkColor .. challengeNameText .. "|r")
table.insert(scrollChild.lines, nameLine)
yOffset = yOffset - lineSpacing
totalHeight = totalHeight + lineSpacing
end
scrollChild:SetHeight(totalHeight)
scrollFrame:SetVerticalScroll(0)
end
function Purity:GenerateVerificationHash(fullStringForHashing)
local hash = 0
for i = 1, #fullStringForHashing do
local char_code = string.byte(fullStringForHashing, i)
hash = (hash * 31 + char_code) % 2^32
end
return self:FormatHex(hash)
end
function Purity:UpdateAndGetStatusStrings()
local data = self:GetRawStatusData()
local statusColor = "|cff00FF00"
if data.status == "Failed" then statusColor = "|cffFF0000"
elseif data.status == "Not Participating" then statusColor = "|cff888888"
elseif data.status == "Temporary Failure - Uptime" then statusColor = "|cffFFFF00"
elseif data.status == "Passed" then statusColor = "|cff00FF00" end
local goldColor = "|cffffd100"
local darkColor = "|cff261a0d"
local currentUptime = (data.totalPlayed > 0 and (data.addonRuntime / data.totalPlayed) * 100) or 0
local uptimeDisplay = string.format("%.2f%%", math.min(100, currentUptime))
local uptimeLabel = "Uptime:|r "