Files
Nanami-DPS/DetailView.lua
rucky 5c3f2243c4 更新发送到功能
更新仇恨计算方式 还在开发
更新其他细节
2026-03-23 10:26:31 +08:00

1046 lines
39 KiB
Lua

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)