Skip to content

Commit 2ad8600

Browse files
committed
feat: add EllesmereUIDamageMeter standalone addon
Blizzard C_DamageMeter API-powered damage meter with full EUI integration. Core features: - Class-colored bars with multiple icon styles (Fabled, Modern, Arcade, etc.) - Per-player spell drill-down with spell icons and percentages - Encounter/session selector with Current, Overall, and per-boss views - Combined DPS/Damage and HPS/Healing display modes - Scroll support for 40+ player raids - Test mode with realistic data for all meter types EUI integration: - Lite addon lifecycle (OnEnable, NewDB, RegisterEvent) - MakeBorder pixel-perfect borders - Global font system via ResolveFontPath - SetElementVisibility for flight/pet battle hiding - Unlock mode registration with resize support - Options page with live preview via RegisterModule/Widgets Polish: - Report to Chat (right-click bar → Party/Raid/Say/Guild) - Bar hover highlight and local player indicator - Group total in header timer display - /euidm slash command (toggle, test, reset, report) - Empty state message when no combat data - Stale cache cleanup on reset and zone change Original implementation by Trenchy.
1 parent 8fde139 commit 2ad8600

8 files changed

Lines changed: 2526 additions & 0 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
## Interface: 120000, 120001
2+
## Title: |cff0cd39cEllesmere|r|cffffffffUI|r Damage Meter
3+
## Notes: Damage meter powered by Blizzard's C_DamageMeter API
4+
## Author: Trenchy
5+
## Version: 0.1
6+
## IconTexture: Interface\Icons\INV_Misc_Map_01
7+
## Dependencies: EllesmereUI
8+
## OptionalDeps: LibSharedMedia-3.0
9+
## SavedVariables: EllesmereUIDamageMeterDB
10+
11+
core.lua
12+
bars.lua
13+
window.lua
14+
update.lua
15+
testdata.lua
16+
options.lua

