-
Notifications
You must be signed in to change notification settings - Fork 13
/
LibHealComm-4.0.lua
2784 lines (2354 loc) · 110 KB
/
LibHealComm-4.0.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
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 major = "LibHealComm-4.0"
local minor = 66
assert(LibStub, string.format("%s requires LibStub.", major))
local HealComm = LibStub:NewLibrary(major, minor)
if( not HealComm ) then return end
-- API CONSTANTS
--local ALL_DATA = 0x0f
local DIRECT_HEALS = 0x01
local CHANNEL_HEALS = 0x02
local HOT_HEALS = 0x04
--local ABSORB_SHIELDS = 0x08
local BOMB_HEALS = 0x10
local ALL_HEALS = bit.bor(DIRECT_HEALS, CHANNEL_HEALS, HOT_HEALS, BOMB_HEALS)
local CASTED_HEALS = bit.bor(DIRECT_HEALS, CHANNEL_HEALS)
local OVERTIME_HEALS = bit.bor(HOT_HEALS, CHANNEL_HEALS)
HealComm.ALL_HEALS, HealComm.CHANNEL_HEALS, HealComm.DIRECT_HEALS, HealComm.HOT_HEALS, HealComm.CASTED_HEALS, HealComm.ABSORB_SHIELDS, HealComm.ALL_DATA, HealComm.BOMB_HEALS = ALL_HEALS, CHANNEL_HEALS, DIRECT_HEALS, HOT_HEALS, CASTED_HEALS, ABSORB_SHIELDS, ALL_DATA, BOMB_HEALS
local COMM_PREFIX = "LHC40"
local playerGUID, playerName, playerLevel
local playerHealModifier = 1
local IS_BUILD30300 = tonumber((select(4, GetBuildInfo()))) >= 30300
HealComm.callbacks = HealComm.callbacks or LibStub:GetLibrary("CallbackHandler-1.0"):New(HealComm)
HealComm.spellData = HealComm.spellData or {}
HealComm.hotData = HealComm.hotData or {}
HealComm.talentData = HealComm.talentData or {}
HealComm.itemSetsData = HealComm.itemSetsData or {}
HealComm.glyphCache = HealComm.glyphCache or {}
HealComm.equippedSetCache = HealComm.equippedSetCache or {}
HealComm.guidToGroup = HealComm.guidToGroup or {}
HealComm.guidToUnit = HealComm.guidToUnit or {}
HealComm.pendingHeals = HealComm.pendingHeals or {}
HealComm.tempPlayerList = HealComm.tempPlayerList or {}
HealComm.activePets = HealComm.activePets or {}
HealComm.activeHots = HealComm.activeHots or {}
if( not HealComm.unitToPet ) then
HealComm.unitToPet = {["player"] = "pet"}
for i=1, MAX_PARTY_MEMBERS do HealComm.unitToPet["party" .. i] = "partypet" .. i end
for i=1, MAX_RAID_MEMBERS do HealComm.unitToPet["raid" .. i] = "raidpet" .. i end
end
local spellData, hotData, tempPlayerList, pendingHeals = HealComm.spellData, HealComm.hotData, HealComm.tempPlayerList, HealComm.pendingHeals
local equippedSetCache, itemSetsData, talentData = HealComm.equippedSetCache, HealComm.itemSetsData, HealComm.talentData
local activeHots, activePets = HealComm.activeHots, HealComm.activePets
-- Figure out what they are now since a few things change based off of this
local playerClass = select(2, UnitClass("player"))
local isHealerClass = playerClass == "DRUID" or playerClass == "PRIEST" or playerClass == "SHAMAN" or playerClass == "PALADIN"
-- Stolen from Threat-2.0, compresses GUIDs from 18 characters to around 8 - 9, 50%/55% savings
-- 44 = , / 58 = : / 255 = \255 / 0 = line break / 64 = @ / 254 = FE, used for escape code so has to be escaped
if( not HealComm.compressGUID or not HealComm.fixedCompress ) then
local map = {[58] = "\254\250", [64] = "\254\251", [44] = "\254\252", [255] = "\254\253", [0] = "\255", [254] = "\254\249"}
local function guidCompressHelper(x)
local a = tonumber(x, 16)
return map[a] or string.char(a)
end
local dfmt = "0x%02X%02X%02X%02X%02X%02X%02X%02X"
local function unescape(str)
str = string.gsub(str, "\255", "\000")
str = string.gsub(str, "\254\250", "\058")
str = string.gsub(str, "\254\251", "\064")
str = string.gsub(str, "\254\252", "\044")
str = string.gsub(str, "\254\253", "\255")
return string.gsub(str, "\254\249", "\254")
end
HealComm.fixedCompress = true
HealComm.compressGUID = setmetatable({}, {
__index = function(tbl, guid)
local cguid = string.match(guid, "0x(.*)")
local str = string.gsub(cguid, "(%x%x)", guidCompressHelper)
rawset(tbl, guid, str)
return str
end})
HealComm.decompressGUID = setmetatable({}, {
__index = function(tbl, str)
if( not str ) then return nil end
local usc = unescape(str)
local a, b, c, d, e, f, g, h = string.byte(usc, 1, 8)
-- Failed to decompress, silently exit
if( not a or not b or not c or not d or not e or not f or not g or not h ) then
return ""
end
local guid = string.format(dfmt, a, b, c, d, e, f, g, h)
rawset(tbl, str, guid)
return guid
end})
end
local compressGUID, decompressGUID = HealComm.compressGUID, HealComm.decompressGUID
-- Handles caching of tables for variable tick spells, like Wild Growth
if( not HealComm.tableCache ) then
HealComm.tableCache = setmetatable({}, {__mode = "k"})
function HealComm:RetrieveTable()
return table.remove(HealComm.tableCache, 1) or {}
end
function HealComm:DeleteTable(tbl)
table.wipe(tbl)
table.insert(HealComm.tableCache, tbl)
end
end
-- Validation for passed arguments
if( not HealComm.tooltip ) then
local tooltip = CreateFrame("GameTooltip")
tooltip:SetOwner(UIParent, "ANCHOR_NONE")
tooltip.TextLeft1 = tooltip:CreateFontString()
tooltip.TextRight1 = tooltip:CreateFontString()
tooltip:AddFontStrings(tooltip.TextLeft1, tooltip.TextRight1)
HealComm.tooltip = tooltip
end
-- So I don't have to keep matching the same numbers every time or create a local copy of every rank -> # map for locals
if( not HealComm.rankNumbers ) then
HealComm.rankNumbers = setmetatable({}, {
__index = function(tbl, index)
local number = tonumber(string.match(index, "(%d+)")) or 1
rawset(tbl, index, number)
return number
end,
})
end
-- Find the spellID by the name/rank combination
-- Need to swap this to a double table metatable something like [spellName][spellRank] so I can reduce the garbage created
if( not HealComm.spellToID ) then
HealComm.spellToID = setmetatable({}, {
__index = function(tbl, index)
-- Find the spell from the spell book and cache the results!
local offset, numSpells = select(3, GetSpellTabInfo(GetNumSpellTabs()))
for id=1, (offset + numSpells) do
-- Match, yay!
local spellName, spellRank = GetSpellName(id, BOOKTYPE_SPELL)
local name = spellName .. spellRank
if( index == name ) then
HealComm.tooltip:SetSpell(id, BOOKTYPE_SPELL)
local spellID = select(3, HealComm.tooltip:GetSpell())
if( spellID ) then
rawset(tbl, index, spellID)
return spellID
end
end
end
rawset(tbl, index, false)
return false
end,
})
end
-- This gets filled out after data has been loaded, this is only for casted heals. Hots just directly pull from the averages as they do not increase in power with level, Cataclysm will change this though.
if( HealComm.averageHealMT and not HealComm.fixedAverage ) then
HealComm.averageHealMT = nil
end
HealComm.fixedAverage = true
HealComm.averageHeal = HealComm.averageHeal or {}
HealComm.averageHealMT = HealComm.averageHealMT or {
__index = function(tbl, index)
local rank = HealComm.rankNumbers[index]
local spellData = HealComm.spellData[rawget(tbl, "spell")]
local spellLevel = spellData.levels[rank]
-- No increase, it doesn't scale with levely
if( not spellData.increase or UnitLevel("player") <= spellLevel ) then
rawset(tbl, index, spellData.averages[rank])
return spellData.averages[rank]
end
local average = spellData.averages[rank]
if( UnitLevel("level") >= MAX_PLAYER_LEVEL ) then
average = average + spellData.increase[rank]
-- Here's how this works: If a spell increases 1,000 between 70 and 80, the player is level 75 the spell is 70
-- it's 1000 / (80 - 70) so 100, the player learned the spell 5 levels ago which means that the spell average increases by 500
-- This figures out how much it increases per level and how ahead of the spells level they are to figure out how much to add
else
average = average + (UnitLevel("player") - spellLevel) * (spellData.increase[rank] / (MAX_PLAYER_LEVEL - spellLevel))
end
rawset(tbl, index, average)
return average
end}
-- Record management, because this is getting more complicted to deal with
local function updateRecord(pending, guid, amount, stack, endTime, ticksLeft)
if( pending[guid] ) then
local id = pending[guid]
pending[id] = guid
pending[id + 1] = amount
pending[id + 2] = stack
pending[id + 3] = endTime or 0
pending[id + 4] = ticksLeft or 0
else
pending[guid] = #(pending) + 1
table.insert(pending, guid)
table.insert(pending, amount)
table.insert(pending, stack)
table.insert(pending, endTime or 0)
table.insert(pending, ticksLeft or 0)
if( pending.bitType == HOT_HEALS ) then
activeHots[guid] = (activeHots[guid] or 0) + 1
HealComm.hotMonitor:Show()
end
end
end
local function getRecord(pending, guid)
local id = pending[guid]
if( not id ) then return nil end
-- amount, stack, endTime, ticksLeft
return pending[id + 1], pending[id + 2], pending[id + 3], pending[id + 4]
end
local function removeRecord(pending, guid)
local id = pending[guid]
if( not id ) then return nil end
-- ticksLeft, endTime, stack, amount, guid
table.remove(pending, id + 4)
table.remove(pending, id + 3)
table.remove(pending, id + 2)
local amount = table.remove(pending, id + 1)
table.remove(pending, id)
pending[guid] = nil
-- Release the table
if( type(amount) == "table" ) then HealComm:DeleteTable(amount) end
if( pending.bitType == HOT_HEALS and activeHots[guid] ) then
activeHots[guid] = activeHots[guid] - 1
activeHots[guid] = activeHots[guid] > 0 and activeHots[guid] or nil
end
-- Shift any records after this ones index down 5 to account for the removal
for i=1, #(pending), 5 do
local guid = pending[i]
if( pending[guid] > id ) then
pending[guid] = pending[guid] - 5
end
end
end
local function removeRecordList(pending, inc, comp, ...)
for i=1, select("#", ...), inc do
local guid = select(i, ...)
guid = comp and decompressGUID[guid] or guid
local id = pending[guid]
-- ticksLeft, endTime, stack, amount, guid
table.remove(pending, id + 4)
table.remove(pending, id + 3)
table.remove(pending, id + 2)
local amount = table.remove(pending, id + 1)
table.remove(pending, id)
pending[guid] = nil
-- Release the table
if( type(amount) == "table" ) then HealComm:DeleteTable(amount) end
end
-- Redo all the id maps
for i=1, #(pending), 5 do
pending[pending[i]] = i
end
end
-- Removes every mention to the given GUID
local function removeAllRecords(guid)
local changed
for _, spells in pairs(pendingHeals) do
for _, pending in pairs(spells) do
if( pending.bitType and pending[guid] ) then
local id = pending[guid]
-- ticksLeft, endTime, stack, amount, guid
table.remove(pending, id + 4)
table.remove(pending, id + 3)
table.remove(pending, id + 2)
local amount = table.remove(pending, id + 1)
table.remove(pending, id)
pending[guid] = nil
-- Release the table
if( type(amount) == "table" ) then HealComm:DeleteTable(amount) end
-- Shift everything back
if( #(pending) > 0 ) then
for i=1, #(pending), 5 do
local guid = pending[i]
if( pending[guid] > id ) then
pending[guid] = pending[guid] - 5
end
end
else
table.wipe(pending)
end
changed = true
end
end
end
activeHots[guid] = nil
if( changed ) then
HealComm.callbacks:Fire("HealComm_GUIDDisappeared", guid)
end
end
-- These are not public APIs and are purely for the wrapper to use
HealComm.removeRecordList = removeRecordList
HealComm.removeRecord = removeRecord
HealComm.getRecord = getRecord
HealComm.updateRecord = updateRecord
-- Removes all pending heals, if it's a group that is causing the clear then we won't remove the players heals on themselves
local function clearPendingHeals()
for casterGUID, spells in pairs(pendingHeals) do
for _, pending in pairs(spells) do
if( pending.bitType ) then
table.wipe(tempPlayerList)
for i=#(pending), 1, -5 do table.insert(tempPlayerList, pending[i - 4]) end
if( #(tempPlayerList) > 0 ) then
local spellID, bitType = pending.spellID, pending.bitType
table.wipe(pending)
HealComm.callbacks:Fire("HealComm_HealStopped", casterGUID, spellID, bitType, true, unpack(tempPlayerList))
end
end
end
end
end
-- APIs
-- Returns the players current heaing modifier
function HealComm:GetPlayerHealingMod()
return playerHealModifier or 1
end
-- Returns the current healing modifier for the GUID
function HealComm:GetHealModifier(guid)
return HealComm.currentModifiers[guid] or 1
end
-- Returns whether or not the GUID has casted a heal
function HealComm:GUIDHasHealed(guid)
return pendingHeals[guid] and true or nil
end
-- Returns the guid to unit table
function HealComm:GetGUIDUnitMapTable()
if( not HealComm.protectedMap ) then
HealComm.protectedMap = setmetatable({}, {
__index = function(tbl, key) return HealComm.guidToUnit[key] end,
__newindex = function() error("This is a read only table and cannot be modified.", 2) end,
__metatable = false
})
end
return HealComm.protectedMap
end
-- Gets the next heal landing on someone using the passed filters
function HealComm:GetNextHealAmount(guid, bitFlag, time, ignoreGUID)
local healTime, healAmount, healFrom
local currentTime = GetTime()
for casterGUID, spells in pairs(pendingHeals) do
if( not ignoreGUID or ignoreGUID ~= casterGUID ) then
for _, pending in pairs(spells) do
if( pending.bitType and bit.band(pending.bitType, bitFlag) > 0 ) then
for i=1, #(pending), 5 do
local guid = pending[i]
local amount = pending[i + 1]
local stack = pending[i + 2]
local endTime = pending[i + 3]
endTime = endTime > 0 and endTime or pending.endTime
-- Direct heals are easy, if they match the filter then return them
if( ( pending.bitType == DIRECT_HEALS or pending.bitType == BOMB_HEALS ) and ( not time or endTime <= time ) ) then
if( not healTime or endTime < healTime ) then
healTime = endTime
healAmount = amount * stack
healFrom = casterGUID
end
-- Channeled heals and hots, have to figure out how many times it'll tick within the given time band
elseif( ( pending.bitType == CHANNEL_HEALS or pending.bitType == HOT_HEALS ) and ( not pending.hasVariableTicks or pending.hasVariableTicks and amount[1] ) ) then
local secondsLeft = time and time - currentTime or endTime - currentTime
local nextTick = currentTime + (secondsLeft % pending.tickInterval)
if( not healTime or nextTick < healTime ) then
healTime = nextTick
healAmount = not pending.hasVariableTicks and amount * stack or amount[1] * stack
healFrom = casterGUID
end
end
end
end
end
end
end
return healTime, healFrom, healAmount
end
-- Get the healing amount that matches the passed filters
local function filterData(spells, filterGUID, bitFlag, time, ignoreGUID)
local healAmount = 0
local currentTime = GetTime()
for _, pending in pairs(spells) do
if( pending.bitType and bit.band(pending.bitType, bitFlag) > 0 ) then
for i=1, #(pending), 5 do
local guid = pending[i]
if( guid == filterGUID or ignoreGUID ) then
local amount = pending[i + 1]
local stack = pending[i + 2]
local endTime = pending[i + 3]
endTime = endTime > 0 and endTime or pending.endTime
-- Direct heals are easy, if they match the filter then return them
if( ( pending.bitType == DIRECT_HEALS or pending.bitType == BOMB_HEALS ) and ( not time or endTime <= time ) ) then
healAmount = healAmount + amount * stack
-- Channeled heals and hots, have to figure out how many times it'll tick within the given time band
elseif( ( pending.bitType == CHANNEL_HEALS or pending.bitType == HOT_HEALS ) and endTime > currentTime ) then
local ticksLeft = pending[i + 4]
if( not time or time >= endTime ) then
if( not pending.hasVariableTicks ) then
healAmount = healAmount + (amount * stack) * ticksLeft
else
for _, heal in pairs(amount) do
healAmount = healAmount + (heal * stack)
end
end
else
local secondsLeft = endTime - currentTime
local bandSeconds = time - currentTime
local ticks = math.floor(math.min(bandSeconds, secondsLeft) / pending.tickInterval)
local nextTickIn = secondsLeft % pending.tickInterval
local fractionalBand = bandSeconds % pending.tickInterval
if( nextTickIn > 0 and nextTickIn < fractionalBand ) then
ticks = ticks + 1
end
if( not pending.hasVariableTicks ) then
healAmount = healAmount + (amount * stack) * math.min(ticks, ticksLeft)
else
for i=1, math.min(ticks, #(amount)) do
healAmount = healAmount + (amount[i] * stack)
end
end
end
end
end
end
end
end
return healAmount
end
-- Gets healing amount using the passed filters
function HealComm:GetHealAmount(guid, bitFlag, time, casterGUID)
local amount = 0
if( casterGUID and pendingHeals[casterGUID] ) then
amount = filterData(pendingHeals[casterGUID], guid, bitFlag, time)
elseif( not casterGUID ) then
for _, spells in pairs(pendingHeals) do
amount = amount + filterData(spells, guid, bitFlag, time)
end
end
return amount > 0 and amount or nil
end
-- Gets healing amounts for everyone except the player using the passed filters
function HealComm:GetOthersHealAmount(guid, bitFlag, time)
local amount = 0
for casterGUID, spells in pairs(pendingHeals) do
if( casterGUID ~= playerGUID ) then
amount = amount + filterData(spells, guid, bitFlag, time)
end
end
return amount > 0 and amount or nil
end
function HealComm:GetCasterHealAmount(guid, bitFlag, time)
local amount = pendingHeals[guid] and filterData(pendingHeals[guid], nil, bitFlag, time, true) or 0
return amount > 0 and amount or nil
end
-- Healing class data
-- Thanks to Gagorian (DrDamage) for letting me steal his formulas and such
local playerCurrentRelic
local averageHeal, rankNumbers = HealComm.averageHeal, HealComm.rankNumbers
local guidToUnit, guidToGroup, glyphCache = HealComm.guidToUnit, HealComm.guidToGroup, HealComm.glyphCache
-- UnitBuff priortizes our buffs over everyone elses when there is a name conflict, so yay for that
local function unitHasAura(unit, name)
return select(8, UnitBuff(unit, name)) == "player"
end
-- Note because I always forget on the order:
-- Talents that effective the coeffiency of spell power to healing are first and are tacked directly onto the coeffiency (Empowered Rejuvenation)
-- Penalty modifiers (downranking/spell level too low) are applied directly to the spell power
-- Spell power modifiers are then applied to the spell power
-- Heal modifiers are applied after all of that
-- Crit modifiers are applied after
-- Any other modifiers such as Mortal Strike or Avenging Wrath are applied after everything else
local function calculateGeneralAmount(level, amount, spellPower, spModifier, healModifier)
-- Apply downranking penalities for spells below 20
local penalty = level > 20 and 1 or (1 - ((20 - level) * 0.0375))
-- Apply further downranking penalities
spellPower = spellPower * (penalty * math.min(1, math.max(0, 1 - (playerLevel - level - 11) * 0.05)))
-- Apply zone modifier
healModifier = healModifier * HealComm.zoneHealModifier
-- Do the general factoring
return healModifier * (amount + (spellPower * spModifier))
end
-- For spells like Wild Growth, it's a waste to do the calculations for each tick, easier to calculate spell power now and then manually calculate it all after
local function calculateSpellPower(level, spellPower)
-- Apply downranking penalities for spells below 20
local penalty = level > 20 and 1 or (1 - ((20 - level) * 0.0375))
-- Apply further downranking penalities
return spellPower * (penalty * math.min(1, math.max(0, 1 - (playerLevel - level - 11) * 0.05)))
end
-- Yes silly function, just cleaner to look at
local function avg(a, b)
return (a + b) / 2
end
--[[
What the different callbacks do:
AuraHandler: Specific aura tracking needed for this class, who has Beacon up on them and such
ResetChargeData: Due to spell "queuing" you can't always rely on aura data for buffs that last one or two casts, for example Divine Favor (+100% crit, one spell)
if you cast Holy Light and queue Flash of Light the library would still see they have Divine Favor and give them crits on both spells. The reset means that the flag that indicates
they have the aura can be killed and if they interrupt the cast then it will call this and let you reset the flags.
What happens in terms of what the client thinks and what actually is, is something like this:
UNIT_SPELLCAST_START, Holy Light -> Divine Favor up
UNIT_SPELLCAST_SUCCEEDED, Holy Light -> Divine Favor up (But it was really used)
UNIT_SPELLCAST_START, Flash of Light -> Divine Favor up (It's not actually up but auras didn't update)
UNIT_AURA -> Divine Favor up (Split second where it still thinks it's up)
UNIT_AURA -> Divine Favor faded (Client catches up and realizes it's down)
CalculateHealing: Calculates the healing value, does all the formula calculations talent modifiers and such
CalculateHotHealing: Used specifically for calculating the heals of hots
GetHealTargets: Who the heal is going to hit, used for setting extra targets for Beacon of Light + Paladin heal or Prayer of Healing.
The returns should either be:
"compressedGUID1,compressedGUID2,compressedGUID3,compressedGUID4", healthAmount
Or if you need to set specific healing values for one GUID it should be
"compressedGUID1,healthAmount1,compressedGUID2,healAmount2,compressedGUID3,healAmount3", -1
The latter is for cases like Glyph of Healing Wave where you need a heal for 1,000 on A and a heal for 200 on the player for B without sending 2 events.
The -1 tells the library to look in the GUId list for the heal amounts
**NOTE** Any GUID returned from GetHealTargets must be compressed through a call to compressGUID[guid]
]]
local CalculateHealing, GetHealTargets, AuraHandler, CalculateHotHealing, ResetChargeData, LoadClassData
-- DRUIDS
-- All data is accurate as of 3.2.2 (build 10392)
if( playerClass == "DRUID" ) then
LoadClassData = function()
-- Rejuvenation
local Rejuvenation = GetSpellInfo(774)
hotData[Rejuvenation] = {interval = 3,
levels = {4, 10, 16, 22, 28, 34, 40, 46, 52, 58, 60, 63, 69, 75, 80}, averages = {32, 56, 116, 180, 244, 304, 388, 488, 608, 756, 888, 932, 1060, 1192, 1690}}
-- Regrowth
local Regrowth = GetSpellInfo(8936)
hotData[Regrowth] = {interval = 3, ticks = 7, coeff = 1.316,
levels = {12, 18, 24, 30, 36, 42, 48, 54, 60, 65, 71, 77}, averages = {98, 175, 259, 343, 427, 546, 686, 861, 1064, 1274, 1792, 2345}}
-- Lifebloom
local Lifebloom = GetSpellInfo(33763)
hotData[Lifebloom] = {interval = 1, ticks = 7, coeff = 0.66626, dhCoeff = 0.34324 * 0.8, levels = {64, 72, 80}, averages = {224, 287, 371}, bomb = {480, 616, 776}}
-- Wild Growth
local WildGrowth = GetSpellInfo(48438)
hotData[WildGrowth] = {interval = 1, ticks = 7, coeff = 0.8056, levels = {60, 70, 75, 80}, averages = {686, 861, 1239, 1442}}
-- Regrowth
spellData[Regrowth] = {coeff = 0.2867,
levels = hotData[Regrowth].levels,
averages = {avg(84, 98), avg(164, 188), avg(240, 274), avg(318, 360), avg(405, 457), avg(511, 575), avg(646, 724), avg(809, 905), avg(1003, 1119), avg(1215, 1355), avg(1710, 1908), avg(2234, 2494)},
increase = {122, 155, 173, 180, 180, 178, 169, 156, 136, 115, 97, 23}}
-- Healing Touch
local HealingTouch = GetSpellInfo(5185)
spellData[HealingTouch] = {
levels = {1, 8, 14, 20, 26, 32, 38, 44, 50, 56, 60, 62, 69, 74, 79},
averages = {avg(37, 51), avg(88, 112), avg(195, 243), avg(363, 445), avg(490, 594), avg(636, 766), avg(802, 960), avg(1199, 1427), avg(1299, 1539), avg(1620, 1912), avg(1944, 2294), avg(2026, 2392), avg(2321, 2739), avg(3223, 3805), avg(3750, 4428)}}
-- Nourish
local Nourish = GetSpellInfo(50464)
spellData[Nourish] = {coeff = 0.358005, levels = {80}, averages = {avg(1883, 2187)}}
-- Tranquility
local Tranquility = GetSpellInfo(740)
spellData[Tranquility] = {coeff = 1.144681, ticks = 4, levels = {30, 40, 50, 60, 70, 75, 80}, averages = {351, 515, 765, 1097, 1518, 2598, 3035}}
-- Talent data, these are filled in later and modified on talent changes
-- Master Shapeshifter (Multi)
local MasterShapeshifter = GetSpellInfo(48411)
talentData[MasterShapeshifter] = {mod = 0.02, current = 0}
-- Gift of Nature (Add)
local GiftofNature = GetSpellInfo(17104)
talentData[GiftofNature] = {mod = 0.02, current = 0}
-- Empowered Touch (Add, increases spell power HT/Nourish gains)
local EmpoweredTouch = GetSpellInfo(33879)
talentData[EmpoweredTouch] = {mod = 0.2, current = 0}
-- Empowered Rejuvenation (Multi, this ups both the direct heal and the hot)
local EmpoweredRejuv = GetSpellInfo(33886)
talentData[EmpoweredRejuv] = {mod = 0.04, current = 0}
-- Genesis (Add)
local Genesis = GetSpellInfo(57810)
talentData[Genesis] = {mod = 0.01, current = 0}
-- Improved Rejuvenation (Add)
local ImprovedRejuv = GetSpellInfo(17111)
talentData[ImprovedRejuv] = {mod = 0.05, current = 0}
-- Nature's Splendor (+3s Rejuv/+6s Regrowth/+2s Lifebloom)
local NaturesSplendor = GetSpellInfo(57865)
talentData[NaturesSplendor] = {mod = 1, current = 0}
local TreeofLife = GetSpellInfo(33891)
local Innervate = GetSpellInfo(29166)
-- Set data
-- 2 piece, +6 seconds to Regrowth
itemSetsData["T5 Resto"] = {30216, 30217, 30219, 30220, 30221}
-- +5% more healing to Nourish per hot
itemSetsData["T7 Resto"] = {40460, 40461, 40462, 40463, 40465, 39531, 39538, 39539, 39542, 39543}
--itemSetsData["T8 Resto"] = {46183, 46184, 46185, 46186, 46187, 45345, 45346, 45347, 45348, 45349}
--itemSetsData["T9 Resto"] = {48102, 48129, 48130, 48131, 48132, 48153, 48154, 48155, 48156, 48157, 48133, 48134, 48135, 48136, 48137, 48142, 48141, 48140, 48139, 48138, 48152, 48151, 48150, 48149, 48148, 48143, 48144, 48145, 48146, 48147}
-- 2 piece, 30% less healing lost on WG
itemSetsData["T10 Resto"] = {50106, 50107, 50108, 50109, 50113, 51139, 51138, 51137, 51136, 51135, 51300, 51301, 51302, 51303, 51304}
local bloomBombIdols = {[28355] = 87, [33076] = 105, [33841] = 116, [35021] = 131, [42576] = 188, [42577] = 217, [42578] = 246, [42579] = 294, [42580] = 376, [51423] = 448}
local hotTotals, hasRegrowth = {}, {}
AuraHandler = function(unit, guid)
hotTotals[guid] = 0
if( unitHasAura(unit, Rejuvenation) ) then hotTotals[guid] = hotTotals[guid] + 1 end
if( unitHasAura(unit, Lifebloom) ) then hotTotals[guid] = hotTotals[guid] + 1 end
if( unitHasAura(unit, WildGrowth) ) then hotTotals[guid] = hotTotals[guid] + 1 end
if( unitHasAura(unit, Regrowth) ) then
hasRegrowth[guid] = true
hotTotals[guid] = hotTotals[guid] + 1
else
hasRegrowth[guid] = nil
end
end
GetHealTargets = function(bitType, guid, healAmount, spellName, hasVariableTicks)
-- Tranquility pulses on everyone within 30 yards, if they are in range of Innervate they'll get Tranquility
if( spellName == Tranquility ) then
local targets = compressGUID[playerGUID]
local playerGroup = guidToGroup[playerGUID]
for groupGUID, id in pairs(guidToGroup) do
if( id == playerGroup and playerGUID ~= groupGUID and not UnitHasVehicleUI(guidToUnit[groupID]) and IsSpellInRange(Innervate, guidToUnit[groupGUID]) == 1 ) then
targets = targets .. "," .. compressGUID[groupGUID]
end
end
return targets, healAmount
elseif( hasVariableTicks ) then
healAmount = table.concat(healAmount, "@")
end
return compressGUID[guid], healAmount
end
-- Calculate hot heals
local wgTicks = {}
CalculateHotHealing = function(guid, spellID)
local spellName, spellRank = GetSpellInfo(spellID)
local rank = HealComm.rankNumbers[spellRank]
local healAmount = hotData[spellName].averages[rank]
local spellPower = GetSpellBonusHealing()
local healModifier, spModifier = playerHealModifier, 1
local bombAmount, totalTicks
healModifier = healModifier + talentData[GiftofNature].current
healModifier = healModifier + talentData[Genesis].current
-- Master Shapeshifter does not apply directly when using Lifebloom
if( unitHasAura("player", TreeofLife) ) then
healModifier = healModifier * (1 + talentData[MasterShapeshifter].current)
-- 32387 - Idol of the Raven Godess, +44 SP while in TOL
if( playerCurrentRelic == 32387 ) then
spellPower = spellPower + 44
end
end
-- Rejuvenation
if( spellName == Rejuvenation ) then
healModifier = healModifier + talentData[ImprovedRejuv].current
-- 25643 - Harold's Rejuvenation Broach, +86 Rejuv SP
if( playerCurrentRelic == 25643 ) then
spellPower = spellPower + 86
-- 22398 - Idol of Rejuvenation, +50 SP to Rejuv
elseif( playerCurrentRelic == 22398 ) then
spellPower = spellPower + 50
end
local duration, ticks
if( IS_BUILD30300 ) then
duration = 15
ticks = 5
totalTicks = 5
else
duration = rank > 14 and 15 or 12
ticks = duration / hotData[spellName].interval
totalTicks = ticks
end
spellPower = spellPower * (((duration / 15) * 1.88) * (1 + talentData[EmpoweredRejuv].current))
spellPower = spellPower / ticks
healAmount = healAmount / ticks
--38366 - Idol of Pure Thoughts, +33 SP base per tick
if( playerCurrentRelic == 38366 ) then
spellPower = spellPower + 33
end
-- Nature's Splendor, +6 seconds
if( talentData[NaturesSplendor].mod >= 1 ) then totalTicks = totalTicks + 1 end
-- Regrowth
elseif( spellName == Regrowth ) then
spellPower = spellPower * (hotData[spellName].coeff * (1 + talentData[EmpoweredRejuv].current))
spellPower = spellPower / hotData[spellName].ticks
healAmount = healAmount / hotData[spellName].ticks
totalTicks = 7
-- Nature's Splendor, +6 seconds
if( talentData[NaturesSplendor].mod >= 1 ) then totalTicks = totalTicks + 2 end
-- T5 Resto, +6 seconds
if( equippedSetCache["T5 Resto"] >= 2 ) then totalTicks = totalTicks + 2 end
-- Lifebloom
elseif( spellName == Lifebloom ) then
-- Figure out the bomb heal, apparently Gift of Nature double dips and will heal 10% for the HOT + 10% again for the direct heal
local bombSpellPower = spellPower
if( playerCurrentRelic and bloomBombIdols[playerCurrentRelic] ) then
bombSpellPower = bombSpellPower + bloomBombIdols[playerCurrentRelic]
end
local bombSpell = bombSpellPower * (hotData[spellName].dhCoeff * 1.88)
bombAmount = math.ceil(calculateGeneralAmount(hotData[spellName].levels[rank], hotData[spellName].bomb[rank], bombSpell, spModifier, healModifier + talentData[GiftofNature].current))
-- Figure out the hot tick healing
spellPower = spellPower * (hotData[spellName].coeff * (1 + talentData[EmpoweredRejuv].current))
spellPower = spellPower / hotData[spellName].ticks
healAmount = healAmount / hotData[spellName].ticks
-- Figure out total ticks
totalTicks = 7
-- Idol of Lush Moss, +125 SP per tick
if( playerCurrentRelic == 40711 ) then
spellPower = spellPower + 125
-- Idol of the Emerald Queen, +47 SP per tick
elseif( playerCurrentRelic == 27886 ) then
spellPower = spellPower + 47
end
-- Glyph of Lifebloom, +1 second
if( glyphCache[54826] ) then totalTicks = totalTicks + 1 end
-- Nature's Splendor, +2 seconds
if( talentData[NaturesSplendor].mod >= 1 ) then totalTicks = totalTicks + 1 end
-- Wild Growth
elseif( spellName == WildGrowth ) then
spellPower = spellPower * (hotData[spellName].coeff * (1 + talentData[EmpoweredRejuv].current))
spellPower = spellPower / hotData[spellName].ticks
spellPower = calculateSpellPower(hotData[spellName].levels[rank], spellPower)
healAmount = healAmount / hotData[spellName].ticks
healModifier = healModifier * HealComm.zoneHealModifier
table.wipe(wgTicks)
local tickModifier = equippedSetCache["T10 Resto"] >= 2 and 0.70 or 1
local tickAmount = healAmount / hotData[spellName].ticks
for i=1, hotData[spellName].ticks do
table.insert(wgTicks, math.ceil(healModifier * ((healAmount + tickAmount * (3 - (i - 1) * tickModifier)) + (spellPower * spModifier))))
end
return HOT_HEALS, wgTicks, hotData[spellName].ticks, hotData[spellName].interval, nil, true
end
healAmount = calculateGeneralAmount(hotData[spellName].levels[rank], healAmount, spellPower, spModifier, healModifier)
return HOT_HEALS, math.ceil(healAmount), totalTicks, hotData[spellName].interval, bombAmount
end
-- Calcualte direct and channeled heals
CalculateHealing = function(guid, spellName, spellRank)
local healAmount = averageHeal[spellName][spellRank]
local spellPower = GetSpellBonusHealing()
local healModifier, spModifier = playerHealModifier, 1
local rank = HealComm.rankNumbers[spellRank]
-- Gift of Nature
healModifier = healModifier + talentData[GiftofNature].current
-- Master Shapeshifter does not apply directly when using Lifebloom
if( unitHasAura("player", TreeofLife) ) then
healModifier = healModifier * (1 + talentData[MasterShapeshifter].current)
-- 32387 - Idol of the Raven Godess, +44 SP while in TOL
if( playerCurrentRelic == 32387 ) then
spellPower = spellPower + 44
end
end
-- Regrowth
if( spellName == Regrowth ) then
-- Glyph of Regrowth - +20% if target has Regrowth
if( glyphCache[54743] and hasRegrowth[guid] ) then
healModifier = healModifier * 1.20
end
spellPower = spellPower * ((spellData[spellName].coeff * 1.88) * (1 + talentData[EmpoweredRejuv].current))
-- Nourish
elseif( spellName == Nourish ) then
-- 46138 - Idol of Flourishing Life, +187 Nourish SP
if( playerCurrentRelic == 46138 ) then
spellPower = spellPower + 187
end
-- Apply any hot specific bonuses
local hots = hotTotals[guid]
if( hots and hots > 0 ) then
local bonus = 1.20
-- T7 Resto, +5% healing per each of the players hot on their target
if( equippedSetCache["T7 Resto"] >= 2 ) then
bonus = bonus + 0.05 * hots
end
-- Glyph of Nourish - 6% per HoT
if( glyphCache[62971] ) then
bonus = bonus + 0.06 * hots
end
healModifier = healModifier * bonus
end
spellPower = spellPower * ((spellData[spellName].coeff * 1.88) + talentData[EmpoweredTouch].spent * 0.10)
-- Healing Touch
elseif( spellName == HealingTouch ) then
-- Glyph of Healing Touch, -50% healing
if( glyphCache[54825] ) then
healModifier = healModifier - 0.50
end
-- Idol of the Avian Heart, +136 baseh ealing
if( playerCurrentRelic == 28568 ) then
healAmount = healAmount + 136
-- Idol of Health, +100 base healing
elseif( playerCurrentRelic == 22399 ) then
healAmount = healAmount + 100
end
-- Rank 1 - 3: 1.5/2/2.5 cast time, Rank 4+: 3 cast time
local castTime = rank > 3 and 3 or rank == 3 and 2.5 or rank == 2 and 2 or 1.5
spellPower = spellPower * (((castTime / 3.5) * 1.88) + talentData[EmpoweredTouch].current)
-- Tranquility
elseif( spellName == Tranquility ) then
healModifier = healModifier + talentData[Genesis].current
spellPower = spellPower * ((spellData[spellName].coeff * 1.88) * (1 + talentData[EmpoweredRejuv].current))
spellPower = spellPower / spellData[spellName].ticks
end
healAmount = calculateGeneralAmount(spellData[spellName].levels[rank], healAmount, spellPower, spModifier, healModifier)
-- 100% chance to crit with Nature, this mostly just covers fights like Loatheb where you will basically have 100% crit
if( GetSpellCritChance(4) >= 100 ) then
healAmount = healAmount * 1.50
end
if( spellData[spellName].ticks ) then
return CHANNEL_HEALS, math.ceil(healAmount), spellData[spellName].ticks, spellData[spellName].ticks
end
return DIRECT_HEALS, math.ceil(healAmount)
end
end
end
-- PALADINS
-- All data is accurate as of 3.2.2 (build 10392)
if( playerClass == "PALADIN" ) then
LoadClassData = function()
-- Hot data, this is just so it realizes that FoL can be a hot so it will call the calculator
--local FlashofLight = GetSpellInfo(19750)
--hotData[FlashofLight] = true
-- Spell data
-- Holy Light
local HolyLight = GetSpellInfo(635)
spellData[HolyLight] = {coeff = 2.5 / 3.5 * 1.25,
levels = {1, 6, 14, 22, 30, 38, 46, 54, 60, 62, 70, 75, 80},
averages = {avg(50, 60), avg(96, 116), avg(203, 239), avg(397, 455), avg(628, 708), avg(894, 998), avg(1209, 1349), avg(1595, 1777), avg(2034, 2266), avg(2232, 2486), avg(2818, 3138), avg(4199, 4677), avg(4888, 5444)},
increase = {63, 81, 112, 139, 155, 159, 156, 135, 116, 115, 70, 52, 0}}
-- Flash of Light
local FlashofLight = GetSpellInfo(19750)
spellData[FlashofLight] = {coeff = 1.5 / 3.5 * 1.25,
levels = {20, 26, 34, 42, 50, 58, 66, 74, 79},
averages = {avg(81, 93), avg(124, 144), avg(189, 211), avg(256, 288), avg(346, 390), avg(445, 499), avg(588, 658), avg(682, 764), avg(785, 879)},
increase = {60, 70, 73, 72, 66, 57, 42, 20, 3}}
-- Talent data
-- Need to figure out a way of supporting +6% healing from imp devo aura, might not be able to
-- Healing Light (Add)
local HealingLight = GetSpellInfo(20237)
talentData[HealingLight] = {mod = 0.04, current = 0}
-- Divinity (Add)
local Divinity = GetSpellInfo(63646)
talentData[Divinity] = {mod = 0.01, current = 0}
-- Touched by the Light (Add?)
local TouchedbytheLight = GetSpellInfo(53592)
talentData[TouchedbytheLight] = {mod = 0.10, current = 0}
-- 100% of your heal on someone within range of your beacon heals the beacon target too
local BeaconofLight = GetSpellInfo(53563)
-- 100% chance to crit
local DivineFavor = GetSpellInfo(20216)
-- Seal of Light + Glyph = 5% healing
local SealofLight = GetSpellInfo(20165)
-- Divine Illumination, used in T10 holy
local DivineIllumination = GetSpellInfo(31842)
local flashLibrams = {[42616] = 436, [42615] = 375, [42614] = 331, [42613] = 293, [42612] = 204, [28592] = 89, [25644] = 79, [23006] = 43, [23201] = 28}
local holyLibrams = {[45436] = 160, [40268] = 141, [28296] = 47}
local flashSPLibrams = {[51472] = 510}
-- Holy Shock crits put a hot that heals for 15% of the HS over 9s
--itemSetsData["T8 Holy"] = { 45370, 45371, 45372, 45373, 45374, 46178, 46179, 46180, 46181, 46182 }
-- +100% to the hot when using Flash of Light + Sacred Shield
--itemSetsData["T9 Holy"] = { 48595, 48596, 48597, 48598, 48599, 48564, 48566, 48568, 48572, 48574, 48593, 48591, 48592, 48590, 48594, 48588, 48586, 48587, 48585, 48589, 48576, 48578, 48577, 48579, 48575, 48583, 48581, 48582, 48580, 48584}
itemSetsData["T10 Holy"] = {50865, 50866, 50867, 50868, 50869, 51270, 51271, 51272, 51273, 51274, 51165, 51166, 51167, 51168, 51169}
-- Need the GUID of whoever has beacon on them so we can make sure they are visible to us and so we can check the mapping
local activeBeaconGUID, hasDivineFavor
AuraHandler = function(unit, guid)
if( unitHasAura(unit, BeaconofLight) ) then
activeBeaconGUID = guid
elseif( activeBeaconGUID == guid ) then
activeBeaconGUID = nil
end
-- Check Divine Favor
if( unit == "player" ) then
hasDivineFavor = unitHasAura("player", DivineFavor)
end
end
ResetChargeData = function(guid)
hasDivineFavor = unitHasAura("player", DivineFavor)
end
-- Check for beacon when figuring out who to heal
GetHealTargets = function(bitType, guid, healAmount, spellName, hasVariableTicks)
if( activeBeaconGUID and activeBeaconGUID ~= guid and guidToUnit[activeBeaconGUID] and UnitIsVisible(guidToUnit[activeBeaconGUID]) ) then
return string.format("%s,%s", compressGUID[guid], compressGUID[activeBeaconGUID]), healAmount
elseif( hasVariableTicks ) then
healAmount = table.concat(healAmount, "@")
end
return compressGUID[guid], healAmount
end