local NanamiDPS = NanamiDPS local DataStore = NanamiDPS.DataStore local L = NanamiDPS.L local DetailView = {} NanamiDPS.DetailView = DetailView local detailFrame = nil local MAX_SPELL_BARS = 20 local MAX_COMPARE_BARS = 10 ----------------------------------------------------------------------- -- Data-type resolution: which sub-table to look up for a given module ----------------------------------------------------------------------- local function GetPlayerEntry(moduleName, segment, playerName) if not segment or not segment.data then return nil, nil end local dataMap = { DamageDone = "damage", DPS = "damage", DamageTaken = "damageTaken", HealingDone = "healing", HPS = "healing", Overhealing = "healing", Deaths = "deaths", Dispels = "dispels", Interrupts = "interrupts", ThreatEstimate = "threat", Activity = "activity", EnemyDamageDone = "damage", DamageBySpell = "damage", HealingBySpell = "healing", } local key = dataMap[moduleName] if not key then return nil, nil end local tbl = segment.data[key] if not tbl then return nil, key end return tbl[playerName], key end ----------------------------------------------------------------------- -- Internal: create a mini bar (used for spell breakdown & comparison) ----------------------------------------------------------------------- local function CreateMiniBar(parent, index) local A = SFrames.ActiveTheme local bar = CreateFrame("StatusBar", nil, parent) bar:SetStatusBarTexture(SFrames:GetTexture()) bar:SetHeight(14) bar:SetMinMaxValues(0, 1) bar:SetValue(0) bar.bg = bar:CreateTexture(nil, "BACKGROUND") bar.bg:SetTexture(SFrames:GetTexture()) bar.bg:SetAllPoints(bar) if A and A.barBg then bar.bg:SetVertexColor(A.barBg[1], A.barBg[2], A.barBg[3], A.barBg[4] or 0.5) else bar.bg:SetVertexColor(0.12, 0.12, 0.12, 0.5) end bar.textLeft = SFrames:CreateFontString(bar, 9, "LEFT") bar.textLeft:SetPoint("LEFT", bar, "LEFT", 4, 0) bar.textLeft:SetPoint("RIGHT", bar, "RIGHT", -70, 0) bar.textRight = SFrames:CreateFontString(bar, 9, "RIGHT") bar.textRight:SetPoint("RIGHT", bar, "RIGHT", -4, 0) bar.textRight:SetWidth(66) bar.index = index return bar end local function LayoutMiniBars(bars, parent, startY, spacing) for i, bar in ipairs(bars) do bar:ClearAllPoints() bar:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, startY - (i - 1) * (14 + spacing)) bar:SetPoint("TOPRIGHT", parent, "TOPRIGHT", 0, startY - (i - 1) * (14 + spacing)) end end ----------------------------------------------------------------------- -- Create the detail frame (once) ----------------------------------------------------------------------- local function EnsureFrame() if detailFrame then return detailFrame end local A = SFrames.ActiveTheme detailFrame = CreateFrame("Frame", "NanamiDPSDetailView", UIParent) detailFrame:SetWidth(380) detailFrame:SetHeight(480) detailFrame:SetPoint("CENTER", UIParent, "CENTER", 200, 0) detailFrame:SetMovable(true) detailFrame:EnableMouse(true) detailFrame:SetClampedToScreen(true) detailFrame:SetFrameStrata("HIGH") detailFrame:EnableMouseWheel(1) detailFrame:SetResizable(true) detailFrame:SetMinResize(280, 300) SFrames:CreateRoundBackdrop(detailFrame) if A and A.panelBg then detailFrame:SetBackdropColor(A.panelBg[1], A.panelBg[2], A.panelBg[3], 0.96) end if A and A.panelBorder then detailFrame:SetBackdropBorderColor(A.panelBorder[1], A.panelBorder[2], A.panelBorder[3], A.panelBorder[4] or 0.9) end -- Title bar local titleBar = CreateFrame("Frame", nil, detailFrame) titleBar:SetHeight(24) titleBar:SetPoint("TOPLEFT", detailFrame, "TOPLEFT", 4, -4) titleBar:SetPoint("TOPRIGHT", detailFrame, "TOPRIGHT", -4, -4) titleBar:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", tile = false, edgeSize = 0, }) if A and A.headerBg then titleBar:SetBackdropColor(A.headerBg[1], A.headerBg[2], A.headerBg[3], A.headerBg[4] or 0.98) else titleBar:SetBackdropColor(0.06, 0.06, 0.08, 0.98) end titleBar:EnableMouse(true) titleBar:RegisterForDrag("LeftButton") titleBar:SetScript("OnDragStart", function() detailFrame:StartMoving() end) titleBar:SetScript("OnDragStop", function() detailFrame:StopMovingOrSizing() end) detailFrame.titleBar = titleBar -- Title text (player name + module) detailFrame.titleText = SFrames:CreateFontString(titleBar, 11, "LEFT") detailFrame.titleText:SetPoint("LEFT", titleBar, "LEFT", 8, 0) detailFrame.titleText:SetPoint("RIGHT", titleBar, "RIGHT", -55, 0) -- Close button local closeBtn = CreateFrame("Button", nil, titleBar) closeBtn:SetWidth(18) closeBtn:SetHeight(18) closeBtn:SetPoint("RIGHT", titleBar, "RIGHT", -3, 0) local closeIcon = SFrames:CreateIcon(closeBtn, "close", 12) closeIcon:SetPoint("CENTER", 0, 0) closeBtn:SetScript("OnClick", function() detailFrame:Hide() end) closeBtn:SetScript("OnEnter", function() closeIcon:SetVertexColor(1, 0.3, 0.3) end) closeBtn:SetScript("OnLeave", function() closeIcon:SetVertexColor(1, 1, 1) end) -- Report button local reportBtn = CreateFrame("Button", nil, titleBar) reportBtn:SetWidth(16) reportBtn:SetHeight(16) reportBtn:SetPoint("RIGHT", closeBtn, "LEFT", -2, 0) local reportIconFs = SFrames:CreateFontString(reportBtn, 9, "CENTER") reportIconFs:SetAllPoints() reportIconFs:SetText("R") if A and A.text then reportIconFs:SetTextColor(A.text[1], A.text[2], A.text[3]) end reportBtn:SetScript("OnClick", function() DetailView:ShowReport() end) reportBtn:SetScript("OnEnter", function() GameTooltip:SetOwner(this, "ANCHOR_RIGHT") GameTooltip:AddLine(L["Report to Chat"]) GameTooltip:Show() if A and A.accent then reportIconFs:SetTextColor(A.accent[1], A.accent[2], A.accent[3]) end end) reportBtn:SetScript("OnLeave", function() GameTooltip:Hide() if A and A.text then reportIconFs:SetTextColor(A.text[1], A.text[2], A.text[3]) else reportIconFs:SetTextColor(1, 1, 1) end end) ------------------------------------------------------------------- -- Tab bar under title ------------------------------------------------------------------- local tabBar = CreateFrame("Frame", nil, detailFrame) tabBar:SetHeight(18) tabBar:SetPoint("TOPLEFT", titleBar, "BOTTOMLEFT", 0, 0) tabBar:SetPoint("TOPRIGHT", titleBar, "BOTTOMRIGHT", 0, 0) tabBar:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", tile = false, edgeSize = 0, }) if A and A.sectionBg then tabBar:SetBackdropColor(A.sectionBg[1], A.sectionBg[2], A.sectionBg[3], A.sectionBg[4] or 0.95) else tabBar:SetBackdropColor(0.05, 0.03, 0.04, 0.95) end tabBar.tabs = {} detailFrame.tabBar = tabBar local tabNames = { L["Spell Breakdown"], L["Summary"], L["Comparison"] } local tabCount = table.getn(tabNames) for i, name in ipairs(tabNames) do local tab = CreateFrame("Button", nil, tabBar) tab:SetHeight(18) if i == 1 then tab:SetPoint("TOPLEFT", tabBar, "TOPLEFT", 0, 0) else tab:SetPoint("LEFT", tabBar.tabs[i - 1], "RIGHT", 0, 0) end if i == tabCount then tab:SetPoint("RIGHT", tabBar, "RIGHT", 0, 0) else local tabWidth = math.floor((detailFrame:GetWidth() - 8) / tabCount) tab:SetWidth(tabWidth) end tab:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", tile = false, edgeSize = 0, }) if A and A.tabBg then tab:SetBackdropColor(A.tabBg[1], A.tabBg[2], A.tabBg[3], A.tabBg[4] or 0.6) else tab:SetBackdropColor(0.12, 0.06, 0.09, 0.6) end local label = SFrames:CreateFontString(tab, 9, "CENTER") label:SetAllPoints() label:SetText(name) if A and A.tabText then label:SetTextColor(A.tabText[1], A.tabText[2], A.tabText[3]) end tab.label = label tab.tabIndex = i tab:SetScript("OnClick", function() DetailView:SwitchTab(this.tabIndex) end) tab:SetScript("OnEnter", function() if detailFrame.activeTab ~= this.tabIndex then if A and A.buttonHoverBg then this:SetBackdropColor(A.buttonHoverBg[1], A.buttonHoverBg[2], A.buttonHoverBg[3], 0.3) else this:SetBackdropColor(0.3, 0.3, 0.3, 0.15) end end end) tab:SetScript("OnLeave", function() if detailFrame.activeTab ~= this.tabIndex then if A and A.tabBg then this:SetBackdropColor(A.tabBg[1], A.tabBg[2], A.tabBg[3], A.tabBg[4] or 0.6) else this:SetBackdropColor(0.12, 0.06, 0.09, 0.6) end end end) tabBar.tabs[i] = tab end ------------------------------------------------------------------- -- Content area ------------------------------------------------------------------- local content = CreateFrame("Frame", nil, detailFrame) content:SetPoint("TOPLEFT", tabBar, "BOTTOMLEFT", 8, -6) content:SetPoint("BOTTOMRIGHT", detailFrame, "BOTTOMRIGHT", -10, 22) detailFrame.content = content ------------------------------------------------------------------- -- Page containers ------------------------------------------------------------------- -- Page 1: Spell breakdown detailFrame.pageSpells = CreateFrame("Frame", nil, content) detailFrame.pageSpells:SetAllPoints(content) detailFrame.pageSpells.bars = {} detailFrame.pageSpells.scroll = 0 -- Page 2: Summary stats detailFrame.pageSummary = CreateFrame("Frame", nil, content) detailFrame.pageSummary:SetAllPoints(content) detailFrame.pageSummary.lines = {} -- Page 3: Comparison detailFrame.pageCompare = CreateFrame("Frame", nil, content) detailFrame.pageCompare:SetAllPoints(content) detailFrame.pageCompare.bars = {} ------------------------------------------------------------------- -- Scroll wheel ------------------------------------------------------------------- detailFrame:SetScript("OnMouseWheel", function() if detailFrame.activeTab == 1 then local page = detailFrame.pageSpells page.scroll = page.scroll + (arg1 > 0 and -1 or 1) page.scroll = math.max(page.scroll, 0) DetailView:RefreshSpells() end end) ------------------------------------------------------------------- -- Resize handle ------------------------------------------------------------------- local resizeBtn = CreateFrame("Frame", nil, detailFrame) resizeBtn:SetWidth(12) resizeBtn:SetHeight(12) resizeBtn:SetPoint("BOTTOMRIGHT", detailFrame, "BOTTOMRIGHT", -5, 5) resizeBtn:EnableMouse(true) resizeBtn:SetFrameLevel(detailFrame:GetFrameLevel() + 10) resizeBtn.tex = resizeBtn:CreateTexture(nil, "OVERLAY") resizeBtn.tex:SetAllPoints() resizeBtn.tex:SetTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Up") if A and A.accent then resizeBtn.tex:SetVertexColor(A.accent[1], A.accent[2], A.accent[3], 0.5) else resizeBtn.tex:SetVertexColor(0.5, 0.5, 0.5, 0.3) end resizeBtn:SetScript("OnMouseDown", function() detailFrame:StartSizing("BOTTOMRIGHT") end) resizeBtn:SetScript("OnMouseUp", function() detailFrame:StopMovingOrSizing() DetailView:Refresh() end) detailFrame.activeTab = 1 detailFrame:Hide() return detailFrame end ----------------------------------------------------------------------- -- Tab switching ----------------------------------------------------------------------- function DetailView:SwitchTab(tabIndex) local frame = EnsureFrame() local A = SFrames.ActiveTheme frame.activeTab = tabIndex for i, tab in ipairs(frame.tabBar.tabs) do if i == tabIndex then if A and A.tabActiveBg then tab:SetBackdropColor(A.tabActiveBg[1], A.tabActiveBg[2], A.tabActiveBg[3], A.tabActiveBg[4] or 0.3) else tab:SetBackdropColor(0.3, 0.3, 0.3, 0.2) end if A and A.tabActiveText then tab.label:SetTextColor(A.tabActiveText[1], A.tabActiveText[2], A.tabActiveText[3]) else tab.label:SetTextColor(1, 0.85, 0.9) end else if A and A.tabBg then tab:SetBackdropColor(A.tabBg[1], A.tabBg[2], A.tabBg[3], A.tabBg[4] or 0.6) else tab:SetBackdropColor(0.12, 0.06, 0.09, 0.6) end if A and A.tabText then tab.label:SetTextColor(A.tabText[1], A.tabText[2], A.tabText[3]) else tab.label:SetTextColor(0.7, 0.7, 0.7) end end end frame.pageSpells:Hide() frame.pageSummary:Hide() frame.pageCompare:Hide() if tabIndex == 1 then frame.pageSpells:Show() self:RefreshSpells() elseif tabIndex == 2 then frame.pageSummary:Show() self:RefreshSummary() elseif tabIndex == 3 then frame.pageCompare:Show() self:RefreshCompare() end end ----------------------------------------------------------------------- -- Show detail view for a bar ----------------------------------------------------------------------- function DetailView:Show(barData, moduleName, segmentIndex) local frame = EnsureFrame() frame.playerName = barData.id or barData.name frame.moduleName = moduleName frame.segmentIndex = segmentIndex local mod = NanamiDPS.modules[moduleName] local modDisplayName = mod and mod:GetName() or moduleName local accentHex = (SFrames.ActiveTheme and SFrames.ActiveTheme.accentHex) or "ffFF88AA" frame.titleText:SetText("|c" .. accentHex .. (barData.name or L["Unknown"]) .. "|r - " .. modDisplayName) frame.pageSpells.scroll = 0 frame:Show() self:SwitchTab(1) end ----------------------------------------------------------------------- -- Page 1: Spell Breakdown (visual bar chart) ----------------------------------------------------------------------- function DetailView:RefreshSpells() local frame = detailFrame if not frame or not frame:IsShown() then return end local page = frame.pageSpells local seg = DataStore:GetSegment(frame.segmentIndex or 1) local entry, dataKey = GetPlayerEntry(frame.moduleName, seg, frame.playerName) -- Collect spell data local spellData = {} local totalVal = 0 if entry then if entry.spells then for spell, amount in pairs(entry.spells) do table.insert(spellData, { name = spell, value = amount, effective = entry.effective and entry.effective[spell], }) totalVal = totalVal + amount end elseif entry.events then -- Deaths module: show event log local evts = entry.events if table.getn(evts) > 0 then local lastDeath = evts[table.getn(evts)] if lastDeath.events then for _, evt in ipairs(lastDeath.events) do local spell = evt.spell or L["Unknown"] local amount = math.abs(evt.amount or 0) table.insert(spellData, { name = spell .. " (" .. (evt.source or "?") .. ")", value = amount, isDamage = evt.type == "damage", isHeal = evt.type == "heal", }) totalVal = totalVal + amount end end end elseif dataKey == "activity" then -- Activity doesn't have spells local activeTime = entry._activeTime or 0 table.insert(spellData, { name = L["Active Time"], value = activeTime }) totalVal = activeTime end end table.sort(spellData, function(a, b) return a.value > b.value end) -- Determine how many bars fit local contentH = page:GetHeight() if not contentH or contentH < 20 then contentH = 340 end local barH = 16 local barSpacing = 2 local maxBars = math.max(1, math.floor(contentH / (barH + barSpacing))) local totalEntries = table.getn(spellData) page.scroll = math.min(page.scroll, math.max(0, totalEntries - maxBars)) local barsNeeded = math.max(0, math.min(maxBars, totalEntries - page.scroll)) -- Create bars as needed while table.getn(page.bars) < barsNeeded do local bar = CreateMiniBar(page, table.getn(page.bars) + 1) table.insert(page.bars, bar) end local maxVal = 0 for _, sd in ipairs(spellData) do if sd.value > maxVal then maxVal = sd.value end end -- Update bars for i = 1, math.max(table.getn(page.bars), barsNeeded) do local dataIdx = i + page.scroll local bar = page.bars[i] if not bar then break end if dataIdx <= totalEntries then local sd = spellData[dataIdx] bar:Show() bar:SetMinMaxValues(0, maxVal > 0 and maxVal or 1) bar:SetValue(sd.value) if sd.isDamage then bar:SetStatusBarColor(0.9, 0.3, 0.3, 0.8) bar.bg:SetVertexColor(0.2, 0.06, 0.06, 0.5) elseif sd.isHeal then bar:SetStatusBarColor(0.3, 0.9, 0.3, 0.8) bar.bg:SetVertexColor(0.06, 0.2, 0.06, 0.5) else local A = SFrames.ActiveTheme if A and A.accent then bar:SetStatusBarColor(A.accent[1], A.accent[2], A.accent[3], 0.7) bar.bg:SetVertexColor(A.accent[1] * 0.2, A.accent[2] * 0.2, A.accent[3] * 0.2, 0.5) else bar:SetStatusBarColor(1, 0.53, 0.67, 0.7) bar.bg:SetVertexColor(0.15, 0.08, 0.1, 0.5) end end local pct = totalVal > 0 and NanamiDPS.round(sd.value / totalVal * 100, 1) or 0 bar.textLeft:SetText("|cffffffff" .. dataIdx .. ". " .. sd.name) local rightStr = NanamiDPS.formatNumber(sd.value) if sd.effective and sd.effective ~= sd.value then local oh = sd.value - sd.effective rightStr = NanamiDPS.formatNumber(sd.effective) .. " |cffcc8888+" .. NanamiDPS.formatNumber(oh) end rightStr = rightStr .. " |cffaaaaaa(" .. pct .. "%)" bar.textRight:SetText(rightStr) bar:ClearAllPoints() bar:SetPoint("TOPLEFT", page, "TOPLEFT", 0, -((i - 1) * (barH + barSpacing))) bar:SetPoint("TOPRIGHT", page, "TOPRIGHT", 0, -((i - 1) * (barH + barSpacing))) bar:SetHeight(barH) else bar:Hide() end end end ----------------------------------------------------------------------- -- Page 2: Summary ----------------------------------------------------------------------- function DetailView:RefreshSummary() local frame = detailFrame if not frame or not frame:IsShown() then return end local page = frame.pageSummary local seg = DataStore:GetSegment(frame.segmentIndex or 1) local entry, dataKey = GetPlayerEntry(frame.moduleName, seg, frame.playerName) -- Clear old lines for _, item in ipairs(page.lines) do if type(item) == "table" and item.left then item.left:SetText("") item.right:SetText("") elseif type(item) ~= "table" then item:SetText("") end end local lineIdx = 0 local labelHex = "|cffffd100" local localA = SFrames.ActiveTheme if localA and localA.sectionTitle then local lr, lg, lb = localA.sectionTitle[1], localA.sectionTitle[2], localA.sectionTitle[3] labelHex = "|c" .. SFrames.Theme.RGBtoHex(lr, lg, lb) end local function AddLine(left, right, leftColor, rightColor) lineIdx = lineIdx + 1 if not page.lines[lineIdx] then local leftFs = SFrames:CreateFontString(page, 10, "LEFT") local rightFs = SFrames:CreateFontString(page, 10, "RIGHT") page.lines[lineIdx] = { left = leftFs, right = rightFs } end local pair = page.lines[lineIdx] if type(pair) ~= "table" or not pair.left then local leftFs = SFrames:CreateFontString(page, 10, "LEFT") local rightFs = SFrames:CreateFontString(page, 10, "RIGHT") page.lines[lineIdx] = { left = leftFs, right = rightFs } pair = page.lines[lineIdx] end local yOff = -((lineIdx - 1) * 18) pair.left:ClearAllPoints() pair.left:SetPoint("TOPLEFT", page, "TOPLEFT", 4, yOff) pair.left:SetWidth(160) pair.left:SetText((leftColor or labelHex) .. left) pair.right:ClearAllPoints() pair.right:SetPoint("TOPRIGHT", page, "TOPRIGHT", -4, yOff) pair.right:SetWidth(180) pair.right:SetText((rightColor or "|cffffffff") .. (right or "")) end local function AddSeparator() lineIdx = lineIdx + 1 if not page.lines[lineIdx] then local fs = SFrames:CreateFontString(page, 6, "LEFT") page.lines[lineIdx] = fs end local fs = page.lines[lineIdx] if type(fs) == "table" and fs.left then fs.left:SetText("") fs.right:SetText("") elseif type(fs) ~= "table" then fs:ClearAllPoints() fs:SetPoint("TOPLEFT", page, "TOPLEFT", 0, -((lineIdx - 1) * 18)) fs:SetText("") end end if not entry then AddLine(L["No Data"], "") return end local playerName = frame.playerName local class = DataStore:GetClass(playerName) -- Player header local pAccentHex = (SFrames.ActiveTheme and SFrames.ActiveTheme.accentHex) or "ffFF88AA" AddLine(playerName, class or "", "|c" .. pAccentHex, "|cffaaaaaa") AddSeparator() -- Module-specific summary if dataKey == "damage" then local sum = entry._sum or 0 local ctime = entry._ctime or 1 local dps = sum / math.max(ctime, 1) AddLine(L["Total Damage"], NanamiDPS.formatNumber(sum)) AddLine(L["DPS"], NanamiDPS.round(dps, 1)) AddLine(L["Combat Time"], NanamiDPS.formatTime(ctime)) if entry.spells then local maxHit = 0 local spellCount = 0 for _, amount in pairs(entry.spells) do if amount > maxHit then maxHit = amount end spellCount = spellCount + 1 end AddLine(L["Top Spells"], tostring(spellCount)) end -- Rank in segment local mod = NanamiDPS.modules[frame.moduleName] if mod then local bars = mod:GetBars(seg) for i, b in ipairs(bars) do if b.id == playerName then AddLine(L["Rank"], "#" .. i .. " " .. L["of total"] .. " " .. table.getn(bars)) if b.percent then AddLine(L["Percent"], NanamiDPS.round(b.percent, 1) .. "%") end break end end end elseif dataKey == "healing" then local sum = entry._sum or 0 local esum = entry._esum or sum local overheal = sum - esum local ctime = entry._ctime or 1 local hps = esum / math.max(ctime, 1) local overPct = sum > 0 and NanamiDPS.round(overheal / sum * 100, 1) or 0 AddLine(L["Total Healing"], NanamiDPS.formatNumber(sum)) AddLine(L["Effective Healing"], NanamiDPS.formatNumber(esum)) AddLine(L["Overhealing"], "|cffcc8888+" .. NanamiDPS.formatNumber(overheal) .. " (" .. overPct .. "%)") AddLine(L["HPS"], NanamiDPS.round(hps, 1)) AddLine(L["Combat Time"], NanamiDPS.formatTime(ctime)) local mod = NanamiDPS.modules[frame.moduleName] if mod then local bars = mod:GetBars(seg) for i, b in ipairs(bars) do if b.id == playerName then AddLine(L["Rank"], "#" .. i .. " " .. L["of total"] .. " " .. table.getn(bars)) if b.percent then AddLine(L["Percent"], NanamiDPS.round(b.percent, 1) .. "%") end break end end end elseif dataKey == "damageTaken" then local sum = entry._sum or 0 local ctime = entry._ctime or 1 AddLine(L["Damage Taken"], NanamiDPS.formatNumber(sum)) AddLine(L["Per Second"], NanamiDPS.round(sum / math.max(ctime, 1), 1)) AddLine(L["Combat Time"], NanamiDPS.formatTime(ctime)) elseif dataKey == "deaths" then AddLine(L["Deaths"], tostring(entry._sum or 0)) if entry.events and table.getn(entry.events) > 0 then AddSeparator() AddLine(L["Death Log"], "", labelHex) local evts = entry.events local showCount = math.min(table.getn(evts), 5) for di = table.getn(evts), math.max(1, table.getn(evts) - showCount + 1), -1 do local death = evts[di] if death then AddLine(" #" .. di, death.timeStr or "", "|cffaaaaaa", "|cffaaaaaa") if death.events then local last = death.events[table.getn(death.events)] if last then local info = (last.spell or "?") .. " (" .. (last.source or "?") .. ")" AddLine(" " .. L["Killing Blow"], info, "|cffff4444", "|cffff8888") end end end end end elseif dataKey == "dispels" then AddLine(L["Dispels"], tostring(entry._sum or 0)) if entry.spells then AddSeparator() AddLine(L["Spell Breakdown"], "", labelHex) local sorted = {} for spell, count in pairs(entry.spells) do table.insert(sorted, { spell = spell, count = count }) end table.sort(sorted, function(a, b) return a.count > b.count end) for _, s in ipairs(sorted) do AddLine(" " .. s.spell, tostring(s.count), "|cffffffff", "|cffffffff") end end elseif dataKey == "interrupts" then AddLine(L["Interrupts"], tostring(entry._sum or 0)) if entry.spells then AddSeparator() AddLine(L["Spell Breakdown"], "", labelHex) local sorted = {} for spell, count in pairs(entry.spells) do table.insert(sorted, { spell = spell, count = count }) end table.sort(sorted, function(a, b) return a.count > b.count end) for _, s in ipairs(sorted) do AddLine(" " .. s.spell, tostring(s.count), "|cffffffff", "|cffffffff") end end elseif dataKey == "threat" then AddLine(L["Threat (Est.)"], NanamiDPS.formatNumber(entry._sum or 0)) local dmgEntry = seg.data.damage[playerName] local healEntry = seg.data.healing[playerName] AddSeparator() AddLine(L["Threat Breakdown"], "", labelHex) if dmgEntry then AddLine(" " .. L["Damage Contribution"], NanamiDPS.formatNumber(dmgEntry._sum)) end if healEntry then AddLine(" " .. L["Healing Contribution"], NanamiDPS.formatNumber(healEntry._sum) .. " (x0.5)") end elseif dataKey == "activity" then local segDuration = seg.duration if segDuration <= 0 then segDuration = GetTime() - (seg.startTime or GetTime()) end if segDuration <= 0 then segDuration = 1 end local activeTime = entry._activeTime or 0 local pct = math.min(activeTime / segDuration * 100, 100) AddLine(L["Activity"], NanamiDPS.round(pct, 1) .. "%") AddLine(L["Active Time"], NanamiDPS.formatTime(activeTime)) AddLine(L["Fight Duration"], NanamiDPS.formatTime(segDuration)) end end ----------------------------------------------------------------------- -- Page 3: Comparison (show all players as horizontal bars) ----------------------------------------------------------------------- function DetailView:RefreshCompare() local frame = detailFrame if not frame or not frame:IsShown() then return end local page = frame.pageCompare local seg = DataStore:GetSegment(frame.segmentIndex or 1) local mod = NanamiDPS.modules[frame.moduleName] if not mod then return end local bars = mod:GetBars(seg) if not bars then bars = {} end local maxVal = 0 for _, b in ipairs(bars) do if (b.value or 0) > maxVal then maxVal = b.value end end local barH = 16 local barSpacing = 2 -- Create bars as needed while table.getn(page.bars) < table.getn(bars) do local bar = CreateMiniBar(page, table.getn(page.bars) + 1) table.insert(page.bars, bar) end local playerName = frame.playerName for i = 1, math.max(table.getn(page.bars), table.getn(bars)) do local bar = page.bars[i] if not bar then break end if i <= table.getn(bars) then local d = bars[i] bar:Show() bar:SetMinMaxValues(0, maxVal > 0 and maxVal or 1) bar:SetValue(d.value or 0) local r, g, b = d.r or 0.6, d.g or 0.6, d.b or 0.6 if d.id == playerName then local A = SFrames.ActiveTheme if A and A.accent then bar:SetStatusBarColor(A.accent[1], A.accent[2], A.accent[3], 0.9) bar.bg:SetVertexColor(A.accent[1] * 0.3, A.accent[2] * 0.3, A.accent[3] * 0.3, 0.6) else bar:SetStatusBarColor(1, 0.53, 0.67, 0.9) bar.bg:SetVertexColor(0.25, 0.1, 0.15, 0.6) end else bar:SetStatusBarColor(r, g, b, 0.7) bar.bg:SetVertexColor(r * 0.2, g * 0.2, b * 0.2, 0.5) end bar.textLeft:SetText("|cffffffff" .. i .. ". " .. (d.name or L["Unknown"])) local valStr if d.valueText then valStr = d.valueText else valStr = NanamiDPS.formatNumber(d.value or 0) if d.percent then valStr = valStr .. " (" .. NanamiDPS.round(d.percent, 1) .. "%)" end end bar.textRight:SetText("|cffffffff" .. valStr) bar:ClearAllPoints() bar:SetPoint("TOPLEFT", page, "TOPLEFT", 0, -((i - 1) * (barH + barSpacing))) bar:SetPoint("TOPRIGHT", page, "TOPRIGHT", 0, -((i - 1) * (barH + barSpacing))) bar:SetHeight(barH) else bar:Hide() end end end ----------------------------------------------------------------------- -- Refresh current active tab ----------------------------------------------------------------------- function DetailView:Refresh() if not detailFrame or not detailFrame:IsShown() then return end self:SwitchTab(detailFrame.activeTab or 1) end ----------------------------------------------------------------------- -- Report detail data to chat ----------------------------------------------------------------------- local function GetSpellReportLines(count) if not detailFrame then return {} end local seg = DataStore:GetSegment(detailFrame.segmentIndex or 1) local entry, dataKey = GetPlayerEntry(detailFrame.moduleName, seg, detailFrame.playerName) if not entry then return {} end local spellData = {} local totalVal = 0 if entry.spells then for spell, amount in pairs(entry.spells) do table.insert(spellData, { name = spell, value = amount, effective = entry.effective and entry.effective[spell], }) totalVal = totalVal + amount end elseif entry.events then local evts = entry.events if table.getn(evts) > 0 then local lastDeath = evts[table.getn(evts)] if lastDeath.events then for _, evt in ipairs(lastDeath.events) do local spell = evt.spell or L["Unknown"] local amount = math.abs(evt.amount or 0) table.insert(spellData, { name = spell .. " (" .. (evt.source or "?") .. ")", value = amount, }) totalVal = totalVal + amount end end end elseif dataKey == "activity" then local activeTime = entry._activeTime or 0 table.insert(spellData, { name = L["Active Time"], value = activeTime }) totalVal = activeTime end table.sort(spellData, function(a, b) return a.value > b.value end) local lines = {} count = count or 5 for i = 1, math.min(count, table.getn(spellData)) do local sd = spellData[i] local pct = totalVal > 0 and NanamiDPS.round(sd.value / totalVal * 100, 1) or 0 local valStr = NanamiDPS.formatNumber(sd.value) if sd.effective and sd.effective ~= sd.value then valStr = NanamiDPS.formatNumber(sd.effective) .. " [+" .. NanamiDPS.formatNumber(sd.value - sd.effective) .. "]" end table.insert(lines, string.format("%d. %s - %s (%.1f%%)", i, sd.name, valStr, pct)) end return lines end local function GetSummaryReportLines(count) if not detailFrame then return {} end local seg = DataStore:GetSegment(detailFrame.segmentIndex or 1) local entry, dataKey = GetPlayerEntry(detailFrame.moduleName, seg, detailFrame.playerName) if not entry then return {} end local lines = {} if dataKey == "damage" then local sum = entry._sum or 0 local ctime = entry._ctime or 1 local dps = sum / math.max(ctime, 1) table.insert(lines, L["Total Damage"] .. ": " .. NanamiDPS.formatNumber(sum)) table.insert(lines, L["DPS"] .. ": " .. NanamiDPS.round(dps, 1)) table.insert(lines, L["Combat Time"] .. ": " .. NanamiDPS.formatTime(ctime)) elseif dataKey == "healing" then local sum = entry._sum or 0 local esum = entry._esum or sum local overheal = sum - esum local ctime = entry._ctime or 1 local hps = esum / math.max(ctime, 1) local overPct = sum > 0 and NanamiDPS.round(overheal / sum * 100, 1) or 0 table.insert(lines, L["Total Healing"] .. ": " .. NanamiDPS.formatNumber(sum)) table.insert(lines, L["Effective Healing"] .. ": " .. NanamiDPS.formatNumber(esum)) table.insert(lines, L["Overhealing"] .. ": +" .. NanamiDPS.formatNumber(overheal) .. " (" .. overPct .. "%)") table.insert(lines, L["HPS"] .. ": " .. NanamiDPS.round(hps, 1)) table.insert(lines, L["Combat Time"] .. ": " .. NanamiDPS.formatTime(ctime)) elseif dataKey == "damageTaken" then local sum = entry._sum or 0 local ctime = entry._ctime or 1 table.insert(lines, L["Damage Taken"] .. ": " .. NanamiDPS.formatNumber(sum)) table.insert(lines, L["Per Second"] .. ": " .. NanamiDPS.round(sum / math.max(ctime, 1), 1)) table.insert(lines, L["Combat Time"] .. ": " .. NanamiDPS.formatTime(ctime)) elseif dataKey == "deaths" then table.insert(lines, L["Deaths"] .. ": " .. tostring(entry._sum or 0)) elseif dataKey == "dispels" then table.insert(lines, L["Dispels"] .. ": " .. tostring(entry._sum or 0)) elseif dataKey == "interrupts" then table.insert(lines, L["Interrupts"] .. ": " .. tostring(entry._sum or 0)) elseif dataKey == "threat" then table.insert(lines, L["Threat (Est.)"] .. ": " .. NanamiDPS.formatNumber(entry._sum or 0)) elseif dataKey == "activity" then local segDuration = seg.duration if segDuration <= 0 then segDuration = GetTime() - (seg.startTime or GetTime()) end if segDuration <= 0 then segDuration = 1 end local activeTime = entry._activeTime or 0 local pct = math.min(activeTime / segDuration * 100, 100) table.insert(lines, L["Activity"] .. ": " .. NanamiDPS.round(pct, 1) .. "%") table.insert(lines, L["Active Time"] .. ": " .. NanamiDPS.formatTime(activeTime)) table.insert(lines, L["Fight Duration"] .. ": " .. NanamiDPS.formatTime(segDuration)) end return lines end function DetailView:ShowReport() if not detailFrame or not detailFrame:IsShown() then return end local Window = NanamiDPS.Window if not Window or not Window.ShowReportCustom then return end local playerName = detailFrame.playerName or L["Unknown"] local mod = NanamiDPS.modules[detailFrame.moduleName] local modName = mod and mod:GetName() or detailFrame.moduleName local activeTab = detailFrame.activeTab or 1 local segIdx = detailFrame.segmentIndex or 1 local segName if segIdx == 0 then segName = L["Total"] elseif segIdx == 1 then segName = L["Current"] else local segList = DataStore:GetSegmentList() segName = "Segment " .. segIdx for _, s in ipairs(segList) do if s.index == segIdx then segName = s.name; break end end end local accentHex = (SFrames.ActiveTheme and SFrames.ActiveTheme.accentHex) or "ffFF88AA" local tabName if activeTab == 1 then tabName = L["Spell Breakdown"] elseif activeTab == 2 then tabName = L["Summary"] else tabName = L["Comparison"] end local infoStr = "|c" .. accentHex .. playerName .. "|r - " .. modName .. " (" .. tabName .. ")" local headerFn, linesFn if activeTab == 3 then headerFn = function() return "Nanami DPS - " .. segName .. " " .. modName .. ":" end linesFn = function(count) local seg = DataStore:GetSegment(segIdx) if mod and mod.GetReportLines then return mod:GetReportLines(seg, count) end return {} end elseif activeTab == 2 then headerFn = function() return "Nanami DPS - " .. playerName .. " " .. modName .. " (" .. segName .. "):" end linesFn = function(count) return GetSummaryReportLines(count) end else headerFn = function() return "Nanami DPS - " .. playerName .. " " .. modName .. " " .. L["Spell Breakdown"] .. " (" .. segName .. "):" end linesFn = function(count) return GetSpellReportLines(count) end end Window:ShowReportCustom(detailFrame, infoStr, headerFn, linesFn) end ----------------------------------------------------------------------- -- Register refresh callback ----------------------------------------------------------------------- NanamiDPS:RegisterCallback("INIT", "DetailView", function() NanamiDPS:RegisterCallback("refresh", "DetailViewRefresh", function() if detailFrame and detailFrame:IsShown() then DetailView:Refresh() end end) end)