EllesmereUIDamageMeter/bars.lua

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
local addon = EllesmereUIDamageMeter
2+
local S = addon.S
3+
if not S then return end
4+
5+
local floor = math.floor
6+
7+
local MakeBorder = EllesmereUI.MakeBorder
8+
9+
local GetClassColor = S.GetClassColor
10+
11+
function S.CreateBar(parent)
12+
local bar = {}
13+
14+
bar.frame = CreateFrame("Frame", nil, parent)
15+
16+
bar.background = bar.frame:CreateTexture(nil, "BACKGROUND")
17+
bar.background:SetAllPoints()
18+
bar.background:SetTexture(S.DEFAULT_TEX)
19+
bar.background:SetVertexColor(0.15, 0.15, 0.15, 0.35)
20+
21+
bar.statusbar = CreateFrame("StatusBar", nil, bar.frame)
22+
bar.statusbar:SetAllPoints()
23+
bar.statusbar:SetStatusBarTexture(S.DEFAULT_TEX)
24+
bar.statusbar:SetMinMaxValues(0, 1)
25+
bar.statusbar:SetValue(0)
26+
bar.statusbar.smoothing = Enum.StatusBarInterpolation and Enum.StatusBarInterpolation.ExponentialEaseOut or nil
27+
28+
bar.classIcon = bar.statusbar:CreateTexture(nil, "OVERLAY")
29+
bar.classIcon:SetTexture(S.CLASS_ICONS)
30+
bar.classIcon:SetSize(16, 16)
31+
bar.classIcon:SetPoint("LEFT", 1, 0)
32+
bar.classIcon:Hide()
33+
34+
bar.pctText = bar.statusbar:CreateFontString(nil, "OVERLAY")
35+
bar.pctText:SetPoint("RIGHT", -4, 0)
36+
bar.pctText:SetJustifyH("RIGHT")
37+
bar.pctText:SetWordWrap(false)
38+
bar.pctText:SetShadowOffset(1, -1)
39+
bar.pctText:Hide()
40+
41+
bar.rightText = bar.statusbar:CreateFontString(nil, "OVERLAY")
42+
bar.rightText:SetPoint("RIGHT", -4, 0)
43+
bar.rightText:SetJustifyH("RIGHT")
44+
bar.rightText:SetWordWrap(false)
45+
bar.rightText:SetShadowOffset(1, -1)
46+
47+
bar.leftText = bar.statusbar:CreateFontString(nil, "OVERLAY")
48+
bar.leftText:SetPoint("LEFT", 4, 0)
49+
bar.leftText:SetPoint("RIGHT", bar.rightText, "LEFT", -4, 0)
50+
bar.leftText:SetJustifyH("LEFT")
51+
bar.leftText:SetWordWrap(false)
52+
bar.leftText:SetShadowOffset(1, -1)
53+
54+
bar.border = MakeBorder(bar.frame, 0, 0, 0, 1)
55+
bar.border._frame:SetFrameLevel(bar.statusbar:GetFrameLevel() + 2)
56+
bar.border._frame:Hide()
57+
58+
bar.textFrame = CreateFrame("Frame", nil, bar.frame)
59+
bar.textFrame:SetAllPoints()
60+
bar.textFrame:SetFrameLevel(bar.border._frame:GetFrameLevel() + 1)
61+
62+
bar.leftText:SetParent(bar.textFrame)
63+
bar.rightText:SetParent(bar.textFrame)
64+
bar.pctText:SetParent(bar.textFrame)
65+
66+
bar.highlight = bar.statusbar:CreateTexture(nil, "ARTWORK")
67+
bar.highlight:SetAllPoints()
68+
bar.highlight:SetColorTexture(1, 1, 1, 0.1)
69+
bar.highlight:Hide()
70+
71+
bar.selfIndicator = bar.frame:CreateTexture(nil, "ARTWORK")
72+
bar.selfIndicator:SetPoint("TOPLEFT", bar.frame, "TOPLEFT", 0, 0)
73+
bar.selfIndicator:SetPoint("BOTTOMLEFT", bar.frame, "BOTTOMLEFT", 0, 0)
74+
bar.selfIndicator:SetWidth(2)
75+
bar.selfIndicator:SetColorTexture(1, 1, 1, 0.6)
76+
bar.selfIndicator:Hide()
77+
78+
bar.frame:EnableMouse(true)
79+
bar.frame:Hide()
80+
return bar
81+
end
82+
83+
function S.ApplyBarIconLayout(bar, db)
84+
local iconSize = max(8, (db.barHeight or 18) - 2)
85+
bar.classIcon:SetSize(iconSize, iconSize)
86+
bar.leftText:ClearAllPoints()
87+
if db.showClassIcon then
88+
bar.leftText:SetPoint("LEFT", bar.classIcon, "RIGHT", 2, 0)
89+
else
90+
bar.leftText:SetPoint("LEFT", 4, 0)
91+
end
92+
bar.leftText:SetPoint("RIGHT", bar.rightText, "LEFT", -4, 0)
93+
end
94+
95+
function S.ApplyBarBorder(bar, db)
96+
if db.barBorderEnabled then
97+
bar.border._frame:Show()
98+
else
99+
bar.border._frame:Hide()
100+
end
101+
end
102+
103+
function S.ComputeNumVisible(win)
104+
local db = S.GetWinDB(win.index)
105+
local barHt = max(1, db.barHeight or 18)
106+
107+
if not win.window then return 1 end
108+
local availH = win.window:GetHeight() - S.HEADER_HEIGHT
109+
110+
if not availH or availH < 1 then return 1 end
111+
local spacing = max(0, db.barSpacing or 1)
112+
return max(1, floor(availH / (barHt + spacing)))
113+
end
114+
115+
function S.ResizeStandalone(win)
116+
if not win or not win.window or not win.frame then return end
117+
118+
local db = S.GetWinDB(win.index)
119+
local w, h = db.standaloneWidth, db.standaloneHeight
120+
win.window:SetSize(w, h)
121+
122+
if win.window.mover then
123+
win.window.mover:SetSize(w, h)
124+
end
125+
126+
local barHt = max(1, db.barHeight or 18)
127+
for i = 1, S.MAX_BARS do
128+
if win.bars[i] then win.bars[i].frame:SetHeight(barHt) end
129+
end
130+
end
131+
132+
function S.EnterDrillDown(win, guid, name, classFilename)
133+
win.drillSource = { guid = guid, name = name, class = classFilename }
134+
win.scrollOffset = 0
135+
S.RefreshWindow(win)
136+
end
137+
138+
function S.ExitDrillDown(win)
139+
if not win.drillSource then return end
140+
win.drillSource = nil
141+
win.scrollOffset = 0
142+
S.RefreshWindow(win)
143+
end
144+
145+
function S.GetDrillSpellCount(win)
146+
local ds = win.drillSource
147+
if not ds then return 0 end
148+
149+
if S.testMode then
150+
local tdata = S.GetTestData(win)
151+
for _, td in ipairs(tdata) do
152+
if td.name == ds.name then return td.spells and #td.spells or 0 end
153+
end
154+
return 0
155+
end
156+
157+
local meterType = S.ResolveMeterType(S.MODE_ORDER[win.modeIndex])
158+
local sourceData = ds.guid and S.GetSessionSource(win, meterType, ds.guid)
159+
return (sourceData and sourceData.combatSpells) and #sourceData.combatSpells or 0
160+
end
161+
162+
function S.SetupBarInteraction(bar, win)
163+
bar.frame:SetScript("OnEnter", function(self)
164+
if EllesmereUI._unlockActive then return end
165+
bar.highlight:Show()
166+
if win.drillSource then
167+
if self.drillSpellID then
168+
GameTooltip_SetDefaultAnchor(GameTooltip, self)
169+
GameTooltip:SetSpellByID(self.drillSpellID)
170+
GameTooltip:Show()
171+
end
172+
return
173+
end
174+
175+
local unitShown = false
176+
local guid = self.sourceGUID
177+
if guid then
178+
local unit = S.FindUnitByGUID(guid)
179+
if unit then
180+
GameTooltip_SetDefaultAnchor(GameTooltip, self)
181+
GameTooltip:SetUnit(unit)
182+
unitShown = true
183+
end
184+
end
185+
if not unitShown then
186+
GameTooltip_SetDefaultAnchor(GameTooltip, self)
187+
if self.sourceName then
188+
local cls = self.sourceClass
189+
if not cls then cls = guid and S.classCache[guid] end
190+
if not cls and self.testIndex then
191+
local td = S.GetTestData(win)[self.testIndex]
192+
if td then cls = td.class end
193+
end
194+
local cr, cg, cb = GetClassColor(cls)
195+
GameTooltip:AddLine(self.sourceName, cr, cg, cb)
196+
end
197+
end
198+
GameTooltip:AddLine("Click for spell breakdown", 0.7, 0.7, 0.7)
199+
GameTooltip:Show()
200+
end)
201+
202+
bar.frame:SetScript("OnLeave", function()
203+
bar.highlight:Hide()
204+
GameTooltip_Hide()
205+
end)
206+
207+
bar.frame:SetScript("OnMouseUp", function(self, button)
208+
if win.drillSource then
209+
if button == "RightButton" then
210+
S.ExitDrillDown(win)
211+
end
212+
return
213+
end
214+
215+
if button == "LeftButton" then
216+
GameTooltip:Hide()
217+
if S.testMode and self.testIndex then
218+
local td = S.GetTestData(win)[self.testIndex]
219+
if td then
220+
S.EnterDrillDown(win, nil, td.name, td.class)
221+
end
222+
return
223+
end
224+
if self.sourceGUID and self.sourceName then
225+
local class = S.classCache[self.sourceGUID]
226+
S.EnterDrillDown(win, self.sourceGUID, self.sourceName, class)
227+
end
228+
elseif button == "RightButton" then
229+
GameTooltip:Hide()
230+
local addon = EllesmereUIDamageMeter
231+
if addon and addon.ReportMeter then
232+
MenuUtil.CreateContextMenu(self, function(_, rootDescription)
233+
rootDescription:CreateTitle("Report Meter")
234+
local channels = { {"Party", "PARTY"}, {"Raid", "RAID"}, {"Say", "SAY"}, {"Guild", "GUILD"} }
235+
for _, ch in ipairs(channels) do
236+
rootDescription:CreateButton(ch[1], function()
237+
addon:ReportMeter(ch[2])
238+
end)
239+
end
240+
end)
241+
end
242+
end
243+
end)
244+
end
245+
246+
function S.ApplySessionHighlight(win, db)
247+
if win.sessionId then
248+
win.header.sessText:SetTextColor(1, 0.3, 0.3)
249+
else
250+
win.header.sessText:SetTextColor(db.headerFontColor.r, db.headerFontColor.g, db.headerFontColor.b)
251+
end
252+
end
253+
254+
function S.ResetDrillBar(bar, db)
255+
bar._isDrill = nil
256+
bar._drillHasIcon = nil
257+
bar.pctText:Hide()
258+
bar.rightText:ClearAllPoints()
259+
bar.rightText:SetPoint("RIGHT", -4, 0)
260+
local style = db.classIconStyle or 'fabled'
261+
if style == 'fabled' or not style then
262+
bar.classIcon:SetTexture(S.CLASS_ICONS)
263+
end
264+
S.ApplyBarIconLayout(bar, db)
265+
end
266+
267+
function S.ResetWindowState(win)
268+
win.scrollOffset = 0
269+
win.drillSource = nil
270+
win.sessionId = nil
271+
win.sessionType = Enum.DamageMeterSessionType.Current
272+
end

0 commit comments

Comments
 (0)