Repository navigation
Expand file tree
/
Copy pathSupportMeter.lua
More file actions
2846 lines (2675 loc) · 109 KB
/
Copy pathSupportMeter.lua
File metadata and controls
2846 lines (2675 loc) · 109 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
--[[---------------------------------------------------------------------------
SupportMeter - retail-Augmentation-style support attribution for the
Wind Stormbringer's Aerodynamics on Ascension (3.3.5).
Model: while YOUR Aerodynamics buff is on a raid member, every damage
event they produce is partially yours:
attributed = damage x share x buffValue / (pool + buffValue)
The event's DAMAGE SCHOOL picks the pool: physical -> their attack
power, magic -> their spell power. Pools come from /sm inspect (gear
scan of nearby raiders, same method the logs website uses), manual
overrides, or defaults.
/sm - toggle window
/sm inspect - gear-scan party/raid members for their SP/AP pools
/sm value N - set your Aerodynamics value (auto-read from spellbook)
/sm share X - power-scaling share for spells (default 0.45)
/sm mshare X - power-scaling share for melee/physical (default 0.55)
/sm pool Name sp|ap N - manual pool override
/sm reset - reset session totals
-----------------------------------------------------------------------------]]
local TAG = "|cff00ccffSupportMeter|r: "
---------------------------------------------------------------------------
-- 3.3.5 widget backfills
---------------------------------------------------------------------------
local function ExtendWidget(widget)
local mt = getmetatable(widget)
local idx = mt and mt.__index
if type(idx) ~= "table" then return end
if not idx.SetShown then
idx.SetShown = function(self, shown)
if shown then self:Show() else self:Hide() end
end
end
if not idx.SetSize and idx.SetWidth then
idx.SetSize = function(self, w, h)
self:SetWidth(w)
self:SetHeight(h)
end
end
if idx.SetTexture and not idx.SetColorTexture then
idx.SetColorTexture = function(self, r, g, b, a)
self:SetTexture(r, g, b, a)
end
end
end
do
local holder = CreateFrame("Frame")
ExtendWidget(holder)
ExtendWidget(CreateFrame("Button"))
ExtendWidget(holder:CreateTexture())
ExtendWidget(holder:CreateFontString())
end
local db
---------------------------------------------------------------------------
-- state
---------------------------------------------------------------------------
local band = bit.band
local AFFIL_MINE = COMBATLOG_OBJECT_AFFILIATION_MINE or 0x1
local buffed = {} -- [destGUID] = destName, while MY buff is on them
local buffedAt = {} -- [destGUID] = time of last apply/refresh (expiry watchdog)
local lastValueRead = 0
local lastReadSpellId
local dmgMix = {} -- [name] = {phys, magic} observed damage, infers highest stat
local detailsData = {} -- [detailsCombatNumber] = { [name] = attributed } (SV-backed)
local segTotals = {} -- [detailsCombatNumber] = summed attributed (SV-backed)
local myRows = {} -- [detailsCombatNumber] = OUR buff spell row (never in shared data)
local injApplied = {} -- [combatNumber] = { me = amount, byName = {name=amount} }
local maxCombatId = 0
local procActive = {} -- [name] = { [procBuffName] = true } (CLEU-tracked)
local totals = {} -- [name] = attributed damage (session)
local fightTotals = {} -- [name] = attributed damage (current/last fight)
local sessionAttr = 0
local fightAttr = 0
local inCombat = false
local fightStart = 0
local fightDur = 0
local playerGUID, playerName
local gui
local RefreshGUI
local RefreshDebug -- assigned in the debug-window section
local dbgWin, CreateDebugWindow
local audit -- current-fight audit data
local auditLog = {} -- newest-first report strings of finished fights
local spellsSeen = {} -- [spellId or 0] = {n, d, t} session ledger of observed spells
local function Round(n) return math.floor(n + 0.5) end
local function Fmt(n)
if n >= 1000000 then return ("%.2fM"):format(n / 1000000) end
if n >= 1000 then return ("%.1fK"):format(n / 1000) end
return tostring(Round(n))
end
---------------------------------------------------------------------------
-- pools
---------------------------------------------------------------------------
local levels = {} -- [name] = unit level (group roster cache)
local function GetPool(name, isPhysical)
local p = db.pools[name]
-- exact stats (their own SupportMeter told us) are full totals: as-is
if p and p.exact then
local v = isPhysical and p.ap or p.sp
if v and v > 0 then return v end
end
-- gear scans are FLOORS (no base stats): add the base-stat bonus and
-- never drop below the fallback - raw scan values over-credit badly.
-- base/fallback assume level 60; scale them for lower-level players
local f = 1
local lvl = levels[name]
if lvl and lvl > 0 and lvl < 60 then f = lvl / 60 end
local scanned = p and (isPhysical and p.ap or p.sp) or 0
local pool = scanned + (isPhysical and db.baseAP or db.baseSP) * f
local floorPool = (isPhysical and db.defaults.ap or db.defaults.sp) * f
if pool < floorPool then pool = floorPool end
local procs = procActive[name]
if procs then
local key = isPhysical and "ap" or "sp"
for buff in pairs(procs) do
local def = db.procs and db.procs[buff]
if def and def[key] then pool = pool + def[key] end
end
end
return pool
end
local function UpdateRosterLevels()
wipe(levels)
local n = GetNumRaidMembers()
if n > 0 then
for i = 1, n do
local u = "raid" .. i
local nm = UnitName(u)
if nm then levels[nm] = UnitLevel(u) end
end
else
for i = 1, GetNumPartyMembers() do
local u = "party" .. i
local nm = UnitName(u)
if nm then levels[nm] = UnitLevel(u) end
end
end
end
---------------------------------------------------------------------------
-- pet -> owner map: the buff lands on the PLAYER and a fraction of the
-- buffed stat flows to their pet (inheritance), so pet damage is credited
-- through the owner's buff window at a reduced effective value
---------------------------------------------------------------------------
local petOwner = {} -- [petGUID] = { g = ownerGUID, n = ownerName }
local function UpdatePetMap()
wipe(petOwner)
local function add(petUnit, ownerUnit)
if UnitExists(petUnit) then
local pg = UnitGUID(petUnit)
local og = UnitGUID(ownerUnit)
local on = UnitName(ownerUnit)
if pg and og and on then petOwner[pg] = { g = og, n = on } end
end
end
local n = GetNumRaidMembers()
if n > 0 then
for i = 1, n do add("raidpet" .. i, "raid" .. i) end
else
for i = 1, GetNumPartyMembers() do add("partypet" .. i, "party" .. i) end
end
end
---------------------------------------------------------------------------
-- my Aerodynamics value: read from my own spellbook tooltip
---------------------------------------------------------------------------
local scanTip = CreateFrame("GameTooltip", "SupportMeterScanTip", nil, "GameTooltipTemplate")
scanTip:SetOwner(UIParent, "ANCHOR_NONE")
local function AutoReadBuffValue()
-- the 3.3.5 spellbook lists EVERY rank as its own entry - scan them
-- all and keep the highest value (= the max rank)
local getName = GetSpellName or GetSpellBookItemName
local best
local i = 1
while true do
local name = getName(i, "spell")
if not name then break end
if name == db.buffName then
scanTip:ClearLines()
scanTip:SetOwner(UIParent, "ANCHOR_NONE")
scanTip:SetSpell(i, "spell")
for l = 1, 8 do
local line = _G["SupportMeterScanTipTextLeft" .. l]
local t = line and line:GetText()
local v = t and t:match("power by (%d+)")
if v then
v = tonumber(v)
if not best or v > best then best = v end
break
end
end
end
i = i + 1
if i > 1024 then break end
end
return best
end
-- support buffs known to share the exclusive raid-power stack group;
-- used to auto-detect which one THIS character provides
local KNOWN_BUFFS = {
"Aerodynamics", "Battle Screech", "Flourishing Growth",
"Song of Steel", "Splash Zone", "Heartbreak",
}
local KNOWN_SET = {}
for _, n in ipairs(KNOWN_BUFFS) do KNOWN_SET[n] = true end
local function FindSupportBuffInSpellbook()
local getName = GetSpellName or GetSpellBookItemName
local i = 1
while true do
local name = getName(i, "spell")
if not name then break end
for _, buff in ipairs(KNOWN_BUFFS) do
if name == buff then return buff end
end
i = i + 1
if i > 1024 then break end
end
end
-- read the buff's actual granted value off a LIVE aura on a group member -
-- handles buffs that scale with the caster's stats (Battle Screech/Agi,
-- Fleshcrafter/SP): the aura tooltip carries the applied number
local function ReadValueFromUnitAura(unit)
for i = 1, 40 do
local name = UnitBuff(unit, i)
if not name then break end
if name == db.buffName then
scanTip:ClearLines()
scanTip:SetOwner(UIParent, "ANCHOR_NONE")
scanTip:SetUnitBuff(unit, i)
for l = 1, 8 do
local line = _G["SupportMeterScanTipTextLeft" .. l]
local t = line and line:GetText()
local v = t and t:match("power by (%d+)")
if v then return tonumber(v) end
end
break
end
end
end
local function UnitTokenForName(name)
if name == playerName then return "player" end
local n = GetNumRaidMembers()
if n > 0 then
for i = 1, n do
if UnitName("raid" .. i) == name then return "raid" .. i end
end
else
for i = 1, GetNumPartyMembers() do
if UnitName("party" .. i) == name then return "party" .. i end
end
end
end
---------------------------------------------------------------------------
-- attribution
---------------------------------------------------------------------------
local COEF_DB = _G.SUPPORTMETER_COEFS or {}
local COEF_NAMES = _G.SUPPORTMETER_COEF_NAMES or {}
local function Attribute(srcGUID, srcName, amount, school, spellName, spellId, isRanged, petO)
if not amount or amount <= 0 then return end
-- pet damage rides its OWNER's buff window (inherited stats)
local guid = petO and petO.g or srcGUID
local benef = buffed[guid]
if not benef then return end
-- expiry watchdog: if we never saw the REMOVED (out of log range),
-- the window still dies when the buff duration has passed
local t0 = buffedAt[guid]
if db.buffDur > 0 and (not t0 or GetTime() - t0 > db.buffDur) then
if audit and audit.win[guid] and t0 then
local nm = buffed[guid]
if nm then
audit.up[nm] = (audit.up[nm] or 0)
+ math.max(0, (t0 + db.buffDur) - audit.win[guid])
end
audit.win[guid] = nil
end
buffed[guid] = nil
buffedAt[guid] = nil
return
end
local key = petO and petO.n or srcName or benef
-- record their damage mix (fallback for inferring their highest stat);
-- pet hits stay OUT of the mix - a melee pet would misclassify a caster
local isPhysical = (school == 1)
local m = dmgMix[key]
if not m then m = { phys = 0, magic = 0 } dmgMix[key] = m end
if not petO then
if isPhysical then m.phys = m.phys + amount else m.magic = m.magic + amount end
end
-- role: exact stats decide when available; otherwise the OBSERVED
-- damage mix - partial gear scans misclassify hybrids/casters badly
local role
local p = db.pools[key]
if p and p.exact then
role = ((p.sp or 0) >= (p.ap or 0)) and "sp" or "ap"
else
role = (m.magic >= m.phys) and "sp" or "ap"
end
-- fight audit: record everything we saw from this unit
local a
if audit then
a = audit.benef[key]
if not a then
a = { phys = 0, magic = 0, attr = 0, skip = 0, petd = 0 }
audit.benef[key] = a
end
a.role = role
a.pool = GetPool(key, role == "ap")
if petO then
a.petd = (a.petd or 0) + amount
elseif isPhysical then
a.phys = a.phys + amount
else
a.magic = a.magic + amount
end
end
-- the buff feeds the ROLE stat; every path below credits against it
local roleAP = (role == "ap")
local V = db.buffValue
local pool = GetPool(key, roleAP)
if petO then
-- pet inherits ratio x owner stat, so the buff's slice inside the
-- pet's pool is ratio x V against a ratio-scaled pool (the ratio
-- cancels in the share path; coef path approximates the pet pool)
local r = db.petRatio or 0.5
V = V * r
pool = pool * r
end
-- best available model per ability: user override > generated DB by
-- spell ID (exact rank) > measured share > class-average guess
local attr
local tier = 3
local coef = (spellName and db.abilityCoef[spellName])
or (spellId and COEF_DB[spellId])
or (spellName and COEF_NAMES[spellName])
-- verified-flat spells earn a hard ZERO (under-estimate policy)
if coef and coef.z and not coef.c and not coef.ca and not coef.w then
if a then a.skip = a.skip + amount end
if audit then audit.model.zero = audit.model.zero + amount end
do
local key2 = spellId or 0
local s = spellsSeen[key2]
if not s then
s = { n = spellName or "?", d = 0, t = 4 }
spellsSeen[key2] = s
end
s.d = s.d + amount
end
return
end
-- coefficient choice: when the damage school MATCHES the buffed stat,
-- either coefficient proves scaling (db wording varies); when it does
-- NOT match, only an explicit coefficient on the buffed stat counts -
-- hybrids like Tinker fire abilities scale with AP despite the school,
-- and they must NOT be school-gated away
local cEff
if coef then
if isPhysical == roleAP then
cEff = roleAP and (coef.ca or coef.c) or (coef.c or coef.ca)
elseif roleAP then
cEff = coef.ca
else
cEff = coef.c
end
end
if cEff and cEff > 0 then
tier = 1
attr = amount * (cEff * V) / ((coef.b or 0) + cEff * (pool + V))
else
-- non-coefficient paths can't prove cross-school scaling: damage
-- whose school doesn't match the buffed stat earns no credit
-- (pets exempt - inherited stats feed everything they do)
if not petO and roleAP ~= isPhysical then
if a then a.skip = a.skip + amount end
return
end
-- weapon-damage hits (auto attacks + "X% weapon damage" abilities):
-- AP feeds them as AP/14 per weapon-speed second, so with the
-- target's weapon DPS known the buff slice is exact (speed and
-- the % multiplier cancel out)
local wd
-- (owner's weapon DPS says nothing about the pet's weapon: pets
-- always fall through to the share path)
if isPhysical and not petO then
local pp = db.pools[key]
if pp then wd = isRanged and pp.rdps or pp.wdps end
end
if wd and wd > 0 and (not spellName or isRanged or (coef and coef.w)) then
tier = 2
attr = amount * (V / 14) / (wd + (pool + V) / 14)
else
local share = (spellName and db.abilityShare[spellName])
or (isPhysical and db.shareMelee or db.share)
attr = amount * share * V / (pool + V)
-- log spells the coefficient DB doesn't know for future scrapes
if spellId and spellName and not (coef and coef.w) and not db.newSpells[spellId] then
local n = 0
for _ in pairs(db.newSpells) do n = n + 1 end
if n < 500 then db.newSpells[spellId] = spellName end
end
end
end
if a then a.attr = a.attr + attr end
do
local key2 = spellId or 0
local s = spellsSeen[key2]
if not s then
s = { n = spellName or (isRanged and "Auto Shot" or "Auto Attack"), d = 0, t = tier }
spellsSeen[key2] = s
end
s.d = s.d + amount
if tier < s.t then s.t = tier end
end
if audit then
local m = audit.model
if petO then m.pet = (m.pet or 0) + attr end
if tier == 1 then
m.coef = m.coef + attr
elseif tier == 2 then
m.weap = m.weap + attr
else
m.share = m.share + attr
if spellId and spellName then
local u = audit.unk[spellId]
if not u then
u = { n = spellName, d = 0 }
audit.unk[spellId] = u
end
u.d = u.d + amount
end
end
end
totals[key] = (totals[key] or 0) + attr
sessionAttr = sessionAttr + attr
if inCombat then
fightTotals[key] = (fightTotals[key] or 0) + attr
fightAttr = fightAttr + attr
end
-- mirror into the Details segment bucket for the custom display
local D = _G.Details
if D then
local c = D:GetCurrentCombat()
if c and c.GetCombatNumber then
local id = c:GetCombatNumber()
if id then
if id > maxCombatId then maxCombatId = id end
local t = detailsData[id]
if not t then t = {} detailsData[id] = t end
t[key] = (t[key] or 0) + attr
segTotals[id] = (segTotals[id] or 0) + attr
-- mode "extra": Aug-style extension segment on MY bar in
-- the normal Damage Done display (Details' own refresh
-- renders total_extra as the translucent extra statusbar).
-- mode "merge" is handled by the 1s reconciler instead.
if db.mode == "extra" then
local me = c:GetActor(DETAILS_ATTRIBUTE_DAMAGE, playerName)
if me then me.total_extra = math.floor(segTotals[id]) end
end
end
end
end
end
---------------------------------------------------------------------------
-- per-fight audit (debug): what the addon saw and why it credited what
---------------------------------------------------------------------------
local function StartAudit()
audit = { t0 = GetTime(), win = {}, up = {}, benef = {},
model = { coef = 0, weap = 0, share = 0, zero = 0, pet = 0 }, unk = {} }
for guid in pairs(buffed) do
audit.win[guid] = audit.t0
end
end
local function FinishAudit()
if not audit then return end
local now = GetTime()
for guid, t in pairs(audit.win) do
local name = buffed[guid]
if name then
audit.up[name] = (audit.up[name] or 0) + (now - t)
end
end
local dur = math.max(1, now - audit.t0)
local L = {}
L[#L + 1] = ("== Fight %s | %ds | %s +%d | shares %.2f/%.2f | mode %s"):format(
date("%H:%M:%S"), dur, db.buffName, db.buffValue, db.share, db.shareMelee, db.mode or "?")
local any = false
for name, a in pairs(audit.benef) do
any = true
local up = audit.up[name] or 0
L[#L + 1] = ("%s [%s pool %d%s] buffed %.0fs (%d%%)"):format(
name, a.role == "ap" and "AP" or "SP", a.pool or 0,
(db.pools[name] and db.pools[name].exact) and " EXACT" or "",
up, up / dur * 100)
L[#L + 1] = (" phys seen %s | magic seen %s | pet seen %s | credited %s | wrong-school skipped %s"):format(
Fmt(a.phys), Fmt(a.magic), Fmt(a.petd or 0), Fmt(a.attr), Fmt(a.skip))
end
if not any then
local n = 0
for _ in pairs(buffed) do n = n + 1 end
L[#L + 1] = (" nothing credited: %d unit(s) buffed at fight end; no damage from buffed units seen"):format(n)
end
local m = audit.model
L[#L + 1] = (" model coverage: exact-coef %s | weapon-math %s | share-guess %s | flat-zeroed %s dmg | via pets %s (ratio %.2f)"):format(
Fmt(m.coef), Fmt(m.weap), Fmt(m.share), Fmt(m.zero or 0), Fmt(m.pet or 0), db.petRatio or 0.5)
local ul = {}
for id, u in pairs(audit.unk) do ul[#ul + 1] = { id, u.n, u.d } end
if #ul > 0 then
table.sort(ul, function(x, y) return x[3] > y[3] end)
local parts = {}
for i = 1, math.min(#ul, 8) do
parts[#parts + 1] = ("%s (%d) %s"):format(ul[i][2], ul[i][1], Fmt(ul[i][3]))
end
L[#L + 1] = " MISSING from coef DB: " .. table.concat(parts, ", ")
.. (#ul > 8 and (" +" .. (#ul - 8) .. " more") or "")
end
L[#L + 1] = (" fight attributed total: %s"):format(Fmt(fightAttr))
table.insert(auditLog, 1, table.concat(L, "\n"))
if #auditLog > 8 then table.remove(auditLog) end
audit = nil
if RefreshDebug then RefreshDebug() end
end
-- the buff row NEVER lives in shared Details data (so hover tooltips,
-- reports and every other surface on Normal windows stay clean). We keep
-- our own rows and slot one into my actor's spell table ONLY while the
-- breakdown panel is open from a Redistributed window.
local function NewRow()
return { id = db.buffSpellId, total = 0, counter = 0, successful_casted = 0,
n_min = 0, n_max = 0, n_amt = 0, n_total = 0,
c_min = 0, c_max = 0, c_amt = 0, c_total = 0,
g_amt = 0, g_dmg = 0, r_amt = 0, r_dmg = 0,
b_amt = 0, b_dmg = 0, a_amt = 0, a_dmg = 0,
targets = {}, extra = {} }
end
local function EnsureRow(id)
local row = myRows[id]
if not row then
row = NewRow()
myRows[id] = row
end
return row
end
-- overall aggregate row: ONLY counts attribution through players that
-- actually exist in this overall combat - Details resets Overall on new
-- groups while our history spans older fights (raids etc), and the bar's
-- redistribution filters by actor presence, so the row must match it
local function BuildOverallRow(combat)
local row = NewRow()
if not (combat and combat.GetActor) then return row end
for id, map in pairs(detailsData) do
for n, v in pairs(map) do
if combat:GetActor(DETAILS_ATTRIBUTE_DAMAGE, n) then
local fv = math.floor(v)
row.total = row.total + fv
row.targets[n] = (row.targets[n] or 0) + fv
end
end
end
row.counter = 1
row.n_amt = 1
row.n_total = row.total
row.n_max = row.total
return row
end
-- rebuild a per-fight row from the persisted buckets (post-reload); prime
-- injApplied so the live reconciler does not double-add on top of it
local function RowFromData(id)
local map = detailsData[id]
if not map then return nil end
local row = NewRow()
local total = math.floor(segTotals[id] or 0)
row.total = total
row.counter = 1
row.n_amt = 1
row.n_total = total
row.n_max = total
row.n_min = total
local inj = { me = total, byName = {} }
for n, v in pairs(map) do
local fv = math.floor(v)
row.targets[n] = fv
inj.byName[n] = fv
end
myRows[id] = row
injApplied[id] = inj
return row
end
local insertedTbl, insertedId
local function SyncBreakdownRow()
local D = _G.Details
if not (D and db and playerName) then return end
local bw = D.GetBreakdownWindow and D:GetBreakdownWindow()
local show, combat = false, nil
if bw and bw.ativo and bw.instancia and bw.instancia.meu_id
and db.redistWin[bw.instancia.meu_id] then
local actor = D.GetActorObjectFromBreakdownWindow and D:GetActorObjectFromBreakdownWindow()
if actor and (actor.nome or actor.name) == playerName then
show = true
combat = bw.instancia.showing
end
end
local desiredTbl, row
if show and combat then
local me = combat.GetActor and combat:GetActor(DETAILS_ATTRIBUTE_DAMAGE, playerName)
local tbl = me and me.spells and me.spells._ActorTable
if tbl then
if combat == D:GetOverallCombat() then
row = BuildOverallRow(combat)
elseif combat.GetCombatNumber then
local cid = combat:GetCombatNumber()
row = myRows[cid] or RowFromData(cid)
end
if row and row.total >= 1 then desiredTbl = tbl end
end
end
local changed = false
if insertedTbl and insertedTbl ~= desiredTbl then
if insertedId then insertedTbl[insertedId] = nil end
insertedTbl, insertedId = nil, nil
changed = true
end
if desiredTbl and row then
local rid = row.id or db.buffSpellId
if rid then
if desiredTbl[rid] == nil then changed = true end
desiredTbl[rid] = row
insertedTbl, insertedId = desiredTbl, rid
end
end
-- the panel does not redraw itself out of combat: force a rebuild
-- whenever the row was just inserted or removed while it is open
if changed and bw and bw.ativo and D.OpenBreakdownWindow then
local actor = D.GetActorObjectFromBreakdownWindow and D:GetActorObjectFromBreakdownWindow()
if actor and bw.instancia then
D:OpenBreakdownWindow(bw.instancia, actor, false, true)
end
end
end
-- old versions persisted the row into saved segments - scrub them away
local function PurgeOldRows()
local D = _G.Details
if not (D and playerName and db.knownBuffIds) then return end
local function purge(combat)
local me = combat and combat.GetActor
and combat:GetActor(DETAILS_ATTRIBUTE_DAMAGE, playerName)
local tbl = me and me.spells and me.spells._ActorTable
if tbl then
for sid in pairs(db.knownBuffIds) do tbl[sid] = nil end
end
end
purge(D:GetCurrentCombat())
purge(D:GetOverallCombat())
local segs = D.GetCombatSegments and D:GetCombatSegments()
if segs then
for _, cmb in ipairs(segs) do purge(cmb) end
end
end
-- hovering the buff's row in the breakdown: show the per-player source
-- list as a tooltip anchored to the bar (recognized via the bar frame's
-- cached spellId under the mouse - works on the bar, icon or dial)
local hoverOwner
local hoverWatch = CreateFrame("Frame")
local hvAcc = 0
hoverWatch:SetScript("OnUpdate", function(self, e)
hvAcc = hvAcc + e
if hvAcc < 0.15 then return end
hvAcc = 0
if not db then return end
local D = _G.Details
local bw = D and D.GetBreakdownWindow and D:GetBreakdownWindow()
local sid = db.buffSpellId
local f = GetMouseFocus and GetMouseFocus()
-- keep showing for the hovered bar; if the bar was CLICKED (selection
-- overlay visible) the tooltip stays pinned after the mouse leaves
local cur
if bw and bw.ativo and sid then
if f and f.spellId == sid then
cur = f
elseif hoverOwner and hoverOwner.spellId == sid and hoverOwner:IsVisible()
and hoverOwner.overlayTexture and hoverOwner.overlayTexture:IsShown() then
cur = hoverOwner
end
end
if cur then
if hoverOwner ~= cur or not GameTooltip:IsOwned(cur) then
hoverOwner = cur
local combat = bw.instancia and bw.instancia.showing
local row
if combat then
if combat == D:GetOverallCombat() then
row = BuildOverallRow(combat)
elseif combat.GetCombatNumber then
local cid = combat:GetCombatNumber()
row = myRows[cid] or RowFromData(cid)
end
end
GameTooltip:SetOwner(cur, "ANCHOR_RIGHT")
GameTooltip:AddLine(db.buffName .. " - attributed from", 0, 0.8, 1)
if row and next(row.targets) then
local list = {}
for n, v in pairs(row.targets) do list[#list + 1] = { n, v } end
table.sort(list, function(x, y) return x[2] > y[2] end)
for i = 1, math.min(#list, 12) do
GameTooltip:AddDoubleLine(list[i][1], Fmt(list[i][2]), 1, 1, 1, 1, 0.82, 0)
end
if #list > 12 then
GameTooltip:AddLine(("... and %d more"):format(#list - 12), 0.6, 0.6, 0.6)
end
GameTooltip:AddDoubleLine("Total", Fmt(row.total or 0), 0.6, 1, 0.6, 0.6, 1, 0.6)
else
GameTooltip:AddLine("no attribution data for this segment", 0.7, 0.7, 0.7)
end
GameTooltip:Show()
end
elseif hoverOwner then
if GameTooltip:IsOwned(hoverOwner) then GameTooltip:Hide() end
hoverOwner = nil
end
end)
-- zero out my extension segment everywhere (option turned off)
local function ClearExtraBars()
local D = _G.Details
if not (D and playerName) then return end
local function clearIn(combat)
if combat and combat.GetActor then
local actor = combat:GetActor(DETAILS_ATTRIBUTE_DAMAGE, playerName)
if actor then actor.total_extra = 0 end
end
end
clearIn(D:GetCurrentCombat())
clearIn(D:GetOverallCombat())
local segs = D.GetCombatSegments and D:GetCombatSegments()
if segs then
for _, cmb in ipairs(segs) do clearIn(cmb) end
end
end
---------------------------------------------------------------------------
-- mode "merge": full redistribution inside the DEFAULT Damage Done / DPS.
-- Batched once a second: my actor.total grows by the attributed delta (and
-- a real spell row for the buff is added so clicking my bar accounts for
-- it), each beneficiary's actor.total shrinks by their share. Solid bars,
-- real ranking - the retail post-support-events look.
---------------------------------------------------------------------------
-- maintains OUR OWN buff row per fight (never written into shared data;
-- SyncBreakdownRow slots it into view only when appropriate). Bar
-- redistribution is per-window at draw time (InstallRedistHook).
local function ReconcileInjection()
if not db then return end
local D = _G.Details
if not D then return end
local c = D:GetCurrentCombat()
if not (c and c.GetCombatNumber) then return end
local id = c:GetCombatNumber()
local map = detailsData[id]
if not map then return end
local inj = injApplied[id]
if not inj then inj = { me = 0, byName = {} } injApplied[id] = inj end
local row = EnsureRow(id)
if db.buffSpellId then row.id = db.buffSpellId end
local target = math.floor(segTotals[id] or 0)
local delta = target - inj.me
if delta >= 1 then
row.total = row.total + delta
row.counter = row.counter + 1
row.n_amt = row.n_amt + 1
row.n_total = row.n_total + delta
if delta > row.n_max then row.n_max = delta end
if row.n_min == 0 or delta < row.n_min then row.n_min = delta end
inj.me = target
end
for name, v in pairs(map) do
local applied = inj.byName[name] or 0
local d2 = math.floor(v) - applied
if d2 >= 1 then
inj.byName[name] = applied + d2
row.targets[name] = (row.targets[name] or 0) + d2
end
end
end
---------------------------------------------------------------------------
-- Details! custom display ("Support DPS")
---------------------------------------------------------------------------
-- attributed-damage map (name -> value) for whichever segment Details shows
local function GetSegmentMap(combat)
local D = _G.Details
if not D then return nil end
if combat == D:GetOverallCombat() then
local map = {}
for _, t in pairs(detailsData) do
for name, v in pairs(t) do map[name] = (map[name] or 0) + v end
end
return map
elseif combat.GetCombatNumber then
return detailsData[combat:GetCombatNumber()]
end
end
-- breakdown display: one bar per beneficiary the buff earned through
function SupportMeter_FillCustom(combat, container)
local map = GetSegmentMap(combat)
if not map then return 0, 0, 0 end
local total, top, amount = 0, 0, 0
for name, value in pairs(map) do
if value >= 1 then
local actor = combat:GetActor(DETAILS_ATTRIBUTE_DAMAGE, name)
if actor then
container:AddValue(actor, math.floor(value))
total = total + value
if value > top then top = value end
amount = amount + 1
end
end
end
return math.floor(total), math.floor(top), amount
end
-- merged display (retail-Augmentation style): every player's damage bar,
-- with the support share MOVED from each beneficiary onto my bar
function SupportMeter_FillMerged(combat, container)
if not combat.GetActorList then return 0, 0, 0 end
local map = GetSegmentMap(combat) or {}
local myBonus = 0
for _, v in pairs(map) do myBonus = myBonus + v end
for _, player in ipairs(combat:GetActorList(DETAILS_ATTRIBUTE_DAMAGE)) do
if player:IsGroupPlayer() then
local nm = player.nome or player.name
local value = (player.total or 0) - (map[nm] or 0)
-- fold pets into their owner, minus what the buff earned off the pet
if player.pets then
for _, petName in ipairs(player.pets) do
local pet = combat:GetActor(DETAILS_ATTRIBUTE_DAMAGE, petName)
if pet then
value = value + (pet.total or 0) - (map[petName] or 0)
end
end
end
if nm == playerName then value = value + myBonus end
if value >= 1 then
container:AddValue(player, math.floor(value))
end
end
end
local total, top = container:GetTotalAndHighestValue()
return math.floor(total), math.floor(top), container:GetNumActors()
end
-- hover tooltip on a breakdown bar: per-spell attributed gain. Every spell
-- of the boosted school gains the SAME fraction of its damage (per-ability
-- coefficients cancel in V/(pool+V)), so gain = spell damage x fraction.
function SupportMeter_FillTooltip(actor, combat)
local GameCooltip = _G.GameCooltip2
if not GameCooltip then return end
local nm = actor.nome or actor.name
if not nm then return end
local player = combat:GetActor(DETAILS_ATTRIBUTE_DAMAGE, nm)
if not (player and player.GetSpellList) then return end
-- same role/pool/fraction logic as the attribution engine
local role
local p = db.pools[nm]
if p and p.exact then
role = ((p.sp or 0) >= (p.ap or 0)) and "sp" or "ap"
else
local m = dmgMix[nm]
role = (m and m.phys > m.magic) and "ap" or "sp"
end
local isAP = (role == "ap")
local share = isAP and db.shareMelee or db.share
local pool = GetPool(nm, isAP)
local frac = share * db.buffValue / (pool + db.buffValue)
local list = {}
for spellID, spellTable in pairs(player:GetSpellList()) do
local school = spellTable.spellschool or spellTable.school or 1
if ((school == 1) == isAP) then
local gain = (spellTable.total or 0) * frac
if gain >= 1 then list[#list + 1] = { spellID, gain } end
end
end
table.sort(list, function(a, b) return a[2] > b[2] end)
local topGain = list[1] and list[1][2] or 0
local lineH = (_G._detalhes and _detalhes.tooltip and _detalhes.tooltip.line_height) or 14
for i = 1, math.min(#list, 8) do
local spellID, gain = list[i][1], list[i][2]
local spellName, _, spellIcon = Details.GetSpellInfo(spellID)
GameCooltip:AddLine(spellName or ("spell " .. spellID), Fmt(gain))
Details:AddTooltipBackgroundStatusbar(false, gain / topGain * 100)
if spellIcon then
GameCooltip:AddIcon(spellIcon, 1, 1, lineH, lineH, 0.078125, 0.921875, 0.078125, 0.921875)
end
end
end
local InstallDetailsPlugin -- defined after the options panel (needs it)
local AttachWindowButtons -- ditto (title-bar toggle icons)
local function InstallDetailsDisplay()
local D = _G.Details
if not (D and D.InstallCustomObject) then return false end
D:InstallCustomObject({
name = "Support DPS",
icon = [[Interface\Icons\Ability_Warrior_BattleShout]],
attribute = false,
spellid = false,
author = "Arc",
desc = "Breakdown: who your support buff earned its damage through (SupportMeter). Hover a bar for per-spell gains.",
source = false,
target = false,
script_version = 3,
script = [[
local combat, container, instance = ...
if _G.SupportMeter_FillCustom then
return _G.SupportMeter_FillCustom(combat, container)
end
return 0, 0, 0
]],
tooltip = [[
local actor, combat, instance = ...
if _G.SupportMeter_FillTooltip then
_G.SupportMeter_FillTooltip(actor, combat)
end
]],
})
D:InstallCustomObject({
name = "Damage Done + Support",
icon = [[Interface\Icons\INV_Sword_27]],
attribute = false,
spellid = false,
author = "Arc",
desc = "Damage meter with support redistribution: your buff's share is moved from each buffed player onto your bar (SupportMeter).",
source = false,
target = false,
script_version = 3,
script = [[
local combat, container, instance = ...
if _G.SupportMeter_FillMerged then
return _G.SupportMeter_FillMerged(combat, container)
end
return 0, 0, 0
]],
tooltip = false,
})
if D.RemoveCustomObject then
D:RemoveCustomObject("Damage Done (No Support)")
D:RemoveCustomObject("Damage Done (Raw)")
end
return true
end
---------------------------------------------------------------------------
-- per-window redistribution in the NATIVE damage displays. The shared