1046 lines
39 KiB
Lua
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)
|