diff --git a/EllesmereUIDamageMeter/EllesmereUIDamageMeter.toc b/EllesmereUIDamageMeter/EllesmereUIDamageMeter.toc new file mode 100644 index 00000000..1043b99e --- /dev/null +++ b/EllesmereUIDamageMeter/EllesmereUIDamageMeter.toc @@ -0,0 +1,16 @@ +## Interface: 120000, 120001 +## Title: |cff0cd39cEllesmere|r|cffffffffUI|r Damage Meter +## Notes: Damage meter powered by Blizzard's C_DamageMeter API +## Author: Trenchy +## Version: 0.1 +## IconTexture: Interface\Icons\INV_Misc_Map_01 +## Dependencies: EllesmereUI +## OptionalDeps: LibSharedMedia-3.0 +## SavedVariables: EllesmereUIDamageMeterDB + +core.lua +bars.lua +window.lua +update.lua +testdata.lua +options.lua diff --git a/EllesmereUIDamageMeter/bars.lua b/EllesmereUIDamageMeter/bars.lua new file mode 100644 index 00000000..44586801 --- /dev/null +++ b/EllesmereUIDamageMeter/bars.lua @@ -0,0 +1,272 @@ +local addon = EllesmereUIDamageMeter +local S = addon.S +if not S then return end + +local floor = math.floor + +local MakeBorder = EllesmereUI.MakeBorder + +local GetClassColor = S.GetClassColor + +function S.CreateBar(parent) + local bar = {} + + bar.frame = CreateFrame("Frame", nil, parent) + + bar.background = bar.frame:CreateTexture(nil, "BACKGROUND") + bar.background:SetAllPoints() + bar.background:SetTexture(S.DEFAULT_TEX) + bar.background:SetVertexColor(0.15, 0.15, 0.15, 0.35) + + bar.statusbar = CreateFrame("StatusBar", nil, bar.frame) + bar.statusbar:SetAllPoints() + bar.statusbar:SetStatusBarTexture(S.DEFAULT_TEX) + bar.statusbar:SetMinMaxValues(0, 1) + bar.statusbar:SetValue(0) + bar.statusbar.smoothing = Enum.StatusBarInterpolation and Enum.StatusBarInterpolation.ExponentialEaseOut or nil + + bar.classIcon = bar.statusbar:CreateTexture(nil, "OVERLAY") + bar.classIcon:SetTexture(S.CLASS_ICONS) + bar.classIcon:SetSize(16, 16) + bar.classIcon:SetPoint("LEFT", 1, 0) + bar.classIcon:Hide() + + bar.pctText = bar.statusbar:CreateFontString(nil, "OVERLAY") + bar.pctText:SetPoint("RIGHT", -4, 0) + bar.pctText:SetJustifyH("RIGHT") + bar.pctText:SetWordWrap(false) + bar.pctText:SetShadowOffset(1, -1) + bar.pctText:Hide() + + bar.rightText = bar.statusbar:CreateFontString(nil, "OVERLAY") + bar.rightText:SetPoint("RIGHT", -4, 0) + bar.rightText:SetJustifyH("RIGHT") + bar.rightText:SetWordWrap(false) + bar.rightText:SetShadowOffset(1, -1) + + bar.leftText = bar.statusbar:CreateFontString(nil, "OVERLAY") + bar.leftText:SetPoint("LEFT", 4, 0) + bar.leftText:SetPoint("RIGHT", bar.rightText, "LEFT", -4, 0) + bar.leftText:SetJustifyH("LEFT") + bar.leftText:SetWordWrap(false) + bar.leftText:SetShadowOffset(1, -1) + + bar.border = MakeBorder(bar.frame, 0, 0, 0, 1) + bar.border._frame:SetFrameLevel(bar.statusbar:GetFrameLevel() + 2) + bar.border._frame:Hide() + + bar.textFrame = CreateFrame("Frame", nil, bar.frame) + bar.textFrame:SetAllPoints() + bar.textFrame:SetFrameLevel(bar.border._frame:GetFrameLevel() + 1) + + bar.leftText:SetParent(bar.textFrame) + bar.rightText:SetParent(bar.textFrame) + bar.pctText:SetParent(bar.textFrame) + + bar.highlight = bar.statusbar:CreateTexture(nil, "ARTWORK") + bar.highlight:SetAllPoints() + bar.highlight:SetColorTexture(1, 1, 1, 0.1) + bar.highlight:Hide() + + bar.selfIndicator = bar.frame:CreateTexture(nil, "ARTWORK") + bar.selfIndicator:SetPoint("TOPLEFT", bar.frame, "TOPLEFT", 0, 0) + bar.selfIndicator:SetPoint("BOTTOMLEFT", bar.frame, "BOTTOMLEFT", 0, 0) + bar.selfIndicator:SetWidth(2) + bar.selfIndicator:SetColorTexture(1, 1, 1, 0.6) + bar.selfIndicator:Hide() + + bar.frame:EnableMouse(true) + bar.frame:Hide() + return bar +end + +function S.ApplyBarIconLayout(bar, db) + local iconSize = max(8, (db.barHeight or 18) - 2) + bar.classIcon:SetSize(iconSize, iconSize) + bar.leftText:ClearAllPoints() + if db.showClassIcon then + bar.leftText:SetPoint("LEFT", bar.classIcon, "RIGHT", 2, 0) + else + bar.leftText:SetPoint("LEFT", 4, 0) + end + bar.leftText:SetPoint("RIGHT", bar.rightText, "LEFT", -4, 0) +end + +function S.ApplyBarBorder(bar, db) + if db.barBorderEnabled then + bar.border._frame:Show() + else + bar.border._frame:Hide() + end +end + +function S.ComputeNumVisible(win) + local db = S.GetWinDB(win.index) + local barHt = max(1, db.barHeight or 18) + + if not win.window then return 1 end + local availH = win.window:GetHeight() - S.HEADER_HEIGHT + + if not availH or availH < 1 then return 1 end + local spacing = max(0, db.barSpacing or 1) + return max(1, floor(availH / (barHt + spacing))) +end + +function S.ResizeStandalone(win) + if not win or not win.window or not win.frame then return end + + local db = S.GetWinDB(win.index) + local w, h = db.standaloneWidth, db.standaloneHeight + win.window:SetSize(w, h) + + if win.window.mover then + win.window.mover:SetSize(w, h) + end + + local barHt = max(1, db.barHeight or 18) + for i = 1, S.MAX_BARS do + if win.bars[i] then win.bars[i].frame:SetHeight(barHt) end + end +end + +function S.EnterDrillDown(win, guid, name, classFilename) + win.drillSource = { guid = guid, name = name, class = classFilename } + win.scrollOffset = 0 + S.RefreshWindow(win) +end + +function S.ExitDrillDown(win) + if not win.drillSource then return end + win.drillSource = nil + win.scrollOffset = 0 + S.RefreshWindow(win) +end + +function S.GetDrillSpellCount(win) + local ds = win.drillSource + if not ds then return 0 end + + if S.testMode then + local tdata = S.GetTestData(win) + for _, td in ipairs(tdata) do + if td.name == ds.name then return td.spells and #td.spells or 0 end + end + return 0 + end + + local meterType = S.ResolveMeterType(S.MODE_ORDER[win.modeIndex]) + local sourceData = ds.guid and S.GetSessionSource(win, meterType, ds.guid) + return (sourceData and sourceData.combatSpells) and #sourceData.combatSpells or 0 +end + +function S.SetupBarInteraction(bar, win) + bar.frame:SetScript("OnEnter", function(self) + if EllesmereUI._unlockActive then return end + bar.highlight:Show() + if win.drillSource then + if self.drillSpellID then + GameTooltip_SetDefaultAnchor(GameTooltip, self) + GameTooltip:SetSpellByID(self.drillSpellID) + GameTooltip:Show() + end + return + end + + local unitShown = false + local guid = self.sourceGUID + if guid then + local unit = S.FindUnitByGUID(guid) + if unit then + GameTooltip_SetDefaultAnchor(GameTooltip, self) + GameTooltip:SetUnit(unit) + unitShown = true + end + end + if not unitShown then + GameTooltip_SetDefaultAnchor(GameTooltip, self) + if self.sourceName then + local cls = self.sourceClass + if not cls then cls = guid and S.classCache[guid] end + if not cls and self.testIndex then + local td = S.GetTestData(win)[self.testIndex] + if td then cls = td.class end + end + local cr, cg, cb = GetClassColor(cls) + GameTooltip:AddLine(self.sourceName, cr, cg, cb) + end + end + GameTooltip:AddLine("Click for spell breakdown", 0.7, 0.7, 0.7) + GameTooltip:Show() + end) + + bar.frame:SetScript("OnLeave", function() + bar.highlight:Hide() + GameTooltip_Hide() + end) + + bar.frame:SetScript("OnMouseUp", function(self, button) + if win.drillSource then + if button == "RightButton" then + S.ExitDrillDown(win) + end + return + end + + if button == "LeftButton" then + GameTooltip:Hide() + if S.testMode and self.testIndex then + local td = S.GetTestData(win)[self.testIndex] + if td then + S.EnterDrillDown(win, nil, td.name, td.class) + end + return + end + if self.sourceGUID and self.sourceName then + local class = S.classCache[self.sourceGUID] + S.EnterDrillDown(win, self.sourceGUID, self.sourceName, class) + end + elseif button == "RightButton" then + GameTooltip:Hide() + local addon = EllesmereUIDamageMeter + if addon and addon.ReportMeter then + MenuUtil.CreateContextMenu(self, function(_, rootDescription) + rootDescription:CreateTitle("Report Meter") + local channels = { {"Party", "PARTY"}, {"Raid", "RAID"}, {"Say", "SAY"}, {"Guild", "GUILD"} } + for _, ch in ipairs(channels) do + rootDescription:CreateButton(ch[1], function() + addon:ReportMeter(ch[2]) + end) + end + end) + end + end + end) +end + +function S.ApplySessionHighlight(win, db) + if win.sessionId then + win.header.sessText:SetTextColor(1, 0.3, 0.3) + else + win.header.sessText:SetTextColor(db.headerFontColor.r, db.headerFontColor.g, db.headerFontColor.b) + end +end + +function S.ResetDrillBar(bar, db) + bar._isDrill = nil + bar._drillHasIcon = nil + bar.pctText:Hide() + bar.rightText:ClearAllPoints() + bar.rightText:SetPoint("RIGHT", -4, 0) + local style = db.classIconStyle or 'fabled' + if style == 'fabled' or not style then + bar.classIcon:SetTexture(S.CLASS_ICONS) + end + S.ApplyBarIconLayout(bar, db) +end + +function S.ResetWindowState(win) + win.scrollOffset = 0 + win.drillSource = nil + win.sessionId = nil + win.sessionType = Enum.DamageMeterSessionType.Current +end diff --git a/EllesmereUIDamageMeter/core.lua b/EllesmereUIDamageMeter/core.lua new file mode 100644 index 00000000..21eec723 --- /dev/null +++ b/EllesmereUIDamageMeter/core.lua @@ -0,0 +1,444 @@ +local addon = EllesmereUI.Lite.NewAddon('EllesmereUIDamageMeter') + +if not C_DamageMeter or not Enum.DamageMeterType then return end + +addon.S = {} +local S = addon.S + +local LSM = LibStub('LibSharedMedia-3.0') + +function S.GetClassColor(classFilename) + if not classFilename then return 1, 1, 1 end + local cc = EllesmereUI.GetClassColor(classFilename) + if cc then return cc.r, cc.g, cc.b end + return 1, 1, 1 +end +local GetClassColor = S.GetClassColor + +function S.ResolveFontPath(fontNameOrNil) + if fontNameOrNil and fontNameOrNil ~= '' then + local path = LSM:Fetch('font', fontNameOrNil) + if path then return path end + end + return EllesmereUI.GetFontPath and EllesmereUI.GetFontPath() or LSM:Fetch('font', 'Expressway') +end + +function S.ApplyFont(fs, fontPath, size, flags) + if not fs then return end + local f = flags or '' + local shadow = false + if f == 'SHADOWOUTLINE' then + f = 'OUTLINE' + shadow = true + end + fs:SetFont(fontPath, size, f) + if shadow then + fs:SetShadowColor(0, 0, 0, 1) + fs:SetShadowOffset(1, -1) + else + fs:SetShadowColor(0, 0, 0, 0) + fs:SetShadowOffset(0, 0) + end +end + +S.MAX_BARS = 40 +S.PANEL_INSET = 2 +S.HEADER_HEIGHT = 22 +S.FLAT_TEX = 'Interface\\Buttons\\WHITE8x8' +S.DEFAULT_TEX = LSM:Fetch('statusbar', 'ElvUI Blank') or S.FLAT_TEX +-- Use of Fabled class icons with permission from Jiberish, 2026-03-10 +S.CLASS_ICONS = 'Interface\\AddOns\\EllesmereUIDamageMeter\\media\\fabled' + +S.texTextures = { ['none'] = S.FLAT_TEX } +S.texNames = { ['none'] = 'Flat' } +S.texOrder = { 'none' } +if EllesmereUI and EllesmereUI.AppendSharedMediaTextures then + EllesmereUI.AppendSharedMediaTextures(S.texNames, S.texOrder, nil, S.texTextures) +end + +S.texDropdownValues = {} +for _, key in ipairs(S.texOrder) do + if key ~= '---' then + S.texDropdownValues[key] = S.texNames[key] or key + end +end +S.texDropdownValues._menuOpts = { + itemHeight = 28, + background = function(key) + return S.texTextures[key] + end, +} + +S.texDropdownOrder = {} +for _, key in ipairs(S.texOrder) do + S.texDropdownOrder[#S.texDropdownOrder + 1] = key +end + +S.COMBINED_DAMAGE = "CombinedDamage" +S.COMBINED_HEALING = "CombinedHealing" + +S.COMBINED_DATA_TYPE = { + [S.COMBINED_DAMAGE] = Enum.DamageMeterType.DamageDone, + [S.COMBINED_HEALING] = Enum.DamageMeterType.HealingDone, +} + +S.MODE_ORDER = { + Enum.DamageMeterType.DamageDone, + Enum.DamageMeterType.Dps, + S.COMBINED_DAMAGE, + Enum.DamageMeterType.HealingDone, + Enum.DamageMeterType.Hps, + S.COMBINED_HEALING, + Enum.DamageMeterType.Absorbs, + Enum.DamageMeterType.Interrupts, + Enum.DamageMeterType.Dispels, + Enum.DamageMeterType.DamageTaken, + Enum.DamageMeterType.AvoidableDamageTaken, +} +if Enum.DamageMeterType.Deaths then S.MODE_ORDER[#S.MODE_ORDER + 1] = Enum.DamageMeterType.Deaths end +if Enum.DamageMeterType.EnemyDamageTaken then S.MODE_ORDER[#S.MODE_ORDER + 1] = Enum.DamageMeterType.EnemyDamageTaken end + +function S.ResolveMeterType(modeEntry) + return S.COMBINED_DATA_TYPE[modeEntry] or modeEntry +end + +S.MODE_LABELS = { + [Enum.DamageMeterType.DamageDone] = "Damage", + [Enum.DamageMeterType.Dps] = "DPS", + [S.COMBINED_DAMAGE] = "DPS/Damage", + [Enum.DamageMeterType.HealingDone] = "Healing", + [Enum.DamageMeterType.Hps] = "HPS", + [S.COMBINED_HEALING] = "HPS/Healing", + [Enum.DamageMeterType.Absorbs] = "Absorbs", + [Enum.DamageMeterType.Interrupts] = "Interrupts", + [Enum.DamageMeterType.Dispels] = "Dispels", + [Enum.DamageMeterType.DamageTaken] = "Damage Taken", + [Enum.DamageMeterType.AvoidableDamageTaken] = "Avoidable Damage Taken", +} +if Enum.DamageMeterType.Deaths then S.MODE_LABELS[Enum.DamageMeterType.Deaths] = "Deaths" end +if Enum.DamageMeterType.EnemyDamageTaken then S.MODE_LABELS[Enum.DamageMeterType.EnemyDamageTaken] = "Enemy Damage Taken" end + +S.MODE_SHORT = { + [Enum.DamageMeterType.DamageDone] = "Damage", + [Enum.DamageMeterType.Dps] = "DPS", + [S.COMBINED_DAMAGE] = "DPS/Dmg", + [Enum.DamageMeterType.HealingDone] = "Healing", + [Enum.DamageMeterType.Hps] = "HPS", + [S.COMBINED_HEALING] = "HPS/Heal", + [Enum.DamageMeterType.Absorbs] = "Absorbs", + [Enum.DamageMeterType.Interrupts] = "Interrupts", + [Enum.DamageMeterType.Dispels] = "Dispels", + [Enum.DamageMeterType.DamageTaken] = "Dmg Taken", + [Enum.DamageMeterType.AvoidableDamageTaken] = "Avoidable", +} +if Enum.DamageMeterType.Deaths then S.MODE_SHORT[Enum.DamageMeterType.Deaths] = "Deaths" end +if Enum.DamageMeterType.EnemyDamageTaken then S.MODE_SHORT[Enum.DamageMeterType.EnemyDamageTaken] = "Enemy Dmg" end + +S.CLASS_ICON_COORDS = { + WARRIOR = { 0, 0, 0, 0.125, 0.125, 0, 0.125, 0.125 }, + MAGE = { 0.125, 0, 0.125, 0.125, 0.25, 0, 0.25, 0.125 }, + ROGUE = { 0.25, 0, 0.25, 0.125, 0.375, 0, 0.375, 0.125 }, + DRUID = { 0.375, 0, 0.375, 0.125, 0.5, 0, 0.5, 0.125 }, + EVOKER = { 0.5, 0, 0.5, 0.125, 0.625, 0, 0.625, 0.125 }, + HUNTER = { 0, 0.125, 0, 0.25, 0.125, 0.125, 0.125, 0.25 }, + SHAMAN = { 0.125, 0.125, 0.125, 0.25, 0.25, 0.125, 0.25, 0.25 }, + PRIEST = { 0.25, 0.125, 0.25, 0.25, 0.375, 0.125, 0.375, 0.25 }, + WARLOCK = { 0.375, 0.125, 0.375, 0.25, 0.5, 0.125, 0.5, 0.25 }, + PALADIN = { 0, 0.25, 0, 0.375, 0.125, 0.25, 0.125, 0.375 }, + DEATHKNIGHT = { 0.125, 0.25, 0.125, 0.375, 0.25, 0.25, 0.25, 0.375 }, + MONK = { 0.25, 0.25, 0.25, 0.375, 0.375, 0.25, 0.375, 0.375 }, + DEMONHUNTER = { 0.375, 0.25, 0.375, 0.375, 0.5, 0.25, 0.5, 0.375 }, +} + +S.CLASS_FULL_COORDS = { + WARRIOR = { 0, 0.125, 0, 0.125 }, + MAGE = { 0.125, 0.25, 0, 0.125 }, + ROGUE = { 0.25, 0.375, 0, 0.125 }, + DRUID = { 0.375, 0.5, 0, 0.125 }, + EVOKER = { 0.5, 0.625, 0, 0.125 }, + HUNTER = { 0, 0.125, 0.125, 0.25 }, + SHAMAN = { 0.125, 0.25, 0.125, 0.25 }, + PRIEST = { 0.25, 0.375, 0.125, 0.25 }, + WARLOCK = { 0.375, 0.5, 0.125, 0.25 }, + PALADIN = { 0, 0.125, 0.25, 0.375 }, + DEATHKNIGHT = { 0.125, 0.25, 0.25, 0.375 }, + MONK = { 0.25, 0.375, 0.25, 0.375 }, + DEMONHUNTER = { 0.375, 0.5, 0.25, 0.375 }, +} + +S.CLASS_FULL_BASE = 'Interface\\AddOns\\EllesmereUI\\media\\icons\\class-full\\' + +function S.ResolveClassIcon(style, classFilename) + if not classFilename or style == 'none' then return nil, nil end + if not style or style == 'fabled' then + local coords = S.CLASS_ICON_COORDS[classFilename] + return S.CLASS_ICONS, coords + end + local coords = S.CLASS_FULL_COORDS[classFilename] + return S.CLASS_FULL_BASE .. style .. '.tga', coords +end + +function S.ApplyClassIcon(icon, style, classFilename) + local tex, coords = S.ResolveClassIcon(style, classFilename) + if not tex or not coords then + icon:Hide() + return + end + icon:SetTexture(tex) + if style == 'fabled' or not style then + icon:SetTexCoord(unpack(coords)) + else + icon:SetTexCoord(coords[1], coords[2], coords[3], coords[4]) + end + icon:Show() +end + +S.SESSION_LABELS = { + [Enum.DamageMeterSessionType.Current] = "Current", + [Enum.DamageMeterSessionType.Overall] = "Overall", +} + +StaticPopupDialogs['EUIDM_RESET'] = { + text = "Reset all damage meter data?", + button1 = ACCEPT, + button2 = CANCEL, + OnAccept = function() + C_DamageMeter.ResetAllCombatSessions() + addon:RefreshMeter() + end, + timeout = 0, + whileDead = true, + hideOnEscape = true, +} + +S.windows = {} +S.testMode = false +S.meterHidden = false +S.meterFadedOut = false +S.flightTicker = nil +S.flightFadeTimer = nil + +S.nameCache = {} +S.classCache = {} +S.specNameCache = {} +S.specCollisions = {} +S.spellCache = {} +S.winDBCache = {} +S.sessionLabelCache = {} + +local strsplit = strsplit +local IsInGroup = IsInGroup +local IsInRaid = IsInRaid +local GetNumGroupMembers = GetNumGroupMembers +local UnitGUID = UnitGUID +local floor = math.floor + +function S.ScanRoster() + local pg = UnitGUID('player') + if pg then + S.nameCache[pg] = UnitName('player') + S.classCache[pg] = select(2, UnitClass('player')) + end + if IsInRaid() then + for i = 1, GetNumGroupMembers() do + local unit = 'raid' .. i + local guid = UnitGUID(unit) + if guid then + S.nameCache[guid] = UnitName(unit) + S.classCache[guid] = select(2, UnitClass(unit)) + end + end + elseif IsInGroup() then + for i = 1, GetNumGroupMembers() - 1 do + local unit = 'party' .. i + local guid = UnitGUID(unit) + if guid then + S.nameCache[guid] = UnitName(unit) + S.classCache[guid] = select(2, UnitClass(unit)) + end + end + end +end + +function S.IsSecret(val) + return val ~= nil and issecretvalue and issecretvalue(val) +end + +function S.FindUnitByGUID(guid) + if UnitGUID('player') == guid then return 'player' end + for i = 1, 40 do + local unit = 'raid' .. i + if UnitGUID(unit) == guid then return unit end + end + for i = 1, 4 do + local unit = 'party' .. i + if UnitGUID(unit) == guid then return unit end + end +end + +function S.RoundIfPlain(val) + if val and not S.IsSecret(val) then + return floor(val + 0.5) + end + return val +end + +-- Strip decimals from sub-1K abbreviated strings (e.g. "209.385" -> "209") +function S.TruncateDecimals(text) + if type(text) ~= 'string' or S.IsSecret(text) then return text end + if text:match('%a') then return text end + return (strsplit('.', text)) +end + +function S.FormatValueText(fontString, val) + if not val then + fontString:SetText('0') + return + end + fontString:SetText(S.TruncateDecimals(AbbreviateNumbers(S.RoundIfPlain(val)))) +end + +function S.FormatCombinedText(fontString, total, perSec) + if not total and not perSec then + fontString:SetText('0') + return + end + local ok = pcall(function() + local p = S.TruncateDecimals(perSec and AbbreviateNumbers(S.RoundIfPlain(perSec)) or '0') + local t = S.TruncateDecimals(total and AbbreviateNumbers(S.RoundIfPlain(total)) or '0') + fontString:SetText(p .. ' (' .. t .. ')') + end) + if not ok then + if total then + fontString:SetText(S.TruncateDecimals(AbbreviateNumbers(S.RoundIfPlain(total)))) + else + fontString:SetText('0') + end + end +end + +function S.FontFlags(outline) + return (outline and outline ~= "NONE") and outline or "" +end + +function S.GetWinDB(winIndex) + local mainDB = addon.db.profile + if winIndex == 1 then return mainDB end + local proxy = S.winDBCache[winIndex] + if not proxy then + proxy = setmetatable({}, { __index = function(_, k) + local ew = addon.db.profile.extraWindows and addon.db.profile.extraWindows[winIndex] + if ew then + local v = ew[k] + if v ~= nil then return v end + end + return addon.db.profile[k] + end }) + S.winDBCache[winIndex] = proxy + end + return proxy +end + +local cachedClassR, cachedClassG, cachedClassB, cachedClassName + +local function CacheClassColor(classFilename) + if classFilename == cachedClassName then return end + cachedClassName = classFilename + cachedClassR, cachedClassG, cachedClassB = GetClassColor(classFilename) +end + +function S.ClassOrColor(db, flagKey, colorKey, classFilename) + if db[flagKey] then + CacheClassColor(classFilename) + if cachedClassR then return cachedClassR, cachedClassG, cachedClassB, db[colorKey].a end + end + local c = db[colorKey] + return c.r, c.g, c.b, c.a +end + +function S.StyleBarTexts(bar, fontPath, size, flags) + S.ApplyFont(bar.leftText, fontPath, size, flags) + S.ApplyFont(bar.rightText, fontPath, size, flags) + S.ApplyFont(bar.pctText, fontPath, size, flags) +end + +function S.NewWindowState(index, savedModeIndex) + return { + index = index, + frame = nil, + header = nil, + window = nil, + bars = {}, + modeIndex = savedModeIndex or 1, + sessionType = Enum.DamageMeterSessionType.Current, + sessionId = nil, + embedded = false, + scrollOffset = 0, + drillSource = nil, + } +end + +function S.GetSession(win, meterType) + if win.sessionId and C_DamageMeter.GetCombatSessionFromID then + return C_DamageMeter.GetCombatSessionFromID(win.sessionId, meterType) + end + return C_DamageMeter.GetCombatSessionFromType(win.sessionType, meterType) +end + +function S.GetSessionSource(win, meterType, guid) + if win.sessionId and C_DamageMeter.GetCombatSessionSourceFromID then + return C_DamageMeter.GetCombatSessionSourceFromID(win.sessionId, meterType, guid) + end + return C_DamageMeter.GetCombatSessionSourceFromType(win.sessionType, meterType, guid) +end + +function S.GetSessionLabel(win) + if win.sessionId then + local cached = S.sessionLabelCache[win.sessionId] + if cached then return cached end + if C_DamageMeter.GetAvailableCombatSessions then + local sessions = C_DamageMeter.GetAvailableCombatSessions() + if sessions then + for i, sess in ipairs(sessions) do + local sid = sess.sessionId or sess.combatSessionId or sess.id or sess.sessionID + if sid == win.sessionId then + local label = sess.name or 'Encounter' + if label == 'Encounter' then label = 'Encounter ' .. i end + S.sessionLabelCache[win.sessionId] = label + return label + end + end + end + end + return 'Encounter' + end + return S.SESSION_LABELS[win.sessionType] or '?' +end + +local defaults = { + profile = { + enabled = true, barHeight = 18, barSpacing = 1, + barFont = 'Expressway', barFontSize = 11, barFontOutline = 'OUTLINE', + barTexture = 'none', barBGTexture = 'none', + barColor = { r = 0.3, g = 0.3, b = 0.8, a = 1 }, + barBGColor = { r = 0.15, g = 0.15, b = 0.15, a = 0.35 }, + barClassColor = true, barBGClassColor = false, + textColor = { r = 1, g = 1, b = 1 }, textClassColor = false, + valueColor = { r = 1, g = 1, b = 1 }, valueClassColor = false, + rankColor = { r = 0.7, g = 0.7, b = 0.7 }, rankClassColor = false, + showRank = false, showClassIcon = true, classIconStyle = 'fabled', + showBackdrop = true, backdropColor = { r = 0.1, g = 0.1, b = 0.1, a = 0.8 }, + showHeaderBackdrop = true, showHeaderBorder = true, + headerBGColor = { r = 0.15, g = 0.15, b = 0.15, a = 0.8 }, + headerFont = 'Expressway', headerFontSize = 11, headerFontOutline = 'OUTLINE', + headerFontColor = { r = 1, g = 1, b = 1 }, headerMouseover = false, + showTimer = true, barBorderEnabled = false, + standaloneWidth = 220, standaloneHeight = 180, + modeIndex = 1, hideInFlight = false, hideInPetBattle = false, + autoResetOnComplete = false, + } +} +addon.defaults = defaults +addon.db = EllesmereUI.Lite.NewDB('EllesmereUIDamageMeterDB', defaults) + +EllesmereUIDamageMeter = addon diff --git a/EllesmereUIDamageMeter/media/fabled.tga b/EllesmereUIDamageMeter/media/fabled.tga new file mode 100644 index 00000000..40d34f02 Binary files /dev/null and b/EllesmereUIDamageMeter/media/fabled.tga differ diff --git a/EllesmereUIDamageMeter/options.lua b/EllesmereUIDamageMeter/options.lua new file mode 100644 index 00000000..5b355291 --- /dev/null +++ b/EllesmereUIDamageMeter/options.lua @@ -0,0 +1,453 @@ +if EllesmereUI and EllesmereUI.ADDON_ROSTER then + local found = false + for _, info in ipairs(EllesmereUI.ADDON_ROSTER) do + if info.folder == 'EllesmereUIDamageMeter' then found = true; break end + end + if not found then + local insertIdx = #EllesmereUI.ADDON_ROSTER + 1 + for i, info in ipairs(EllesmereUI.ADDON_ROSTER) do + if info.folder == 'EllesmereUIPartyMode' or info.comingSoon then + insertIdx = i; break + end + end + table.insert(EllesmereUI.ADDON_ROSTER, insertIdx, { + folder = 'EllesmereUIDamageMeter', + display = 'Damage Meter', + search_name = 'EllesmereUI Damage Meter', + icon_on = 'Interface\\Icons\\INV_Misc_Map_01', + icon_off = 'Interface\\Icons\\INV_Misc_Map_01', + }) + end +end + +local function InitOptions() + if not EllesmereUI or not EllesmereUI.RegisterModule then return end + local addon = EllesmereUIDamageMeter + if not addon or not addon.db then return end + + local function DB() return addon.db.profile end + + local S = addon.S + local CLASS_FULL_SPRITE_BASE = S.CLASS_FULL_BASE + local CLASS_FULL_COORDS = S.CLASS_FULL_COORDS + local FLAT_TEX = S.FLAT_TEX + local texTextures = S.texTextures + local texDropdownValues = S.texDropdownValues + local texDropdownOrder = S.texDropdownOrder + + local classIconValues = { + ['fabled'] = 'Fabled (TUI)', + ['modern'] = 'Modern', ['arcade'] = 'Arcade', ['glyph'] = 'Glyph', + ['legend'] = 'Legend', ['midnight'] = 'Midnight', ['pixel'] = 'Pixel', ['runic'] = 'Runic', + ['none'] = 'None', + } + local classIconOrder = { 'fabled', 'modern', 'arcade', 'glyph', 'legend', 'midnight', 'pixel', 'runic', 'none' } + + local previewData = { + { name = 'Trenchy', class = 'DEATHKNIGHT', pct = 0.85 }, + { name = 'Healbro', class = 'PRIEST', pct = 0.62 }, + { name = 'Stabsworth', class = 'ROGUE', pct = 0.45 }, + } + + local previewFrame + + local function UpdatePreview() + if not previewFrame or not previewFrame.Update then return end + previewFrame:Update() + end + + local function Refresh() + if addon.UpdateMeterLayout then addon:UpdateMeterLayout() end + if addon.RefreshMeter then addon:RefreshMeter() end + UpdatePreview() + end + + local function BuildPreview(parent, yOffset) + local pf = CreateFrame('Frame', nil, parent) + local previewW = 240 + local previewH = 120 + pf:SetSize(previewW, previewH) + pf:SetPoint('TOP', parent, 'TOP', 0, yOffset - 6) + + local previewScale = UIParent:GetEffectiveScale() / parent:GetEffectiveScale() + pf:SetScale(previewScale) + + -- Background + local bg = pf:CreateTexture(nil, 'BACKGROUND') + bg:SetAllPoints() + bg:SetColorTexture(0.1, 0.1, 0.1, 0.8) + + -- Header + local header = CreateFrame('Frame', nil, pf) + header:SetPoint('TOPLEFT', pf, 'TOPLEFT', 0, 0) + header:SetPoint('TOPRIGHT', pf, 'TOPRIGHT', 0, 0) + header:SetHeight(22) + local headerBg = header:CreateTexture(nil, 'BACKGROUND') + headerBg:SetAllPoints() + headerBg:SetColorTexture(0.15, 0.15, 0.15, 0.8) + local headerText = header:CreateFontString(nil, 'OVERLAY') + headerText:SetPoint('LEFT', 4, 0) + headerText:SetFont(EllesmereUI.EXPRESSWAY or 'Fonts\\FRIZQT__.TTF', 12, 'OUTLINE') + headerText:SetText('Damage') + headerText:SetShadowOffset(1, -1) + + local bars = {} + local LSM = LibStub('LibSharedMedia-3.0') + for i, data in ipairs(previewData) do + local bar = CreateFrame('Frame', nil, pf, 'BackdropTemplate') + bar.statusbar = CreateFrame('StatusBar', nil, bar) + bar.statusbar:SetAllPoints() + bar.statusbar:SetMinMaxValues(0, 1) + bar.statusbar:SetValue(data.pct) + bar.bg = bar:CreateTexture(nil, 'BACKGROUND') + bar.bg:SetAllPoints() + bar.icon = bar.statusbar:CreateTexture(nil, 'OVERLAY') + bar.icon:SetPoint('LEFT', 1, 0) + bar.leftText = bar.statusbar:CreateFontString(nil, 'OVERLAY') + bar.leftText:SetJustifyH('LEFT') + bar.leftText:SetShadowOffset(1, -1) + bar.rightText = bar.statusbar:CreateFontString(nil, 'OVERLAY') + bar.rightText:SetPoint('RIGHT', -4, 0) + bar.rightText:SetJustifyH('RIGHT') + bar.rightText:SetShadowOffset(1, -1) + bar.data = data + bars[i] = bar + end + pf._bars = bars + pf._header = header + pf._headerBg = headerBg + pf._headerText = headerText + pf._bg = bg + + function pf:Update() + local db = DB() + if not db then return end + + local barH = db.barHeight or 18 + local spacing = db.barSpacing or 1 + local fFont = EllesmereUI.EXPRESSWAY or 'Fonts\\FRIZQT__.TTF' + if db.barFont then + local fetched = LSM:Fetch('font', db.barFont) + if fetched then fFont = fetched end + end + local fSize = db.barFontSize or 11 + local fFlags = (db.barFontOutline and db.barFontOutline ~= 'NONE') and db.barFontOutline or '' + + local resolveTex = EllesmereUI.ResolveTexturePath + local fgTex = resolveTex and resolveTex(texTextures, db.barTexture, FLAT_TEX) or FLAT_TEX + local bgTex = resolveTex and resolveTex(texTextures, db.barBGTexture, FLAT_TEX) or FLAT_TEX + + if db.showHeaderBackdrop then + local hc = db.headerBGColor + self._headerBg:SetColorTexture(hc.r, hc.g, hc.b, hc.a) + self._headerBg:Show() + else + self._headerBg:Hide() + end + + if db.showBackdrop then + local bc = db.backdropColor + self._bg:SetColorTexture(bc.r, bc.g, bc.b, bc.a) + self._bg:Show() + else + self._bg:Hide() + end + + local iconStyle = db.classIconStyle or 'fabled' + local iconSize = math.max(8, barH - 2) + local showIcons = db.showClassIcon + + for i, bar in ipairs(self._bars) do + local data = bar.data + bar:SetHeight(barH) + bar:ClearAllPoints() + if i == 1 then + bar:SetPoint('TOPLEFT', self._header, 'BOTTOMLEFT', 0, 0) + bar:SetPoint('TOPRIGHT', self._header, 'BOTTOMRIGHT', 0, 0) + else + bar:SetPoint('TOPLEFT', self._bars[i-1], 'BOTTOMLEFT', 0, -spacing) + bar:SetPoint('TOPRIGHT', self._bars[i-1], 'BOTTOMRIGHT', 0, -spacing) + end + bar:Show() + + bar.statusbar:SetStatusBarTexture(fgTex) + bar.statusbar:SetValue(data.pct) + bar.bg:SetTexture(bgTex) + + local cc = EllesmereUI.GetClassColor(data.class) + if db.barClassColor and cc then + bar.statusbar:SetStatusBarColor(cc.r, cc.g, cc.b) + else + local c = db.barColor + bar.statusbar:SetStatusBarColor(c.r, c.g, c.b, c.a) + end + + if db.barBGClassColor and cc then + bar.bg:SetVertexColor(cc.r, cc.g, cc.b, db.barBGColor.a) + else + local c = db.barBGColor + bar.bg:SetVertexColor(c.r, c.g, c.b, c.a) + end + + bar.icon:SetSize(iconSize, iconSize) + if showIcons and iconStyle ~= 'none' then + if iconStyle == 'fabled' then + local S = addon.S + bar.icon:SetTexture(S and S.CLASS_ICONS or '') + local coords = S and S.CLASS_ICON_COORDS and S.CLASS_ICON_COORDS[data.class] + if coords then + bar.icon:SetTexCoord(unpack(coords)) + end + else + bar.icon:SetTexture(CLASS_FULL_SPRITE_BASE .. iconStyle .. '.tga') + local coords = CLASS_FULL_COORDS[data.class] + if coords then + bar.icon:SetTexCoord(coords[1], coords[2], coords[3], coords[4]) + end + end + bar.icon:Show() + else + bar.icon:Hide() + end + + bar.leftText:SetFont(fFont, fSize, fFlags) + bar.rightText:SetFont(fFont, fSize, fFlags) + bar.leftText:ClearAllPoints() + if showIcons and iconStyle ~= 'none' then + bar.leftText:SetPoint('LEFT', bar.icon, 'RIGHT', 2, 0) + else + bar.leftText:SetPoint('LEFT', 4, 0) + end + bar.leftText:SetPoint('RIGHT', bar.rightText, 'LEFT', -4, 0) + + bar.leftText:SetText(data.name) + bar.rightText:SetText(math.floor(data.pct * 100000)) + + if db.textClassColor and cc then + bar.leftText:SetTextColor(cc.r, cc.g, cc.b) + else + local c = db.textColor + bar.leftText:SetTextColor(c.r, c.g, c.b) + end + if db.valueClassColor and cc then + bar.rightText:SetTextColor(cc.r, cc.g, cc.b) + else + local c = db.valueColor + bar.rightText:SetTextColor(c.r, c.g, c.b) + end + end + + local totalH = 22 + (#self._bars * barH) + ((#self._bars - 1) * spacing) + self:SetHeight(totalH) + end + + previewFrame = pf + pf:Update() + + return (previewH + 16) / previewScale + end + + local function BuildGeneralPage(_, parent, yOffset) + local W = EllesmereUI.Widgets + local wrapper = CreateFrame('Frame', nil, parent) + wrapper:SetAllPoints() + + local h = 0 + + local previewH = BuildPreview(wrapper, yOffset - h) + h = h + previewH + 10 + + local _, rh + _, rh = W:SectionHeader(wrapper, 'General', yOffset - h); h = h + rh + + _, rh = W:DualRow(wrapper, yOffset - h, + { type = 'toggle', text = 'Enable', + getValue = function() return DB().enabled end, + setValue = function(v) DB().enabled = v; Refresh() end }, + { type = 'toggle', text = 'Test Mode', + getValue = function() return addon._meterTestMode end, + setValue = function(v) addon:SetMeterTestMode(v) end } + ); h = h + rh + + _, rh = W:DualRow(wrapper, yOffset - h, + { type = 'toggle', text = 'Show Timer', + getValue = function() return DB().showTimer end, + setValue = function(v) DB().showTimer = v; Refresh() end }, + { type = 'toggle', text = 'Show Rank', + getValue = function() return DB().showRank end, + setValue = function(v) DB().showRank = v; Refresh() end } + ); h = h + rh + + _, rh = W:DualRow(wrapper, yOffset - h, + { type = 'toggle', text = 'Show Class Icon', + getValue = function() return DB().showClassIcon end, + setValue = function(v) DB().showClassIcon = v; Refresh() end }, + { type = 'dropdown', text = 'Icon Style', values = classIconValues, order = classIconOrder, + getValue = function() return DB().classIconStyle or 'fabled' end, + setValue = function(v) DB().classIconStyle = v; Refresh() end, + disabled = function() return not DB().showClassIcon end } + ); h = h + rh + + _, rh = W:DualRow(wrapper, yOffset - h, + { type = 'toggle', text = 'Show Backdrop', + getValue = function() return DB().showBackdrop end, + setValue = function(v) DB().showBackdrop = v; Refresh() end }, + { type = 'toggle', text = 'Bar Border', + getValue = function() return DB().barBorderEnabled end, + setValue = function(v) DB().barBorderEnabled = v; Refresh() end } + ); h = h + rh + + _, rh = W:DualRow(wrapper, yOffset - h, + { type = 'toggle', text = 'Header Backdrop', + getValue = function() return DB().showHeaderBackdrop end, + setValue = function(v) DB().showHeaderBackdrop = v; Refresh() end }, + { type = 'toggle', text = 'Header Border', + getValue = function() return DB().showHeaderBorder end, + setValue = function(v) DB().showHeaderBorder = v; Refresh() end } + ); h = h + rh + + _, rh = W:DualRow(wrapper, yOffset - h, + { type = 'toggle', text = 'Header Mouseover', + getValue = function() return DB().headerMouseover end, + setValue = function(v) DB().headerMouseover = v; Refresh() end }, + { type = 'toggle', text = 'Hide in Flight', + getValue = function() return DB().hideInFlight end, + setValue = function(v) DB().hideInFlight = v; if addon.UpdateFlightTicker then addon:UpdateFlightTicker() end end } + ); h = h + rh + + _, rh = W:DualRow(wrapper, yOffset - h, + { type = 'toggle', text = 'Hide in Pet Battle', + getValue = function() return DB().hideInPetBattle end, + setValue = function(v) DB().hideInPetBattle = v end }, + { type = 'toggle', text = 'Auto-Reset on Instance', + getValue = function() return DB().autoResetOnComplete end, + setValue = function(v) DB().autoResetOnComplete = v end } + ); h = h + rh + + _, rh = W:SectionHeader(wrapper, 'Size', yOffset - h); h = h + rh + + _, rh = W:DualRow(wrapper, yOffset - h, + { type = 'slider', text = 'Width', min = 120, max = 500, step = 1, + getValue = function() return DB().standaloneWidth end, + setValue = function(v) DB().standaloneWidth = v; if addon.ResizeMeterWindow then addon:ResizeMeterWindow(1) end; Refresh() end }, + { type = 'slider', text = 'Height', min = 80, max = 500, step = 1, + getValue = function() return DB().standaloneHeight end, + setValue = function(v) DB().standaloneHeight = v; if addon.ResizeMeterWindow then addon:ResizeMeterWindow(1) end; Refresh() end } + ); h = h + rh + + _, rh = W:DualRow(wrapper, yOffset - h, + { type = 'slider', text = 'Bar Height', min = 8, max = 40, step = 1, + getValue = function() return DB().barHeight end, + setValue = function(v) DB().barHeight = v; Refresh() end }, + { type = 'slider', text = 'Bar Spacing', min = 0, max = 10, step = 1, + getValue = function() return DB().barSpacing end, + setValue = function(v) DB().barSpacing = v; Refresh() end } + ); h = h + rh + + _, rh = W:SectionHeader(wrapper, 'Textures', yOffset - h); h = h + rh + + _, rh = W:DualRow(wrapper, yOffset - h, + { type = 'dropdown', text = 'Bar Texture', values = texDropdownValues, order = texDropdownOrder, + getValue = function() return DB().barTexture or 'none' end, + setValue = function(v) DB().barTexture = v; Refresh() end }, + { type = 'dropdown', text = 'Bar BG Texture', values = texDropdownValues, order = texDropdownOrder, + getValue = function() return DB().barBGTexture or 'none' end, + setValue = function(v) DB().barBGTexture = v; Refresh() end } + ); h = h + rh + + _, rh = W:SectionHeader(wrapper, 'Font', yOffset - h); h = h + rh + + _, rh = W:DualRow(wrapper, yOffset - h, + { type = 'slider', text = 'Bar Font Size', min = 6, max = 24, step = 1, + getValue = function() return DB().barFontSize end, + setValue = function(v) DB().barFontSize = v; Refresh() end }, + { type = 'slider', text = 'Header Font Size', min = 6, max = 24, step = 1, + getValue = function() return DB().headerFontSize end, + setValue = function(v) DB().headerFontSize = v; Refresh() end } + ); h = h + rh + + _, rh = W:SectionHeader(wrapper, 'Colors', yOffset - h); h = h + rh + + _, rh = W:DualRow(wrapper, yOffset - h, + { type = 'toggle', text = 'Class Color Bars', + getValue = function() return DB().barClassColor end, + setValue = function(v) DB().barClassColor = v; Refresh() end }, + { type = 'multiSwatch', text = 'Bar Color', swatches = { + { tooltip = 'Bar Color', + getValue = function() local c = DB().barColor; return c.r, c.g, c.b, c.a end, + setValue = function(r, g, b, a) local c = DB().barColor; c.r, c.g, c.b, c.a = r, g, b, a; Refresh() end, + hasAlpha = true }, + }, disabled = function() return DB().barClassColor end } + ); h = h + rh + + _, rh = W:DualRow(wrapper, yOffset - h, + { type = 'toggle', text = 'Class Color Text', + getValue = function() return DB().textClassColor end, + setValue = function(v) DB().textClassColor = v; Refresh() end }, + { type = 'multiSwatch', text = 'Text Color', swatches = { + { tooltip = 'Text Color', + getValue = function() local c = DB().textColor; return c.r, c.g, c.b end, + setValue = function(r, g, b) local c = DB().textColor; c.r, c.g, c.b = r, g, b; Refresh() end }, + }, disabled = function() return DB().textClassColor end } + ); h = h + rh + + _, rh = W:DualRow(wrapper, yOffset - h, + { type = 'toggle', text = 'Class Color Values', + getValue = function() return DB().valueClassColor end, + setValue = function(v) DB().valueClassColor = v; Refresh() end }, + { type = 'multiSwatch', text = 'Value Color', swatches = { + { tooltip = 'Value Color', + getValue = function() local c = DB().valueColor; return c.r, c.g, c.b end, + setValue = function(r, g, b) local c = DB().valueColor; c.r, c.g, c.b = r, g, b; Refresh() end }, + }, disabled = function() return DB().valueClassColor end } + ); h = h + rh + + _, rh = W:DualRow(wrapper, yOffset - h, + { type = 'toggle', text = 'Class Color BG', + getValue = function() return DB().barBGClassColor end, + setValue = function(v) DB().barBGClassColor = v; Refresh() end }, + { type = 'multiSwatch', text = 'BG Color', swatches = { + { tooltip = 'Background Color', + getValue = function() local c = DB().barBGColor; return c.r, c.g, c.b, c.a end, + setValue = function(r, g, b, a) local c = DB().barBGColor; c.r, c.g, c.b, c.a = r, g, b, a; Refresh() end, + hasAlpha = true }, + }, disabled = function() return DB().barBGClassColor end } + ); h = h + rh + + return h + end + + EllesmereUI:RegisterModule('EllesmereUIDamageMeter', { + title = 'Damage Meter', + description = 'Damage meter', + pages = { 'General' }, + buildPage = function(pageName, parent, yOffset) + if pageName == 'General' then + return BuildGeneralPage(pageName, parent, yOffset) + end + end, + onPageCacheRestore = function() + UpdatePreview() + end, + onReset = function() + if addon.db and addon.db.ResetProfile then + addon.db:ResetProfile() + end + wipe(S.winDBCache) + Refresh() + end, + }) + + EllesmereUI:RegisterOnShow(UpdatePreview) +end + +if IsLoggedIn() then + InitOptions() +else + local initFrame = CreateFrame('Frame') + initFrame:RegisterEvent('PLAYER_LOGIN') + initFrame:SetScript('OnEvent', function(self) + self:UnregisterEvent('PLAYER_LOGIN') + InitOptions() + end) +end diff --git a/EllesmereUIDamageMeter/testdata.lua b/EllesmereUIDamageMeter/testdata.lua new file mode 100644 index 00000000..d9144e85 --- /dev/null +++ b/EllesmereUIDamageMeter/testdata.lua @@ -0,0 +1,130 @@ +local addon = EllesmereUIDamageMeter +local S = addon and addon.S +if not S then return end + +local TEST_DAMAGE = { + { name = "Deathknight", value = 980000, class = "DEATHKNIGHT", + spells = {{49020, 340000}, {49143, 280000}, {49184, 195000}, {196770, 110000}, {6603, 55000}} }, + { name = "Demonhunter", value = 920000, class = "DEMONHUNTER", + spells = {{198013, 310000}, {188499, 260000}, {162794, 200000}, {258920, 100000}, {6603, 50000}} }, + { name = "Warrior", value = 860000, class = "WARRIOR", + spells = {{12294, 290000}, {163201, 240000}, {7384, 180000}, {262115, 100000}, {6603, 50000}} }, + { name = "Mage", value = 800000, class = "MAGE", + spells = {{11366, 280000}, {108853, 220000}, {133, 170000}, {12654, 90000}, {257541, 40000}} }, + { name = "Hunter", value = 740000, class = "HUNTER", + spells = {{19434, 260000}, {257044, 200000}, {185358, 150000}, {75, 90000}, {53351, 40000}} }, + { name = "Rogue", value = 680000, class = "ROGUE", + spells = {{196819, 240000}, {1752, 190000}, {315341, 140000}, {13877, 75000}, {6603, 35000}} }, + { name = "Warlock", value = 620000, class = "WARLOCK", + spells = {{116858, 220000}, {29722, 170000}, {348, 120000}, {5740, 70000}, {17962, 40000}} }, + { name = "Evoker", value = 560000, class = "EVOKER", + spells = {{357208, 200000}, {356995, 160000}, {361469, 110000}, {357211, 60000}, {362969, 30000}} }, + { name = "Shaman", value = 500000, class = "SHAMAN", + spells = {{188196, 180000}, {51505, 140000}, {188443, 100000}, {188389, 55000}, {8042, 25000}} }, + { name = "Paladin", value = 440000, class = "PALADIN", + spells = {{85256, 160000}, {184575, 120000}, {255937, 90000}, {26573, 45000}, {6603, 25000}} }, + { name = "Monk", value = 380000, class = "MONK", + spells = {{107428, 140000}, {100784, 105000}, {113656, 80000}, {100780, 35000}, {101546, 20000}} }, + { name = "Druid", value = 320000, class = "DRUID", + spells = {{78674, 120000}, {194153, 90000}, {190984, 60000}, {93402, 35000}, {8921, 15000}} }, + { name = "Priest", value = 260000, class = "PRIEST", + spells = {{8092, 100000}, {32379, 70000}, {34914, 45000}, {589, 30000}, {15407, 15000}} }, + { name = "Deathknight2", value = 210000, class = "DEATHKNIGHT", + spells = {{49020, 80000}, {49143, 60000}, {49184, 40000}, {6603, 30000}} }, + { name = "Mage2", value = 170000, class = "MAGE", + spells = {{11366, 65000}, {133, 50000}, {108853, 35000}, {12654, 20000}} }, + { name = "Hunter2", value = 135000, class = "HUNTER", + spells = {{19434, 55000}, {257044, 40000}, {185358, 25000}, {75, 15000}} }, + { name = "Warrior2", value = 105000, class = "WARRIOR", + spells = {{12294, 45000}, {163201, 30000}, {7384, 20000}, {6603, 10000}} }, + { name = "Rogue2", value = 80000, class = "ROGUE", + spells = {{196819, 35000}, {1752, 25000}, {6603, 20000}} }, + { name = "Shaman2", value = 58000, class = "SHAMAN", + spells = {{188196, 25000}, {51505, 18000}, {188389, 15000}} }, + { name = "Paladin2", value = 40000, class = "PALADIN", + spells = {{85256, 18000}, {184575, 12000}, {6603, 10000}} }, +} + +local TEST_HEALING = { + { name = "Priest", value = 1250000, class = "PRIEST", + spells = {{2061, 420000}, {34861, 310000}, {596, 240000}, {139, 180000}, {47788, 100000}} }, + { name = "Druid", value = 1100000, class = "DRUID", + spells = {{774, 380000}, {48438, 290000}, {8936, 210000}, {33763, 140000}, {145205, 80000}} }, + { name = "Paladin", value = 980000, class = "PALADIN", + spells = {{19750, 340000}, {82326, 260000}, {20473, 190000}, {85222, 120000}, {53563, 70000}} }, + { name = "Shaman", value = 870000, class = "SHAMAN", + spells = {{77472, 300000}, {1064, 230000}, {61295, 170000}, {73920, 110000}, {5394, 60000}} }, + { name = "Monk", value = 760000, class = "MONK", + spells = {{115175, 260000}, {191837, 200000}, {116670, 150000}, {124682, 100000}, {115310, 50000}} }, + { name = "Evoker", value = 650000, class = "EVOKER", + spells = {{355916, 230000}, {364343, 170000}, {361469, 120000}, {382614, 80000}, {355913, 50000}} }, + { name = "Priest2", value = 540000, class = "PRIEST", + spells = {{2061, 200000}, {34861, 150000}, {596, 100000}, {139, 90000}} }, + { name = "Druid2", value = 430000, class = "DRUID", + spells = {{774, 160000}, {48438, 120000}, {8936, 90000}, {33763, 60000}} }, + { name = "Paladin2", value = 320000, class = "PALADIN", + spells = {{19750, 130000}, {82326, 100000}, {20473, 90000}} }, + { name = "Shaman2", value = 210000, class = "SHAMAN", + spells = {{77472, 90000}, {1064, 60000}, {61295, 60000}} }, +} + +local TEST_INTERRUPTS = { + { name = "Rogue", value = 8, class = "ROGUE", spells = {{1766, 8}} }, + { name = "Shaman", value = 7, class = "SHAMAN", spells = {{57994, 7}} }, + { name = "Deathknight", value = 6, class = "DEATHKNIGHT", spells = {{47528, 6}} }, + { name = "Mage", value = 5, class = "MAGE", spells = {{2139, 5}} }, + { name = "Demonhunter", value = 5, class = "DEMONHUNTER", spells = {{183752, 5}} }, + { name = "Warrior", value = 4, class = "WARRIOR", spells = {{6552, 4}} }, + { name = "Hunter", value = 3, class = "HUNTER", spells = {{147362, 3}} }, + { name = "Monk", value = 3, class = "MONK", spells = {{116705, 3}} }, + { name = "Paladin", value = 2, class = "PALADIN", spells = {{96231, 2}} }, + { name = "Warlock", value = 2, class = "WARLOCK", spells = {{119910, 2}} }, + { name = "Evoker", value = 1, class = "EVOKER", spells = {{351338, 1}} }, + { name = "Druid", value = 1, class = "DRUID", spells = {{106839, 1}} }, + { name = "Priest", value = 0, class = "PRIEST", spells = {} }, +} + +local TEST_DISPELS = { + { name = "Priest", value = 12, class = "PRIEST", spells = {{527, 8}, {32375, 4}} }, + { name = "Paladin", value = 9, class = "PALADIN", spells = {{4987, 9}} }, + { name = "Shaman", value = 7, class = "SHAMAN", spells = {{51886, 7}} }, + { name = "Druid", value = 6, class = "DRUID", spells = {{2782, 6}} }, + { name = "Monk", value = 5, class = "MONK", spells = {{115450, 5}} }, + { name = "Evoker", value = 4, class = "EVOKER", spells = {{365585, 4}} }, + { name = "Mage", value = 3, class = "MAGE", spells = {{475, 3}} }, + { name = "Warlock", value = 2, class = "WARLOCK", spells = {{89808, 2}} }, + { name = "Hunter", value = 1, class = "HUNTER", spells = {{19801, 1}} }, +} + +local TEST_DEATHS = { + { name = "Rogue", value = 4, class = "ROGUE", spells = {} }, + { name = "Mage", value = 3, class = "MAGE", spells = {} }, + { name = "Hunter", value = 3, class = "HUNTER", spells = {} }, + { name = "Warlock", value = 2, class = "WARLOCK", spells = {} }, + { name = "Priest", value = 2, class = "PRIEST", spells = {} }, + { name = "Demonhunter", value = 1, class = "DEMONHUNTER", spells = {} }, + { name = "Warrior", value = 1, class = "WARRIOR", spells = {} }, + { name = "Evoker", value = 1, class = "EVOKER", spells = {} }, + { name = "Deathknight", value = 0, class = "DEATHKNIGHT", spells = {} }, + { name = "Paladin", value = 0, class = "PALADIN", spells = {} }, + { name = "Shaman", value = 0, class = "SHAMAN", spells = {} }, + { name = "Monk", value = 0, class = "MONK", spells = {} }, + { name = "Druid", value = 0, class = "DRUID", spells = {} }, +} + +function S.GetTestData(win) + local modeEntry = S.MODE_ORDER[win.modeIndex] + if modeEntry == Enum.DamageMeterType.HealingDone + or modeEntry == Enum.DamageMeterType.Hps + or modeEntry == S.COMBINED_HEALING + or modeEntry == Enum.DamageMeterType.Absorbs then + return TEST_HEALING + elseif modeEntry == Enum.DamageMeterType.Interrupts then + return TEST_INTERRUPTS + elseif modeEntry == Enum.DamageMeterType.Dispels then + return TEST_DISPELS + elseif Enum.DamageMeterType.Deaths and modeEntry == Enum.DamageMeterType.Deaths then + return TEST_DEATHS + end + return TEST_DAMAGE +end diff --git a/EllesmereUIDamageMeter/update.lua b/EllesmereUIDamageMeter/update.lua new file mode 100644 index 00000000..578c4b19 --- /dev/null +++ b/EllesmereUIDamageMeter/update.lua @@ -0,0 +1,698 @@ +local addon = EllesmereUIDamageMeter +local S = addon and addon.S +if not S then return end +local LSM = LibStub('LibSharedMedia-3.0') + +local floor = math.floor +local wipe = wipe +local format = format + +local BACKDROP_FILL = { bgFile = 'Interface\\Buttons\\WHITE8x8' } + +local GetClassColor = S.GetClassColor + +function S.RefreshWindow(win) + if not win or not win.frame or not win.header then return end + if win.emptyText then win.emptyText:Hide() end + + local db = S.GetWinDB(win.index) + + if win.drillSource then + local ds = win.drillSource + local modeEntry = S.MODE_ORDER[win.modeIndex] + local modeLabel = S.MODE_SHORT[modeEntry] or S.MODE_LABELS[modeEntry] or "?" + local sessLabel = S.GetSessionLabel(win) + + local cr, cg, cb = GetClassColor(ds.class) + local nameHex = cr and format("%02x%02x%02x", cr * 255, cg * 255, cb * 255) or "ffffff" + win.header.modeText:SetText(format("|cff%s%s|r \226\128\148 %s", nameHex, ds.name, modeLabel)) + win.header.sessText:SetText(" (" .. sessLabel .. ")") + S.ApplySessionHighlight(win, db) + win.header.timer:Hide() + + local spells + if S.testMode then + local tdata = S.GetTestData(win) + for _, td in ipairs(tdata) do + if td.name == ds.name then spells = td.spells; break end + end + else + local meterType = S.ResolveMeterType(modeEntry) + local sourceData = ds.guid and S.GetSessionSource(win, meterType, ds.guid) + spells = sourceData and sourceData.combatSpells + end + + if not spells or #spells == 0 then + for i = 1, S.MAX_BARS do + if win.bars[i] then win.bars[i].frame:Hide() end + end + return + end + + local numVisible = S.ComputeNumVisible(win) + local total = #spells + win.scrollOffset = max(0, min(win.scrollOffset, max(0, total - numVisible))) + + local topVal, totalAmt = 0, 0 + for si = 1, total do + local s = spells[si] + local amt = s.totalAmount or s[2] or 0 + if not S.IsSecret(amt) then + if amt > topVal then topVal = amt end + totalAmt = totalAmt + amt + end + end + if topVal == 0 then topVal = 1 end + if totalAmt == 0 then totalAmt = 1 end + + local fgR, fgG, fgB = S.ClassOrColor(db, 'barClassColor', 'barColor', ds.class) + local bgR, bgG, bgB, bgA = S.ClassOrColor(db, 'barBGClassColor', 'barBGColor', ds.class) + local tR, tG, tB = S.ClassOrColor(db, 'textClassColor', 'textColor', ds.class) + local vR, vG, vB = S.ClassOrColor(db, 'valueClassColor', 'valueColor', ds.class) + + for i = 1, S.MAX_BARS do + local bar = win.bars[i] + if not bar then break end + local spIdx = win.scrollOffset + i + local s = spells[spIdx] + + if i > numVisible or not s then + bar.frame:Hide() + bar.frame.drillSpellID = nil + else + bar.frame:Show() + local rawSpellID = s.spellID or (type(s[1]) == "number" and s[1]) or nil + local spellID = (rawSpellID and not S.IsSecret(rawSpellID)) and rawSpellID or nil + local spellName = (type(s[1]) == "string" and s[1]) or nil + local amt = s.totalAmount or s[2] or 0 + + local iconID + if spellID then + local cached = S.spellCache[spellID] + if cached then + spellName = cached.name or spellName + iconID = cached.icon + else + local ok, name = pcall(C_Spell.GetSpellName, spellID) + if ok and name then spellName = name end + local ok2, tex = pcall(C_Spell.GetSpellTexture, spellID) + if ok2 and tex then iconID = tex end + S.spellCache[spellID] = { name = spellName, icon = iconID } + end + end + if not spellName then spellName = "?" end + + bar.frame.drillSpellID = spellID + bar.frame.sourceGUID = nil + bar.frame.testIndex = nil + + if iconID then + bar.classIcon:SetTexture(iconID) + bar.classIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92) + bar.classIcon:Show() + else + bar.classIcon:Hide() + end + + if not bar._isDrill then + bar._isDrill = true + bar.rightText:ClearAllPoints() + bar.rightText:SetPoint("RIGHT", -64, 0) + bar.pctText:Show() + bar.leftText:ClearAllPoints() + if iconID then + bar.leftText:SetPoint("LEFT", bar.classIcon, "RIGHT", 2, 0) + else + bar.leftText:SetPoint("LEFT", 4, 0) + end + bar.leftText:SetPoint("RIGHT", bar.rightText, "LEFT", -4, 0) + elseif iconID then + if bar._drillHasIcon ~= spellID then + bar.leftText:ClearAllPoints() + bar.leftText:SetPoint("LEFT", bar.classIcon, "RIGHT", 2, 0) + bar.leftText:SetPoint("RIGHT", bar.rightText, "LEFT", -4, 0) + end + else + if bar._drillHasIcon then + bar.leftText:ClearAllPoints() + bar.leftText:SetPoint("LEFT", 4, 0) + bar.leftText:SetPoint("RIGHT", bar.rightText, "LEFT", -4, 0) + end + end + bar._drillHasIcon = iconID and spellID or nil + + bar.statusbar:SetStatusBarColor(fgR, fgG, fgB) + bar.statusbar:SetMinMaxValues(0, topVal) + bar.background:SetVertexColor(bgR, bgG, bgB, bgA) + bar.leftText:SetText(spellName) + bar.leftText:SetTextColor(tR, tG, tB) + + if S.IsSecret(amt) then + bar.statusbar:SetValue(0) + bar.rightText:SetText('?') + bar.pctText:SetText('') + else + bar.statusbar:SetValue(amt) + bar.rightText:SetText(S.TruncateDecimals(AbbreviateNumbers(S.RoundIfPlain(amt)))) + bar.pctText:SetText(totalAmt > 0 and format('%.1f%%', (amt / totalAmt) * 100) or '') + end + bar.rightText:SetTextColor(vR, vG, vB) + bar.pctText:SetTextColor(vR * 0.7, vG * 0.7, vB * 0.7) + end + end + return + end + + if S.testMode then + win.header.modeText:SetText("|cffff6600[Test Mode]|r") + win.header.sessText:SetText("") + win.header.timer:Hide() + local tdata = S.GetTestData(win) + local numVisible = S.ComputeNumVisible(win) + local maxVal = tdata[1] and tdata[1].value or 1 + local total = #tdata + win.scrollOffset = max(0, min(win.scrollOffset, max(0, total - numVisible))) + for i = 1, S.MAX_BARS do + local bar = win.bars[i] + if not bar then break end + local srcIdx = win.scrollOffset + i + local td = tdata[srcIdx] + if i > numVisible or not td then + bar.frame:Hide() + else + bar.frame:Show() + local fgR, fgG, fgB = S.ClassOrColor(db, 'barClassColor', 'barColor', td.class) + bar.statusbar:SetStatusBarColor(fgR, fgG, fgB) + bar.statusbar:SetMinMaxValues(0, maxVal) + bar.statusbar:SetValue(td.value) + local bgR, bgG, bgB, bgA = S.ClassOrColor(db, 'barBGClassColor', 'barBGColor', td.class) + bar.background:SetVertexColor(bgR, bgG, bgB, bgA) + local tR, tG, tB = S.ClassOrColor(db, 'textClassColor', 'textColor', td.class) + if db.showRank then + local rr, rg, rb = S.ClassOrColor(db, 'rankClassColor', 'rankColor', td.class) + bar.leftText:SetText(format("|cff%02x%02x%02x%d.|r %s", + rr * 255, rg * 255, rb * 255, srcIdx, td.name)) + else + bar.leftText:SetText(td.name) + end + bar.leftText:SetTextColor(tR, tG, tB) + local modeEntry = S.MODE_ORDER[win.modeIndex] + if modeEntry == S.COMBINED_DAMAGE or modeEntry == S.COMBINED_HEALING then + S.FormatCombinedText(bar.rightText, td.value, td.value / 20) + else + S.FormatValueText(bar.rightText, td.value) + end + local vR, vG, vB = S.ClassOrColor(db, 'valueClassColor', 'valueColor', td.class) + bar.rightText:SetTextColor(vR, vG, vB) + if bar._isDrill then S.ResetDrillBar(bar, db) end + if bar.selfIndicator then bar.selfIndicator:Hide() end + if db.showClassIcon then + S.ApplyClassIcon(bar.classIcon, db.classIconStyle, td.class) + else + bar.classIcon:Hide() + end + bar.frame.sourceGUID = nil + bar.frame.sourceClass = td.class + bar.frame.sourceName = td.name + bar.frame.testIndex = srcIdx + bar.frame.drillSpellID = nil + end + end + return + end + + local modeEntry = S.MODE_ORDER[win.modeIndex] + local meterType = S.ResolveMeterType(modeEntry) + local modeLabel = S.MODE_SHORT[modeEntry] or S.MODE_LABELS[modeEntry] or "?" + local sessLabel = S.GetSessionLabel(win) + + win.header.modeText:SetText(modeLabel) + win.header.sessText:SetText(" \226\128\148 " .. sessLabel) + S.ApplySessionHighlight(win, db) + + local session = S.GetSession(win, meterType) + + if win.sessionType then + local dur = C_DamageMeter.GetSessionDurationSeconds(win.sessionType) + if dur and not S.IsSecret(dur) then + local timerStr = format('%d:%02d', floor(dur / 60), floor(dur % 60)) + if session and session.totalAmount and not S.IsSecret(session.totalAmount) and session.totalAmount > 0 then + timerStr = timerStr .. ' \194\183 ' .. S.TruncateDecimals(AbbreviateNumbers(S.RoundIfPlain(session.totalAmount))) + end + win.header.timer:SetText(timerStr) + else + win.header.timer:SetText('') + end + else + win.header.timer:SetText('') + end + + local sources = session and session.combatSources + local usePerSec = (modeEntry == Enum.DamageMeterType.Dps or modeEntry == Enum.DamageMeterType.Hps) + local useCombined = (modeEntry == S.COMBINED_DAMAGE or modeEntry == S.COMBINED_HEALING) + local numVisible = S.ComputeNumVisible(win) + local total = sources and #sources or 0 + win.scrollOffset = max(0, min(win.scrollOffset, max(0, total - numVisible))) + + for i = 1, S.MAX_BARS do + local bar = win.bars[i] + if not bar then break end + + if i > numVisible then + bar.frame:Hide() + else + local srcIdx = win.scrollOffset + i + local src = sources and sources[srcIdx] + if src then + bar.frame:Show() + + local guid = (not S.IsSecret(src.sourceGUID)) and src.sourceGUID or nil + bar.frame.sourceGUID = guid + bar.frame.testIndex = nil + bar.frame.drillSpellID = nil + + local classFilename = src.classFilename + if not classFilename and guid then classFilename = S.classCache[guid] end + if guid and classFilename then S.classCache[guid] = classFilename end + bar.frame.sourceClass = classFilename + + local fgR, fgG, fgB = S.ClassOrColor(db, 'barClassColor', 'barColor', classFilename) + bar.statusbar:SetStatusBarColor(fgR, fgG, fgB) + bar.statusbar:SetMinMaxValues(0, session.maxAmount or 1) + bar.statusbar:SetValue(src.totalAmount or 0) + + local bgR, bgG, bgB, bgA = S.ClassOrColor(db, 'barBGClassColor', 'barBGColor', classFilename) + bar.background:SetVertexColor(bgR, bgG, bgB, bgA) + + local isLocal = src.isLocalPlayer + local specIcon = src.specIconID + local plainName + if isLocal then + local pg = UnitGUID('player') + plainName = (pg and S.nameCache[pg]) or UnitName('player') or '?' + elseif guid and S.nameCache[guid] then + plainName = S.nameCache[guid] + elseif specIcon and not S.specCollisions[specIcon] and S.specNameCache[specIcon] then + plainName = S.specNameCache[specIcon] + elseif not S.IsSecret(src.name) and src.name and src.name ~= '' then + plainName = (strsplit('-', src.name)) + end + if plainName and specIcon then + local existing = S.specNameCache[specIcon] + if existing and existing ~= plainName then + S.specCollisions[specIcon] = true + end + S.specNameCache[specIcon] = plainName + end + bar.frame.sourceName = plainName or '?' + + local tR, tG, tB = S.ClassOrColor(db, 'textClassColor', 'textColor', classFilename) + if plainName then + if db.showRank then + local rr, rg, rb = S.ClassOrColor(db, 'rankClassColor', 'rankColor', classFilename) + bar.leftText:SetText(format('|cff%02x%02x%02x%d.|r %s', + rr * 255, rg * 255, rb * 255, srcIdx, plainName)) + else + bar.leftText:SetText(plainName) + end + elseif S.IsSecret(src.name) then + if db.showRank then + bar.leftText:SetFormattedText('%d. %s', srcIdx, src.name) + else + bar.leftText:SetFormattedText('%s', src.name) + end + else + bar.leftText:SetText('?') + end + bar.leftText:SetTextColor(tR, tG, tB) + + if useCombined then + S.FormatCombinedText(bar.rightText, src.totalAmount, src.amountPerSecond) + else + local rawValue = usePerSec and src.amountPerSecond or src.totalAmount + S.FormatValueText(bar.rightText, rawValue) + end + local vR, vG, vB = S.ClassOrColor(db, 'valueClassColor', 'valueColor', classFilename) + bar.rightText:SetTextColor(vR, vG, vB) + if bar._isDrill then S.ResetDrillBar(bar, db) end + + if db.showClassIcon then + S.ApplyClassIcon(bar.classIcon, db.classIconStyle, classFilename) + else + bar.classIcon:Hide() + end + + if isLocal and bar.selfIndicator then + bar.selfIndicator:Show() + elseif bar.selfIndicator then + bar.selfIndicator:Hide() + end + else + bar.frame:Hide() + bar.frame.sourceGUID = nil + bar.frame.sourceName = nil + end + end + end + + if win.emptyText then + if total == 0 then + win.emptyText:Show() + else + win.emptyText:Hide() + end + end +end + +function addon:RefreshMeter() + for _, win in pairs(S.windows) do + S.RefreshWindow(win) + end +end + +function addon:SetMeterTestMode(enabled) + S.testMode = enabled + addon._meterTestMode = enabled + addon:RefreshMeter() +end + +local SetVis = EllesmereUI.SetElementVisibility + +local function FadeMeterOut() + for _, win in pairs(S.windows) do + if win.window then SetVis(win.window, false) end + end + S.meterFadedOut = true +end + +local function FadeMeterIn() + for _, win in pairs(S.windows) do + if win.window then SetVis(win.window, true) end + local wdb = S.GetWinDB(win.index) + if wdb and wdb.headerMouseover then + if win.header then win.header:SetAlpha(0) end + if win.headerBorder then win.headerBorder:SetAlpha(0) end + end + end + S.meterFadedOut = false +end + +local function CancelFlightFade() + if S.flightFadeTimer then + S.flightFadeTimer:Cancel() + S.flightFadeTimer = nil + end +end + +function addon:UpdateMeterVisibility() + local db = addon.db.profile + local petBattle = db.hideInPetBattle and C_PetBattles and C_PetBattles.IsInBattle() + local inFlight = not petBattle and db.hideInFlight and IsFlying() + local shouldHide = petBattle or inFlight + + if shouldHide == S.meterHidden then return end + S.meterHidden = shouldHide + CancelFlightFade() + + if shouldHide then + if inFlight then + S.flightFadeTimer = C_Timer.NewTimer(0.5, FadeMeterOut) + return + end + for _, win in pairs(S.windows) do + if win.window then SetVis(win.window, false) end + end + else + for _, win in pairs(S.windows) do + if win.window then SetVis(win.window, true) end + local wdb = S.GetWinDB(win.index) + if wdb and wdb.headerMouseover then + if win.header then win.header:SetAlpha(0) end + if win.headerBorder then win.headerBorder:SetAlpha(0) end + end + end + S.meterFadedOut = false + end +end + +function addon:UpdateFlightTicker() + local db = addon.db.profile + if db.hideInFlight and not S.flightTicker then + S.flightTicker = C_Timer.NewTicker(1.5, function() addon:UpdateMeterVisibility() end) + elseif not db.hideInFlight and S.flightTicker then + S.flightTicker:Cancel() + S.flightTicker = nil + addon:UpdateMeterVisibility() + end +end + +local function UpdateTimers() + for _, win in pairs(S.windows) do + if win.header and win.header.timer and win.sessionType and win.header.timer:IsShown() then + local dur = C_DamageMeter.GetSessionDurationSeconds(win.sessionType) + if dur and not S.IsSecret(dur) then + win.header.timer:SetText(format('%d:%02d', floor(dur / 60), floor(dur % 60))) + end + end + end +end + +function addon:ResizeMeterWindow(index) + S.ResizeStandalone(S.windows[index]) +end + +function addon:UpdateMeterLayout() + if not next(S.windows) then return end + + for _, win in pairs(S.windows) do + local db = S.GetWinDB(win.index) + local fontPath = S.ResolveFontPath(db.barFont) + local flags = S.FontFlags(db.barFontOutline) + + local resolveTex = EllesmereUI and EllesmereUI.ResolveTexturePath + local fgTex = resolveTex and resolveTex(S.texTextures, db.barTexture, S.DEFAULT_TEX) or S.DEFAULT_TEX + local bgTex = resolveTex and resolveTex(S.texTextures, db.barBGTexture, S.DEFAULT_TEX) or S.DEFAULT_TEX + + S.ApplyHeaderStyle(win, db) + S.RespaceBarAnchors(win, db) + for i = 1, S.MAX_BARS do + local bar = win.bars[i] + if bar then + S.StyleBarTexts(bar, fontPath, db.barFontSize, flags) + bar.statusbar:SetStatusBarTexture(fgTex) + bar.background:SetTexture(bgTex) + S.ApplyBarIconLayout(bar, db) + S.ApplyBarBorder(bar, db) + end + end + + if win.frame then + if db.showBackdrop then + win.frame:SetBackdrop(BACKDROP_FILL) + local bc = db.backdropColor + if bc then + win.frame:SetBackdropColor(bc.r, bc.g, bc.b, bc.a) + else + win.frame:SetBackdropColor(0.1, 0.1, 0.1, 0.8) + end + else + win.frame:SetBackdrop(nil) + end + end + + if win.headerBorder then + if db.showHeaderBorder then + win.headerBorder:Show() + else + win.headerBorder:Hide() + end + end + + if win.header and win.header.bg then + if db.showHeaderBackdrop then + win.header.bg:Show() + else + win.header.bg:Hide() + end + end + + if win.header then + S.SetupHeaderMouseover(win) + if db.headerMouseover then + win.header:SetAlpha(0) + if win.headerBorder then win.headerBorder:SetAlpha(0) end + else + win.header:SetAlpha(1) + if win.headerBorder then win.headerBorder:SetAlpha(1) end + end + end + + S.ResizeStandalone(win) + end + + addon:RefreshMeter() +end + +function addon:InitDamageMeter() + if not addon.db or not addon.db.profile.enabled then return end + + SetCVar('damageMeterEnabled', 0) + + C_Timer.After(0, function() + local db = addon.db.profile + + local win1 = S.NewWindowState(1, db.modeIndex) + S.windows[1] = win1 + S.CreateMeterFrame(win1) + + if not win1.frame then return end + + local function OnTDMEvent(_, event) + if event == 'PET_BATTLE_OPENING_START' or event == 'PET_BATTLE_CLOSE' then + addon:UpdateMeterVisibility() + return + elseif event == 'PLAYER_REGEN_DISABLED' then + for _, w in pairs(S.windows) do + S.ExitDrillDown(w) + end + return + elseif event == 'PLAYER_REGEN_ENABLED' then + S.ScanRoster() + addon:RefreshMeter() + return + elseif event == 'GROUP_ROSTER_UPDATE' then + S.ScanRoster() + return + elseif event == 'PLAYER_ENTERING_WORLD' then + wipe(S.nameCache) + wipe(S.classCache) + wipe(S.specNameCache) + wipe(S.specCollisions) + wipe(S.sessionLabelCache) + S.ScanRoster() + for _, w in pairs(S.windows) do + S.ResetWindowState(w) + end + if addon.db.profile.autoResetOnComplete then + local _, instanceType = IsInInstance() + if instanceType == 'party' or instanceType == 'raid' or instanceType == 'scenario' then + C_DamageMeter.ResetAllCombatSessions() + end + end + addon:RefreshMeter() + elseif event == 'DAMAGE_METER_RESET' then + wipe(S.sessionLabelCache) + wipe(S.nameCache) + wipe(S.classCache) + wipe(S.specNameCache) + wipe(S.specCollisions) + for _, w in pairs(S.windows) do + S.ResetWindowState(w) + end + addon:RefreshMeter() + else + wipe(S.sessionLabelCache) + addon:RefreshMeter() + end + end + + S.ScanRoster() + addon:RegisterEvent('DAMAGE_METER_COMBAT_SESSION_UPDATED', OnTDMEvent) + addon:RegisterEvent('DAMAGE_METER_CURRENT_SESSION_UPDATED', OnTDMEvent) + addon:RegisterEvent('DAMAGE_METER_RESET', OnTDMEvent) + addon:RegisterEvent('PLAYER_ENTERING_WORLD', OnTDMEvent) + addon:RegisterEvent('PLAYER_REGEN_DISABLED', OnTDMEvent) + addon:RegisterEvent('PLAYER_REGEN_ENABLED', OnTDMEvent) + addon:RegisterEvent('GROUP_ROSTER_UPDATE', OnTDMEvent) + addon:RegisterEvent('PET_BATTLE_OPENING_START', OnTDMEvent) + addon:RegisterEvent('PET_BATTLE_CLOSE', OnTDMEvent) + if not S.timerTicker then + S.timerTicker = C_Timer.NewTicker(0.5, UpdateTimers) + end + + addon:UpdateFlightTicker() + addon:RefreshMeter() + end) +end + +function addon:OnEnable() + self:InitDamageMeter() +end + +function addon:ReportMeter(channel, count, winIndex) + count = count or 5 + winIndex = winIndex or 1 + local win = S.windows[winIndex] + if not win then return end + + local modeEntry = S.MODE_ORDER[win.modeIndex] + local meterType = S.ResolveMeterType(modeEntry) + local modeLabel = S.MODE_LABELS[modeEntry] or "?" + local sessLabel = S.GetSessionLabel(win) + local session = S.GetSession(win, meterType) + local sources = session and session.combatSources + + if not sources or #sources == 0 then + print("|cff0cd39cEUI DM:|r No data to report.") + return + end + + if not channel then + if IsInRaid() then + channel = 'RAID' + elseif IsInGroup() then + channel = 'PARTY' + else + channel = 'SAY' + end + end + + local usePerSec = (modeEntry == Enum.DamageMeterType.Dps or modeEntry == Enum.DamageMeterType.Hps) + SendChatMessage(format("EUI DM: %s — %s", modeLabel, sessLabel), channel) + for i = 1, min(count, #sources) do + local src = sources[i] + local name = '?' + local guid = (not S.IsSecret(src.sourceGUID)) and src.sourceGUID or nil + if src.isLocalPlayer then + name = UnitName('player') or '?' + elseif guid and S.nameCache[guid] then + name = S.nameCache[guid] + elseif not S.IsSecret(src.name) and src.name then + name = (strsplit('-', src.name)) + end + local val = usePerSec and src.amountPerSecond or src.totalAmount + local valStr = val and S.TruncateDecimals(AbbreviateNumbers(S.RoundIfPlain(val))) or '0' + SendChatMessage(format(" %d. %s — %s", i, name, valStr), channel) + end +end + +SLASH_EUIDM1 = '/euidm' +SlashCmdList['EUIDM'] = function(msg) + local cmd = strtrim(msg):lower() + if cmd == 'toggle' then + for _, win in pairs(S.windows) do + if win.window then + if win.window:IsShown() then + win.window:Hide() + else + win.window:Show() + end + end + end + elseif cmd == 'test' then + addon:SetMeterTestMode(not S.testMode) + elseif cmd == 'reset' then + C_DamageMeter.ResetAllCombatSessions() + addon:RefreshMeter() + elseif cmd:match('^report') then + local channel = cmd:match('^report%s+(%a+)') + if channel then channel = channel:upper() end + addon:ReportMeter(channel) + else + print("|cff0cd39cEUI DM|r commands:") + print(" /euidm toggle — show/hide") + print(" /euidm test — toggle test mode") + print(" /euidm reset — reset sessions") + print(" /euidm report [channel] — report top 5") + end +end diff --git a/EllesmereUIDamageMeter/window.lua b/EllesmereUIDamageMeter/window.lua new file mode 100644 index 00000000..e5fbd4dc --- /dev/null +++ b/EllesmereUIDamageMeter/window.lua @@ -0,0 +1,513 @@ +local addon = EllesmereUIDamageMeter +local S = addon.S +if not S then return end +local LSM = LibStub('LibSharedMedia-3.0') + +local floor = math.floor + +local BACKDROP_FILL = { bgFile = 'Interface\\Buttons\\WHITE8x8' } +local MakeBorder = EllesmereUI.MakeBorder + +local function FadeIn(frame, duration, fromAlpha, toAlpha) + if UIFrameFadeIn then + UIFrameFadeIn(frame, duration, fromAlpha, toAlpha) + else + frame:SetAlpha(toAlpha) + end +end + +local function FadeOut(frame, duration, fromAlpha, toAlpha) + if UIFrameFadeOut then + UIFrameFadeOut(frame, duration, fromAlpha, toAlpha) + else + frame:SetAlpha(toAlpha) + end +end + +local function BuildMenuDescription(rootDescription, items) + for _, item in ipairs(items) do + if item.isSeparator then + rootDescription:CreateDivider() + elseif item.hasArrow and item.menuList then + local sub = rootDescription:CreateButton(item.text) + BuildMenuDescription(sub, item.menuList) + else + rootDescription:CreateButton(item.text, item.func) + end + end +end + +local function OpenMenuAnchored(menuItems, header) + MenuUtil.CreateContextMenu(header, function(_, rootDescription) + BuildMenuDescription(rootDescription, menuItems) + end) +end + +function S.SetupScrollWheel(win) + win.frame:EnableMouseWheel(true) + win.frame:SetScript('OnMouseWheel', function(_, delta) + local total + if win.drillSource then + total = S.GetDrillSpellCount(win) + elseif S.testMode then + total = #S.GetTestData(win) + else + local meterType = S.ResolveMeterType(S.MODE_ORDER[win.modeIndex]) + local session = S.GetSession(win, meterType) + total = (session and session.combatSources and #session.combatSources) or 0 + end + local numVis = S.ComputeNumVisible(win) + local maxOff = max(0, total - numVis) + win.scrollOffset = max(0, min(maxOff, win.scrollOffset - delta)) + S.RefreshWindow(win) + end) +end + +function S.FadeHeaderIn(win) + local db = S.GetWinDB(win.index) + if not db.headerMouseover then return end + if win.header then FadeIn(win.header, 0.2, win.header:GetAlpha(), 1) end + if win.headerBorder then FadeIn(win.headerBorder, 0.2, win.headerBorder:GetAlpha(), 1) end +end + +function S.FadeHeaderOut(win) + local db = S.GetWinDB(win.index) + if not db.headerMouseover then return end + if win.header then FadeOut(win.header, 0.2, win.header:GetAlpha(), 0) end + if win.headerBorder then FadeOut(win.headerBorder, 0.2, win.headerBorder:GetAlpha(), 0) end +end + +function S.SetupHeaderMouseover(win) + if win._headerMouseoverHooked then return end + win._headerMouseoverHooked = true + + local function OnEnter() S.FadeHeaderIn(win) end + local function OnLeave() S.FadeHeaderOut(win) end + + if win.header then + win.header:HookScript('OnEnter', OnEnter) + win.header:HookScript('OnLeave', OnLeave) + for _, child in pairs({ win.header.modeArea, win.header.sessArea, win.header.reset }) do + if child then + child:HookScript('OnEnter', OnEnter) + child:HookScript('OnLeave', OnLeave) + end + end + end +end + +function S.ApplyHeaderStyle(win, db) + local header = win.header + if not header then return end + + local fontPath = S.ResolveFontPath(db.headerFont) + local flags = S.FontFlags(db.headerFontOutline) + + local hc = db.headerBGColor + if db.showHeaderBackdrop then + header.bg:SetVertexColor(hc.r, hc.g, hc.b, hc.a) + else + header.bg:SetVertexColor(0, 0, 0, 0) + end + + local tc = db.headerFontColor + S.ApplyFont(header.modeText, fontPath, db.headerFontSize + 1, flags) + header.modeText:SetTextColor(tc.r, tc.g, tc.b) + + S.ApplyFont(header.sessText, fontPath, db.headerFontSize + 1, flags) + header.sessText:SetTextColor(tc.r, tc.g, tc.b) + + S.ApplyFont(header.timer, fontPath, db.headerFontSize, flags) + header.timer:SetTextColor(tc.r, tc.g, tc.b, 0.7) + header.timer:ClearAllPoints() + if db.showTimer then + header.timer:SetPoint('RIGHT', header.reset, 'LEFT', -4, 0) + header.timer:Show() + else + header.timer:Hide() + end +end + +function S.MakeModeEntry(win, mtype) + local idx + for i, mt in ipairs(S.MODE_ORDER) do + if mt == mtype then idx = i; break end + end + if not idx then return nil end + + local label = S.MODE_LABELS[mtype] or '?' + return { + text = (idx == win.modeIndex) and ('|cffffd100' .. label .. '|r') or label, + notCheckable = true, + func = function() + win.modeIndex = idx + win.drillSource = nil + win.scrollOffset = 0 + local wdb = S.GetWinDB(win.index) + wdb.modeIndex = idx + S.RefreshWindow(win) + end, + } +end + +function S.BuildModeMenu(win) + local dmg = { + S.MakeModeEntry(win, Enum.DamageMeterType.DamageDone), + S.MakeModeEntry(win, Enum.DamageMeterType.Dps), + S.MakeModeEntry(win, S.COMBINED_DAMAGE), + S.MakeModeEntry(win, Enum.DamageMeterType.DamageTaken), + S.MakeModeEntry(win, Enum.DamageMeterType.AvoidableDamageTaken), + } + if Enum.DamageMeterType.EnemyDamageTaken then + dmg[#dmg + 1] = S.MakeModeEntry(win, Enum.DamageMeterType.EnemyDamageTaken) + end + + local heal = { + S.MakeModeEntry(win, Enum.DamageMeterType.HealingDone), + S.MakeModeEntry(win, Enum.DamageMeterType.Hps), + S.MakeModeEntry(win, S.COMBINED_HEALING), + S.MakeModeEntry(win, Enum.DamageMeterType.Absorbs), + } + + local actions = { + S.MakeModeEntry(win, Enum.DamageMeterType.Interrupts), + S.MakeModeEntry(win, Enum.DamageMeterType.Dispels), + } + if Enum.DamageMeterType.Deaths then + actions[#actions + 1] = S.MakeModeEntry(win, Enum.DamageMeterType.Deaths) + end + + return { + { text = 'Damage', notCheckable = true, hasArrow = true, menuList = dmg }, + { text = 'Healing', notCheckable = true, hasArrow = true, menuList = heal }, + { text = 'Actions', notCheckable = true, hasArrow = true, menuList = actions }, + } +end + +function S.BuildSessionMenu(win) + local menu = {} + + -- Encounter sessions (oldest at top, newest at bottom) + if C_DamageMeter.GetAvailableCombatSessions then + local sessions = C_DamageMeter.GetAvailableCombatSessions() + if sessions and #sessions > 0 then + for _, sess in ipairs(sessions) do + local sid = sess.sessionId or sess.combatSessionId or sess.id or sess.sessionID + local label = sess.name or 'Encounter' + local dur = sess.durationSeconds or sess.duration + if dur and not S.IsSecret(dur) then + label = label .. format(' [%d:%02d]', floor(dur / 60), floor(dur % 60)) + end + menu[#menu + 1] = { + text = (win.sessionId == sid) and ('|cffffd100' .. label .. '|r') or label, + notCheckable = true, + func = function() + win.sessionId = sid + win.sessionType = nil + win.scrollOffset = 0 + win.drillSource = nil + S.RefreshWindow(win) + end, + } + end + menu[#menu + 1] = { isSeparator = true } + end + end + + -- Current / Overall + menu[#menu + 1] = { + text = (win.sessionId == nil and win.sessionType == Enum.DamageMeterSessionType.Current) + and '|cffffd100Current Segment|r' or 'Current Segment', + notCheckable = true, + func = function() + win.sessionId = nil + win.sessionType = Enum.DamageMeterSessionType.Current + win.scrollOffset = 0 + win.drillSource = nil + S.RefreshWindow(win) + end, + } + + menu[#menu + 1] = { + text = (win.sessionId == nil and win.sessionType == Enum.DamageMeterSessionType.Overall) + and '|cffffd100Overall|r' or 'Overall', + notCheckable = true, + func = function() + win.sessionId = nil + win.sessionType = Enum.DamageMeterSessionType.Overall + win.scrollOffset = 0 + win.drillSource = nil + S.RefreshWindow(win) + end, + } + + return menu +end + +function S.ToggleSession(win) + win.sessionId = nil + if win.sessionType == Enum.DamageMeterSessionType.Current then + win.sessionType = Enum.DamageMeterSessionType.Overall + else + win.sessionType = Enum.DamageMeterSessionType.Current + end + win.scrollOffset = 0 + win.drillSource = nil + S.RefreshWindow(win) +end + +function S.SetupHeaderContent(win, db) + local header = win.header + + header.bg = header:CreateTexture(nil, 'BACKGROUND') + header.bg:SetAllPoints() + header.bg:SetTexture(S.DEFAULT_TEX) + + header.modeText = header:CreateFontString(nil, 'OVERLAY') + header.modeText:SetPoint('LEFT', 4, 0) + header.modeText:SetShadowOffset(1, -1) + + header.sessText = header:CreateFontString(nil, 'OVERLAY') + header.sessText:SetPoint('LEFT', header.modeText, 'RIGHT', 0, 0) + header.sessText:SetShadowOffset(1, -1) + + header.reset = CreateFrame('Button', nil, header) + header.reset:SetSize(16, 16) + header.reset:SetPoint('RIGHT', -4, 0) + header.reset.text = header.reset:CreateFontString(nil, 'OVERLAY') + header.reset.text:SetAllPoints() + header.reset.text:SetFont(STANDARD_TEXT_FONT, 14, 'OUTLINE') + header.reset.text:SetText('x') + header.reset.text:SetTextColor(0.8, 0.2, 0.2) + header.reset:SetScript('OnClick', function(_, btn) + if btn == 'LeftButton' then + StaticPopup_Show('EUIDM_RESET') + end + end) + header.reset:HookScript('OnEnter', function(self) + GameTooltip_SetDefaultAnchor(GameTooltip, self) + GameTooltip:AddLine('Reset Meter', 1, 0.3, 0.3) + GameTooltip:AddLine('Clears all session data.', 0.7, 0.7, 0.7) + GameTooltip:Show() + end) + header.reset:HookScript('OnLeave', GameTooltip_Hide) + + header.timer = header:CreateFontString(nil, 'OVERLAY') + header.timer:SetShadowOffset(1, -1) + + S.ApplyHeaderStyle(win, db) + + header.modeArea = CreateFrame('Frame', nil, header) + header.modeArea:SetPoint('TOPLEFT', header.modeText, 'TOPLEFT', 0, 0) + header.modeArea:SetPoint('BOTTOMRIGHT', header.modeText, 'BOTTOMRIGHT', 0, 0) + header.modeArea:EnableMouse(true) + header.modeArea:SetScript('OnMouseUp', function(_, button) + if button == 'LeftButton' then + if win.drillSource then + S.ExitDrillDown(win) + else + OpenMenuAnchored(S.BuildModeMenu(win), header) + end + elseif button == 'RightButton' then + S.ToggleSession(win) + end + end) + header.modeArea:SetScript('OnEnter', function(self) + GameTooltip_SetDefaultAnchor(GameTooltip, self) + if win.drillSource then + GameTooltip:AddLine('|cffffd100Left-click:|r return to overview', 0.7, 0.7, 0.7) + else + GameTooltip:AddLine('|cffffd100Left-click:|r choose display mode', 0.7, 0.7, 0.7) + end + GameTooltip:AddLine('|cffffd100Right-click:|r toggle Current / Overall', 0.7, 0.7, 0.7) + GameTooltip:Show() + end) + header.modeArea:SetScript('OnLeave', GameTooltip_Hide) + + header.sessArea = CreateFrame('Frame', nil, header) + header.sessArea:SetPoint('TOPLEFT', header.sessText, 'TOPLEFT', 0, 0) + header.sessArea:SetPoint('BOTTOMRIGHT', header.sessText, 'BOTTOMRIGHT', 0, 0) + header.sessArea:EnableMouse(true) + header.sessArea:SetScript('OnMouseUp', function(_, button) + if button == 'LeftButton' then + OpenMenuAnchored(S.BuildSessionMenu(win), header) + elseif button == 'RightButton' then + S.ToggleSession(win) + end + end) + header.sessArea:SetScript('OnEnter', function(self) + GameTooltip_SetDefaultAnchor(GameTooltip, self) + GameTooltip:AddLine('|cffffd100Left-click:|r choose encounter', 0.7, 0.7, 0.7) + GameTooltip:AddLine('|cffffd100Right-click:|r toggle Current / Overall', 0.7, 0.7, 0.7) + GameTooltip:Show() + end) + header.sessArea:SetScript('OnLeave', GameTooltip_Hide) +end + +function S.SetupWindowContent(win, db, parent) + local winName = 'EUIDMMeter' .. (win.index or 1) + local hdrName = 'EUIDMMeterHeader' .. (win.index or 1) + + win.header = CreateFrame('Frame', hdrName, parent) + win.header:SetPoint('TOPLEFT', win.window, 'TOPLEFT', 0, 0) + win.header:SetPoint('TOPRIGHT', win.window, 'TOPRIGHT', 0, 0) + win.header:SetHeight(S.HEADER_HEIGHT) + win.header:SetFrameLevel(win.window:GetFrameLevel() + 1) + + win.headerBorderObj = MakeBorder(win.header, 0, 0, 0, 1) + win.headerBorder = win.headerBorderObj._frame + if not db.showHeaderBorder then + win.headerBorder:Hide() + end + win.header:EnableMouse(true) + + S.SetupHeaderContent(win, db) + + win.frame = CreateFrame('Frame', nil, parent, 'BackdropTemplate') + win.frame:SetFrameStrata('MEDIUM') + win.frame:SetClipsChildren(true) + win.frame:SetPoint('TOPLEFT', win.window, 'TOPLEFT', 0, -S.HEADER_HEIGHT) + win.frame:SetPoint('BOTTOMRIGHT', win.window, 'BOTTOMRIGHT', 0, 0) + + win.emptyText = win.frame:CreateFontString(nil, 'OVERLAY') + win.emptyText:SetPoint('CENTER', win.frame, 'CENTER', 0, 0) + local emptyFont = S.ResolveFontPath(db.barFont) + S.ApplyFont(win.emptyText, emptyFont, db.barFontSize, S.FontFlags(db.barFontOutline)) + win.emptyText:SetTextColor(0.5, 0.5, 0.5, 0.6) + win.emptyText:SetText('No data') + win.emptyText:Hide() + + win.frameBorderObj = MakeBorder(win.frame, 0, 0, 0, 1) + if db.showBackdrop then + win.frame:SetBackdrop(BACKDROP_FILL) + local bc = db.backdropColor + if bc then + win.frame:SetBackdropColor(bc.r, bc.g, bc.b, bc.a) + else + win.frame:SetBackdropColor(0.1, 0.1, 0.1, 0.8) + end + end + + if not db.showHeaderBackdrop and win.header.bg then + win.header.bg:Hide() + end + + local fontPath = S.ResolveFontPath(db.barFont) + local flags = S.FontFlags(db.barFontOutline) + + for j = 1, S.MAX_BARS do + local bar = S.CreateBar(win.frame) + S.StyleBarTexts(bar, fontPath, db.barFontSize, flags) + S.ApplyBarIconLayout(bar, db) + S.ApplyBarBorder(bar, db) + + local sp = max(0, db.barSpacing or 1) + local borderAdj = (db.barBorderEnabled and sp == 0) and 1 or 0 + if j == 1 then + bar.frame:SetPoint('TOPLEFT', win.frame, 'TOPLEFT', 0, 0) + bar.frame:SetPoint('TOPRIGHT', win.frame, 'TOPRIGHT', 0, 0) + else + bar.frame:SetPoint('TOPLEFT', win.bars[j-1].frame, 'BOTTOMLEFT', 0, -sp + borderAdj) + bar.frame:SetPoint('TOPRIGHT', win.bars[j-1].frame, 'BOTTOMRIGHT', 0, -sp + borderAdj) + end + win.bars[j] = bar + S.SetupBarInteraction(bar, win) + end + + S.SetupScrollWheel(win) + + S.SetupHeaderMouseover(win) + if db.headerMouseover then + win.header:SetAlpha(0) + if win.headerBorder then win.headerBorder:SetAlpha(0) end + end +end + +function S.CreateMeterFrame(win) + local db = S.GetWinDB(win.index) + + local w, h = db.standaloneWidth, db.standaloneHeight + + local window = CreateFrame('Frame', 'EUIDMMeter' .. (win.index or 1), UIParent, 'BackdropTemplate') + window:SetSize(w, h) + window:SetMovable(true) + window:EnableMouse(true) + window:RegisterForDrag('LeftButton') + window:SetScript('OnDragStart', window.StartMoving) + window:SetScript('OnDragStop', function(self) + self:StopMovingOrSizing() + self:SetUserPlaced(false) + local point, _, relPoint, x, y = self:GetPoint(1) + addon.db.profile.position = { point = point, relPoint = relPoint, x = x, y = y } + end) + window:SetClampedToScreen(true) + window:SetFrameStrata('BACKGROUND') + window:SetFrameLevel(300) + + local pos = addon.db.profile.position + if pos then + window:SetPoint(pos.point or 'CENTER', UIParent, pos.relPoint or 'CENTER', pos.x or 0, pos.y or 0) + else + window:SetPoint('CENTER', UIParent, 'CENTER', 0, 0) + end + + win.window = window + + S.SetupWindowContent(win, db, window) + S.ResizeStandalone(win) + + if EllesmereUI and EllesmereUI.RegisterUnlockElements and EllesmereUI.MakeUnlockElement then + local MK = EllesmereUI.MakeUnlockElement + EllesmereUI:RegisterUnlockElements({ + MK({ + key = 'EUIDM_Window', + label = 'Damage Meter', + group = 'Damage Meter', + order = 500, + getFrame = function() return window end, + getSize = function() return window:GetSize() end, + setWidth = function(_, w) + addon.db.profile.standaloneWidth = math.max(math.floor(w + 0.5), 120) + addon:ResizeMeterWindow(1) + end, + setHeight = function(_, h) + addon.db.profile.standaloneHeight = math.max(math.floor(h + 0.5), 80) + addon:ResizeMeterWindow(1) + end, + savePos = function(_, point, relPoint, x, y) + addon.db.profile.position = { point = point, relPoint = relPoint, x = x, y = y } + end, + loadPos = function() + return addon.db.profile.position + end, + clearPos = function() + addon.db.profile.position = nil + end, + applyPos = function() + local p = addon.db.profile.position + if p then + window:ClearAllPoints() + window:SetPoint(p.point or 'CENTER', UIParent, p.relPoint or 'CENTER', p.x or 0, p.y or 0) + end + end, + }), + }) + end +end + +function S.RespaceBarAnchors(win, db) + local sp = max(0, db.barSpacing or 1) + local borderAdj = (db.barBorderEnabled and sp == 0) and 1 or 0 + for i = 1, S.MAX_BARS do + local bar = win.bars[i] + if not bar then break end + bar.frame:ClearAllPoints() + if i == 1 then + bar.frame:SetPoint('TOPLEFT', win.frame, 'TOPLEFT', 0, 0) + bar.frame:SetPoint('TOPRIGHT', win.frame, 'TOPRIGHT', 0, 0) + else + bar.frame:SetPoint('TOPLEFT', win.bars[i-1].frame, 'BOTTOMLEFT', 0, -sp + borderAdj) + bar.frame:SetPoint('TOPRIGHT', win.bars[i-1].frame, 'BOTTOMRIGHT', 0, -sp + borderAdj) + end + end +end