更新发送到功能

更新仇恨计算方式 还在开发
更新其他细节
This commit is contained in:
rucky
2026-03-23 10:26:31 +08:00
commit 5c3f2243c4
26 changed files with 6080 additions and 0 deletions

132
BarDisplay.lua Normal file
View File

@@ -0,0 +1,132 @@
local NanamiDPS = NanamiDPS
local DataStore = NanamiDPS.DataStore
local L = NanamiDPS.L
local BarDisplay = {}
NanamiDPS.BarDisplay = BarDisplay
local BAR_POOL = {}
function BarDisplay:CreateBar(parent, index)
local cfg = NanamiDPS.config or {}
local barHeight = cfg.barHeight or 16
local barSpacing = cfg.barSpacing or 1
local fontSize = cfg.fontSize or 10
local A = SFrames.ActiveTheme
local bar = CreateFrame("StatusBar", "NanamiDPSBar" .. parent:GetID() .. "_" .. index, parent)
bar:SetStatusBarTexture(SFrames:GetTexture())
bar:SetHeight(barHeight)
bar:SetMinMaxValues(0, 1)
bar:SetValue(0)
bar:SetFrameLevel(parent:GetFrameLevel() + 3)
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.6)
else
bar.bg:SetVertexColor(0.15, 0.15, 0.15, 0.6)
end
if cfg.showClassIcons and SFrames.CreateClassIcon then
bar.classIcon = SFrames:CreateClassIcon(bar, barHeight - 2)
bar.classIcon.overlay:SetPoint("LEFT", bar, "LEFT", 2, 0)
end
local textOffset = (cfg.showClassIcons and barHeight + 2) or 4
bar.textLeft = SFrames:CreateFontString(bar, fontSize, "LEFT")
bar.textLeft:SetPoint("LEFT", bar, "LEFT", textOffset, 0)
bar.textLeft:SetPoint("RIGHT", bar, "RIGHT", -130, 0)
bar.textRight = SFrames:CreateFontString(bar, fontSize, "RIGHT")
bar.textRight:SetPoint("RIGHT", bar, "RIGHT", -4, 0)
bar.textRight:SetWidth(126)
bar:EnableMouse(true)
bar:SetScript("OnEnter", function()
if this.onEnter then this.onEnter(this) end
end)
bar:SetScript("OnLeave", function()
if this.onLeave then this.onLeave(this) end
end)
bar:SetScript("OnMouseUp", function()
if this.onClick then this.onClick(this, arg1) end
end)
bar.index = index
return bar
end
function BarDisplay:LayoutBars(window)
local cfg = NanamiDPS.config or {}
local barHeight = cfg.barHeight or 16
local barSpacing = cfg.barSpacing or 1
local headerHeight = 22
for i, bar in ipairs(window.bars) do
bar:ClearAllPoints()
bar:SetPoint("TOPLEFT", window.content, "TOPLEFT", 1, -((i - 1) * (barHeight + barSpacing)))
bar:SetPoint("TOPRIGHT", window.content, "TOPRIGHT", -1, -((i - 1) * (barHeight + barSpacing)))
bar:SetHeight(barHeight)
end
end
function BarDisplay:UpdateBar(bar, data, maxVal, rank)
if not data then
bar:Hide()
return
end
bar:Show()
bar:SetMinMaxValues(0, maxVal > 0 and maxVal or 1)
bar:SetValue(data.value or 0)
local r, g, b = data.r or 0.6, data.g or 0.6, data.b or 0.6
bar:SetStatusBarColor(r, g, b, 1)
bar.bg:SetVertexColor(r * 0.25, g * 0.25, b * 0.25, 0.6)
local nameStr = rank .. ". " .. (data.name or L["Unknown"])
bar.textLeft:SetText(nameStr)
local valStr
if data.valueText then
valStr = data.valueText
else
valStr = NanamiDPS.formatNumber(data.value or 0)
if data.percent then
valStr = valStr .. " (" .. NanamiDPS.round(data.percent, 1) .. "%)"
end
end
bar.textRight:SetText(valStr)
if bar.classIcon and data.class and NanamiDPS.validClasses[data.class] then
SFrames:SetClassIcon(bar.classIcon, data.class)
elseif bar.classIcon then
bar.classIcon:Hide()
if bar.classIcon.overlay then bar.classIcon.overlay:Hide() end
end
bar.barData = data
end
function BarDisplay:SetBarCallbacks(bar, onEnter, onLeave, onClick)
bar.onEnter = onEnter
bar.onLeave = onLeave
bar.onClick = onClick
end
function BarDisplay:GetMaxBars(window)
local cfg = NanamiDPS.config or {}
local barHeight = cfg.barHeight or 16
local barSpacing = cfg.barSpacing or 1
local contentHeight = 200
if window.content then
local h = window.content:GetHeight()
if h and h > 10 then contentHeight = h end
end
local maxBars = math.floor(contentHeight / (barHeight + barSpacing))
return math.max(maxBars, 1)
end

78
Core.lua Normal file
View File

@@ -0,0 +1,78 @@
NanamiDPS = {}
NanamiDPS.version = "1.0.0"
NanamiDPS.modules = {}
NanamiDPS.moduleOrder = {}
NanamiDPS.windows = {}
NanamiDPS.callbacks = {
refresh = {},
}
local defaultConfig = {
barHeight = 16,
barSpacing = 1,
fontSize = 10,
trackAllUnits = false,
mergePets = true,
visible = true,
locked = false,
maxSegments = 10,
backdropAlpha = 0.92,
showClassIcons = true,
}
function NanamiDPS:RegisterModule(name, mod)
if self.modules[name] then return end
self.modules[name] = mod
table.insert(self.moduleOrder, name)
end
function NanamiDPS:GetModule(name)
return self.modules[name]
end
function NanamiDPS:FireCallback(name, a1, a2, a3, a4, a5)
if self.callbacks[name] then
for _, fn in pairs(self.callbacks[name]) do
fn(a1, a2, a3, a4, a5)
end
end
end
function NanamiDPS:RegisterCallback(name, id, fn)
if not self.callbacks[name] then
self.callbacks[name] = {}
end
self.callbacks[name][id] = fn
end
StaticPopupDialogs["NANAMI_DPS_CONFIRM"] = {
button1 = YES,
button2 = NO,
timeout = 0,
whileDead = 1,
hideOnEscape = 1,
}
local initFrame = CreateFrame("Frame")
initFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
initFrame:SetScript("OnEvent", function()
if not NanamiDPS_DB then
NanamiDPS_DB = {}
end
NanamiDPS_DB.config = NanamiDPS_DB.config or {}
for k, v in pairs(defaultConfig) do
if NanamiDPS_DB.config[k] == nil then
NanamiDPS_DB.config[k] = v
end
end
NanamiDPS.config = NanamiDPS_DB.config
NanamiDPS_DB.windows = NanamiDPS_DB.windows or {}
NanamiDPS_DB.windowPositions = NanamiDPS_DB.windowPositions or {}
NanamiDPS:FireCallback("INIT")
end)

249
DataStore.lua Normal file
View File

@@ -0,0 +1,249 @@
local NanamiDPS = NanamiDPS
local DataStore = {}
NanamiDPS.DataStore = DataStore
local function CreateEmptySegmentData()
return {
damage = {},
healing = {},
damageTaken = {},
deaths = {},
dispels = {},
interrupts = {},
threat = {},
activity = {},
classes = {},
}
end
local function CreateSegment(name)
return {
name = name or "Unknown",
startTime = GetTime(),
endTime = 0,
duration = 0,
data = CreateEmptySegmentData(),
}
end
DataStore.total = CreateSegment("Total")
DataStore.current = CreateSegment("Current")
DataStore.history = {}
DataStore.inCombat = false
function DataStore:GetSegment(index)
if index == 0 then
return self.total
elseif index == 1 then
return self.current
elseif index and self.history[index - 1] then
return self.history[index - 1]
end
return self.current
end
function DataStore:GetSegmentList()
local list = {}
table.insert(list, { index = 0, name = NanamiDPS.L["Total"] })
table.insert(list, { index = 1, name = NanamiDPS.L["Current"] })
for i, seg in ipairs(self.history) do
table.insert(list, { index = i + 1, name = seg.name })
end
return list
end
function DataStore:StartCombat()
if self.inCombat then return end
self.inCombat = true
self.current = CreateSegment("Current")
self.current.startTime = GetTime()
NanamiDPS:FireCallback("COMBAT_START")
end
function DataStore:StopCombat()
if not self.inCombat then return end
self.inCombat = false
self.current.endTime = GetTime()
self.current.duration = self.current.endTime - self.current.startTime
local hasData = false
for _ in pairs(self.current.data.damage) do hasData = true; break end
if not hasData then
for _ in pairs(self.current.data.healing) do hasData = true; break end
end
if hasData and self.current.duration >= 2 then
local segName = date("%H:%M:%S")
self.current.name = segName
table.insert(self.history, 1, self.current)
local maxSegs = (NanamiDPS.config and NanamiDPS.config.maxSegments) or 10
while table.getn(self.history) > maxSegs do
table.remove(self.history)
end
end
self.current = CreateSegment("Current")
NanamiDPS:FireCallback("COMBAT_STOP")
NanamiDPS:FireCallback("refresh")
end
function DataStore:AddDamage(source, spell, target, amount, school)
if not source or not amount then return end
amount = tonumber(amount)
if not amount or amount <= 0 then return end
local segs = { self.current, self.total }
for _, seg in ipairs(segs) do
local d = seg.data.damage
if not d[source] then
d[source] = { _sum = 0, _ctime = 1, _tick = 0, spells = {} }
end
d[source]._sum = d[source]._sum + amount
d[source].spells[spell] = (d[source].spells[spell] or 0) + amount
self:UpdateCombatTime(d[source])
end
end
function DataStore:AddDamageTaken(target, spell, source, amount, school)
if not target or not amount then return end
amount = tonumber(amount)
if not amount or amount <= 0 then return end
local segs = { self.current, self.total }
for _, seg in ipairs(segs) do
local d = seg.data.damageTaken
if not d[target] then
d[target] = { _sum = 0, _ctime = 1, _tick = 0, spells = {} }
end
d[target]._sum = d[target]._sum + amount
d[target].spells[spell] = (d[target].spells[spell] or 0) + amount
self:UpdateCombatTime(d[target])
end
end
function DataStore:AddHealing(source, spell, target, amount, effective)
if not source or not amount then return end
amount = tonumber(amount)
effective = tonumber(effective) or amount
if not amount or amount <= 0 then return end
local segs = { self.current, self.total }
for _, seg in ipairs(segs) do
local d = seg.data.healing
if not d[source] then
d[source] = { _sum = 0, _esum = 0, _ctime = 1, _tick = 0, spells = {}, effective = {} }
end
d[source]._sum = d[source]._sum + amount
d[source]._esum = d[source]._esum + effective
d[source].spells[spell] = (d[source].spells[spell] or 0) + amount
d[source].effective[spell] = (d[source].effective[spell] or 0) + effective
self:UpdateCombatTime(d[source])
end
end
function DataStore:AddDeath(playerName, deathLog)
local segs = { self.current, self.total }
for _, seg in ipairs(segs) do
local d = seg.data.deaths
if not d[playerName] then
d[playerName] = { _sum = 0, events = {} }
end
d[playerName]._sum = d[playerName]._sum + 1
table.insert(d[playerName].events, deathLog)
end
end
function DataStore:AddDispel(source, spell, target, aura)
if not source then return end
local segs = { self.current, self.total }
for _, seg in ipairs(segs) do
local d = seg.data.dispels
if not d[source] then
d[source] = { _sum = 0, spells = {} }
end
d[source]._sum = d[source]._sum + 1
d[source].spells[spell] = (d[source].spells[spell] or 0) + 1
end
end
function DataStore:AddInterrupt(source, spell, target, interrupted)
if not source then return end
local segs = { self.current, self.total }
for _, seg in ipairs(segs) do
local d = seg.data.interrupts
if not d[source] then
d[source] = { _sum = 0, spells = {} }
end
d[source]._sum = d[source]._sum + 1
d[source].spells[spell] = (d[source].spells[spell] or 0) + 1
end
end
function DataStore:AddThreat(source, amount)
if not source or not amount then return end
local segs = { self.current, self.total }
for _, seg in ipairs(segs) do
local d = seg.data.threat
if not d[source] then
d[source] = { _sum = 0 }
end
d[source]._sum = d[source]._sum + amount
end
end
function DataStore:UpdateActivity(source, timestamp)
if not source then return end
local segs = { self.current, self.total }
for _, seg in ipairs(segs) do
local d = seg.data.activity
if not d[source] then
d[source] = { _firstAction = timestamp, _lastAction = timestamp, _activeTime = 0, _tick = 0 }
end
local entry = d[source]
if entry._tick > 0 and (timestamp - entry._tick) < 5 then
entry._activeTime = entry._activeTime + (timestamp - entry._tick)
end
entry._tick = timestamp
entry._lastAction = timestamp
end
end
function DataStore:SetClass(name, class)
self.current.data.classes[name] = class
self.total.data.classes[name] = class
end
function DataStore:GetClass(name)
return self.current.data.classes[name] or self.total.data.classes[name]
end
function DataStore:UpdateCombatTime(entry)
if not entry._tick then entry._tick = 0 end
local now = GetTime()
if entry._tick == 0 then
entry._tick = now
return
end
local diff = now - entry._tick
if diff >= 5 then
entry._ctime = entry._ctime + 5
else
entry._ctime = entry._ctime + diff
end
entry._tick = now
end
function DataStore:ResetAll()
self.total = CreateSegment("Total")
self.current = CreateSegment("Current")
self.history = {}
NanamiDPS:FireCallback("refresh")
end

1045
DetailView.lua Normal file

File diff suppressed because it is too large Load Diff

216
Locale.lua Normal file
View File

@@ -0,0 +1,216 @@
NanamiDPS.L = {}
local L = NanamiDPS.L
local locale = GetLocale and GetLocale() or "enUS"
L["Damage Done"] = "Damage Done"
L["DPS"] = "DPS"
L["Damage Taken"] = "Damage Taken"
L["Healing Done"] = "Healing Done"
L["HPS"] = "HPS"
L["Overhealing"] = "Overhealing"
L["Deaths"] = "Deaths"
L["Dispels"] = "Dispels"
L["Interrupts"] = "Interrupts"
L["Threat (Est.)"] = "Threat (Est.)"
L["Activity"] = "Activity"
L["Enemy Damage Done"] = "Enemy Damage Done"
L["Damage by Spell"] = "Damage by Spell"
L["Healing by Spell"] = "Healing by Spell"
L["Current"] = "Current"
L["Total"] = "Total"
L["Reset"] = "Reset"
L["Settings"] = "Settings"
L["Report"] = "Report"
L["Lock"] = "Lock"
L["Unlock"] = "Unlock"
L["Segment"] = "Segment"
L["Mode"] = "Mode"
L["No Data"] = "No Data"
L["Pet"] = "Pet"
L["Auto Hit"] = "Auto Hit"
L["Details"] = "Details"
L["Overheal"] = "Overheal"
L["Per Second"] = "Per Second"
L["Total Amount"] = "Total Amount"
L["Active Time"] = "Active Time"
L["Percent"] = "Percent"
L["Died at"] = "Died at"
L["Killing Blow"] = "Killing Blow"
L["Last Events"] = "Last Events"
L["Report to Chat"] = "Report to Chat"
L["Reset Data?"] = "Do you wish to reset all data?"
L["Bar Height"] = "Bar Height"
L["Bar Spacing"] = "Bar Spacing"
L["Font Size"] = "Font Size"
L["Track All Units"] = "Track All Units"
L["Merge Pets"] = "Merge Pets with Owner"
L["Show Class Icons"] = "Show Class Icons"
L["Lock Windows"] = "Lock Windows"
L["Backdrop Alpha"] = "Backdrop Alpha"
L["Max Segments"] = "Max History Segments"
L["Hide Window"] = "Hide Window"
L["Shift-click to reset"] = "Shift-click to reset instantly"
L["Fight Duration"] = "Fight Duration"
L["Effective"] = "Effective"
L["Unknown"] = "Unknown"
L["Display Settings"] = "Display Settings"
L["Data Settings"] = "Data Settings"
L["Window Settings"] = "Window Settings"
L["Players"] = "Players"
L["Targets"] = "Targets"
L["Data Reset"] = "Data reset."
L["Windows Locked"] = "Windows locked"
L["Windows Unlocked"] = "Windows unlocked"
L["Commands"] = "Commands"
L["Show/Hide"] = "Show/Hide"
L["Reset All Data"] = "Reset all data"
L["Open Settings"] = "Open settings"
L["Lock/Unlock"] = "Lock/Unlock windows"
L["Report to chat"] = "Report to chat"
L["Create New Window"] = "Create new window"
L["Back"] = "Back"
L["Detail View"] = "Detail View"
L["Spell Breakdown"] = "Spell Breakdown"
L["Summary"] = "Summary"
L["Comparison"] = "Comparison"
L["Top Spells"] = "Top Spells"
L["Click to view details"] = "Click to view details"
L["Combat Time"] = "Combat Time"
L["Max Hit"] = "Max Hit"
L["Avg Hit"] = "Avg Hit"
L["Overheal %"] = "Overheal %"
L["Total Healing"] = "Total Healing"
L["Effective Healing"] = "Effective Healing"
L["Total Damage"] = "Total Damage"
L["Rank"] = "Rank"
L["of total"] = "of total"
L["Spell Chart"] = "Spell Chart"
L["Player Comparison"] = "Player Comparison"
L["Death Log"] = "Death Log"
L["Threat Breakdown"] = "Threat Breakdown"
L["Damage Contribution"] = "Damage Contribution"
L["Healing Contribution"] = "Healing Contribution"
L["Say"] = "Say"
L["Yell"] = "Yell"
L["Party"] = "Party"
L["Raid"] = "Raid"
L["Guild"] = "Guild"
L["Officer"] = "Officer"
L["Whisper"] = "Whisper"
L["Channel"] = "Channel"
L["Send Report"] = "Send Report"
L["Report Lines"] = "Lines"
L["Report Sent"] = "Report sent!"
L["No Report Data"] = "No data to report."
L["Enter Whisper Target"] = "Enter player name"
L["Owner"] = "Owner"
L["Threat Note"] = "Includes spell-specific threat modifiers"
L["Drag to Resize"] = "Drag to resize"
if locale == "zhCN" or locale == "zhTW" then
L["Damage Done"] = "造成伤害"
L["DPS"] = "每秒伤害"
L["Damage Taken"] = "受到伤害"
L["Healing Done"] = "治疗量"
L["HPS"] = "每秒治疗"
L["Overhealing"] = "过量治疗"
L["Deaths"] = "死亡"
L["Dispels"] = "驱散"
L["Interrupts"] = "打断"
L["Threat (Est.)"] = "仇恨(估算)"
L["Activity"] = "活跃时间"
L["Enemy Damage Done"] = "敌方伤害"
L["Damage by Spell"] = "技能伤害"
L["Healing by Spell"] = "技能治疗"
L["Current"] = "当前"
L["Total"] = "总计"
L["Reset"] = "重置"
L["Settings"] = "设置"
L["Report"] = "汇报"
L["Lock"] = "锁定"
L["Unlock"] = "解锁"
L["Segment"] = "战斗段"
L["Mode"] = "模式"
L["No Data"] = "暂无数据"
L["Pet"] = "宠物"
L["Auto Hit"] = "自动攻击"
L["Details"] = "详情"
L["Overheal"] = "过量治疗"
L["Per Second"] = "每秒"
L["Total Amount"] = "总量"
L["Active Time"] = "活跃时间"
L["Percent"] = "百分比"
L["Died at"] = "死亡于"
L["Killing Blow"] = "致死一击"
L["Last Events"] = "最后事件"
L["Report to Chat"] = "汇报到聊天"
L["Reset Data?"] = "确定重置所有数据吗?"
L["Bar Height"] = "条高度"
L["Bar Spacing"] = "条间距"
L["Font Size"] = "字体大小"
L["Track All Units"] = "追踪所有单位"
L["Merge Pets"] = "合并宠物数据"
L["Show Class Icons"] = "显示职业图标"
L["Lock Windows"] = "锁定窗口"
L["Backdrop Alpha"] = "背景透明度"
L["Max Segments"] = "最大历史段数"
L["Hide Window"] = "隐藏窗口"
L["Shift-click to reset"] = "Shift-点击立即重置"
L["Fight Duration"] = "战斗时长"
L["Effective"] = "有效值"
L["Unknown"] = "未知"
L["Display Settings"] = "显示设置"
L["Data Settings"] = "数据设置"
L["Window Settings"] = "窗口设置"
L["Players"] = "玩家"
L["Targets"] = "目标"
L["Data Reset"] = "数据已重置。"
L["Windows Locked"] = "窗口已锁定"
L["Windows Unlocked"] = "窗口已解锁"
L["Commands"] = "命令列表"
L["Show/Hide"] = "显示/隐藏"
L["Reset All Data"] = "重置所有数据"
L["Open Settings"] = "打开设置"
L["Lock/Unlock"] = "锁定/解锁窗口"
L["Report to chat"] = "汇报到聊天"
L["Create New Window"] = "创建新窗口"
L["Back"] = "返回"
L["Detail View"] = "详细信息"
L["Spell Breakdown"] = "技能分解"
L["Summary"] = "概要"
L["Comparison"] = "对比"
L["Top Spells"] = "技能排名"
L["Click to view details"] = "点击查看详情"
L["Combat Time"] = "战斗时间"
L["Max Hit"] = "最高一击"
L["Avg Hit"] = "平均一击"
L["Overheal %"] = "过量治疗%"
L["Total Healing"] = "总治疗量"
L["Effective Healing"] = "有效治疗"
L["Total Damage"] = "总伤害"
L["Rank"] = "排名"
L["of total"] = "占总量"
L["Spell Chart"] = "技能图表"
L["Player Comparison"] = "玩家对比"
L["Death Log"] = "死亡记录"
L["Threat Breakdown"] = "仇恨分解"
L["Damage Contribution"] = "伤害贡献"
L["Healing Contribution"] = "治疗贡献"
L["Say"] = ""
L["Yell"] = "喊叫"
L["Party"] = "小队"
L["Raid"] = "团队"
L["Guild"] = "公会"
L["Officer"] = "军官"
L["Whisper"] = "密语"
L["Channel"] = "频道"
L["Send Report"] = "发送汇报"
L["Report Lines"] = "行数"
L["Report Sent"] = "汇报已发送!"
L["No Report Data"] = "没有可汇报的数据。"
L["Enter Whisper Target"] = "输入玩家名称"
L["Owner"] = "主人"
L["Threat Note"] = "已计入技能仇恨系数"
L["Drag to Resize"] = "拖拽调整大小"
end

81
Modules/Activity.lua Normal file
View File

@@ -0,0 +1,81 @@
local NanamiDPS = NanamiDPS
local DataStore = NanamiDPS.DataStore
local L = NanamiDPS.L
local Activity = {}
function Activity:GetName()
return L["Activity"]
end
function Activity:GetBars(segment)
if not segment or not segment.data or not segment.data.activity then return {} end
local segDuration = segment.duration
if segDuration <= 0 then
segDuration = GetTime() - (segment.startTime or GetTime())
end
if segDuration <= 0 then segDuration = 1 end
local bars = {}
for name, entry in pairs(segment.data.activity) do
local class = DataStore:GetClass(name)
local r, g, b = NanamiDPS.GetClassColor(class)
if not NanamiDPS.validClasses[class] then
r, g, b = NanamiDPS.str2rgb(name)
r = r * 0.6 + 0.4
g = g * 0.6 + 0.4
b = b * 0.6 + 0.4
end
local activeTime = entry._activeTime or 0
local pct = segDuration > 0 and (activeTime / segDuration * 100) or 0
pct = math.min(pct, 100)
table.insert(bars, {
id = name,
name = name,
value = pct,
class = class,
r = r, g = g, b = b,
valueText = NanamiDPS.round(pct, 1) .. "% (" .. NanamiDPS.formatTime(activeTime) .. ")",
activeTime = activeTime,
})
end
table.sort(bars, function(a, b) return a.value > b.value end)
return bars
end
function Activity:GetTooltip(playerName, segment, tooltip)
if not segment or not segment.data.activity[playerName] then return end
local entry = segment.data.activity[playerName]
local segDuration = segment.duration
if segDuration <= 0 then
segDuration = GetTime() - (segment.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)
tooltip:AddLine("|cffffd100" .. playerName)
tooltip:AddDoubleLine("|cffffffff" .. L["Activity"], "|cffffffff" .. NanamiDPS.round(pct, 1) .. "%")
tooltip:AddDoubleLine("|cffffffff" .. L["Active Time"], "|cffffffff" .. NanamiDPS.formatTime(activeTime))
tooltip:AddDoubleLine("|cffffffff" .. L["Fight Duration"], "|cffffffff" .. NanamiDPS.formatTime(segDuration))
end
function Activity:GetReportLines(segment, count)
local bars = self:GetBars(segment)
local lines = {}
count = count or 5
for i = 1, math.min(count, table.getn(bars)) do
local d = bars[i]
table.insert(lines, string.format("%d. %s - %.1f%% active", i, d.name, d.value))
end
return lines
end
NanamiDPS:RegisterModule("Activity", Activity)

74
Modules/DPS.lua Normal file
View File

@@ -0,0 +1,74 @@
local NanamiDPS = NanamiDPS
local DataStore = NanamiDPS.DataStore
local L = NanamiDPS.L
local DPS = {}
function DPS:GetName()
return L["DPS"]
end
function DPS:GetBars(segment)
if not segment or not segment.data or not segment.data.damage then return {} end
local bars = {}
for name, entry in pairs(segment.data.damage) do
local class = DataStore:GetClass(name)
local r, g, b = NanamiDPS.GetClassColor(class)
if not NanamiDPS.validClasses[class] then
r, g, b = NanamiDPS.str2rgb(name)
r = r * 0.6 + 0.4
g = g * 0.6 + 0.4
b = b * 0.6 + 0.4
end
local dps = entry._sum / math.max(entry._ctime, 1)
table.insert(bars, {
id = name,
name = name,
value = dps,
class = class,
r = r, g = g, b = b,
valueText = NanamiDPS.round(dps, 1) .. " DPS",
totalDamage = entry._sum,
ctime = entry._ctime,
})
end
table.sort(bars, function(a, b) return a.value > b.value end)
local best = bars[1] and bars[1].value or 0
for _, bar in ipairs(bars) do
bar.percent = best > 0 and (bar.value / best * 100) or 0
end
return bars
end
function DPS:GetTooltip(playerName, segment, tooltip)
if not segment or not segment.data.damage[playerName] then return end
local entry = segment.data.damage[playerName]
local dps = entry._sum / math.max(entry._ctime, 1)
tooltip:AddLine("|cffffd100" .. playerName)
tooltip:AddDoubleLine("|cffffffff" .. L["DPS"], "|cffffffff" .. NanamiDPS.round(dps, 1))
tooltip:AddDoubleLine("|cffffffff" .. L["Damage Done"], "|cffffffff" .. NanamiDPS.formatNumber(entry._sum))
tooltip:AddDoubleLine("|cffffffff" .. L["Active Time"],
"|cffffffff" .. NanamiDPS.formatTime(entry._ctime))
NanamiDPS.Tooltip:ShowSpellDetail(playerName, entry.spells, nil, entry._sum, nil, tooltip)
end
function DPS:GetReportLines(segment, count)
local bars = self:GetBars(segment)
local lines = {}
count = count or 5
for i = 1, math.min(count, table.getn(bars)) do
local d = bars[i]
table.insert(lines, string.format("%d. %s - %.1f DPS (%s)",
i, d.name, d.value, NanamiDPS.formatNumber(d.totalDamage)))
end
return lines
end
NanamiDPS:RegisterModule("DPS", DPS)

90
Modules/DamageBySpell.lua Normal file
View File

@@ -0,0 +1,90 @@
local NanamiDPS = NanamiDPS
local DataStore = NanamiDPS.DataStore
local L = NanamiDPS.L
local DamageBySpell = {}
function DamageBySpell:GetName()
return L["Damage by Spell"]
end
function DamageBySpell:GetBars(segment)
if not segment or not segment.data or not segment.data.damage then return {} end
-- Aggregate all spells across all players
local spellTotals = {}
for name, entry in pairs(segment.data.damage) do
if entry.spells then
for spell, amount in pairs(entry.spells) do
spellTotals[spell] = (spellTotals[spell] or 0) + amount
end
end
end
local bars = {}
for spell, total in pairs(spellTotals) do
local r, g, b = NanamiDPS.str2rgb(spell)
r = r * 0.5 + 0.4
g = g * 0.5 + 0.4
b = b * 0.5 + 0.4
table.insert(bars, {
id = spell,
name = spell,
value = total,
r = r, g = g, b = b,
})
end
table.sort(bars, function(a, b) return a.value > b.value end)
local grandTotal = 0
for _, bar in ipairs(bars) do grandTotal = grandTotal + bar.value end
for _, bar in ipairs(bars) do
bar.percent = grandTotal > 0 and (bar.value / grandTotal * 100) or 0
end
return bars
end
function DamageBySpell:GetTooltip(spellName, segment, tooltip)
tooltip:AddLine("|cffffd100" .. (spellName or L["Unknown"]))
if segment and segment.data.damage then
local users = {}
for name, entry in pairs(segment.data.damage) do
if entry.spells and entry.spells[spellName] then
table.insert(users, { name = name, amount = entry.spells[spellName] })
end
end
table.sort(users, function(a, b) return a.amount > b.amount end)
if table.getn(users) > 0 then
local total = 0
for _, u in ipairs(users) do total = total + u.amount end
tooltip:AddDoubleLine("|cffffffff" .. L["Total Amount"], "|cffffffff" .. NanamiDPS.formatNumber(total))
tooltip:AddLine(" ")
tooltip:AddLine("|cffffd100" .. L["Players"] .. ":")
for _, u in ipairs(users) do
local pct = total > 0 and NanamiDPS.round(u.amount / total * 100, 1) or 0
tooltip:AddDoubleLine("|cffffffff" .. u.name,
"|cffffffff" .. NanamiDPS.formatNumber(u.amount) .. " (" .. pct .. "%)")
end
end
end
end
function DamageBySpell:GetReportLines(segment, count)
local bars = self:GetBars(segment)
local lines = {}
count = count or 5
for i = 1, math.min(count, table.getn(bars)) do
local d = bars[i]
table.insert(lines, string.format("%d. %s - %s (%.1f%%)",
i, d.name, NanamiDPS.formatNumber(d.value), d.percent))
end
return lines
end
NanamiDPS:RegisterModule("DamageBySpell", DamageBySpell)

79
Modules/DamageDone.lua Normal file
View File

@@ -0,0 +1,79 @@
local NanamiDPS = NanamiDPS
local DataStore = NanamiDPS.DataStore
local L = NanamiDPS.L
local DamageDone = {}
function DamageDone:GetName()
return L["Damage Done"]
end
function DamageDone:GetBars(segment)
if not segment or not segment.data or not segment.data.damage then return {} end
local bars = {}
for name, entry in pairs(segment.data.damage) do
local class = DataStore:GetClass(name)
local r, g, b = NanamiDPS.GetClassColor(class)
if not NanamiDPS.validClasses[class] then
r, g, b = NanamiDPS.str2rgb(name)
r = r * 0.6 + 0.4
g = g * 0.6 + 0.4
b = b * 0.6 + 0.4
end
local dmg = entry._sum or 0
local dps = dmg / math.max(entry._ctime or 0, 1)
table.insert(bars, {
id = name,
name = name,
value = dmg,
class = class,
r = r, g = g, b = b,
dps = dps,
ctime = entry._ctime,
})
end
table.sort(bars, function(a, b) return a.value > b.value end)
local total = 0
for _, bar in ipairs(bars) do total = total + bar.value end
for _, bar in ipairs(bars) do
bar.percent = total > 0 and (bar.value / total * 100) or 0
bar.valueText = NanamiDPS.formatNumber(bar.value)
.. " " .. NanamiDPS.formatNumber(bar.dps) .. "/s"
.. " (" .. NanamiDPS.round(bar.percent, 1) .. "%)"
end
return bars
end
function DamageDone:GetTooltip(playerName, segment, tooltip)
if not segment or not segment.data.damage[playerName] then return end
local entry = segment.data.damage[playerName]
tooltip:AddLine("|cffffd100" .. playerName)
tooltip:AddDoubleLine("|cffffffff" .. L["Damage Done"], "|cffffffff" .. NanamiDPS.formatNumber(entry._sum))
tooltip:AddDoubleLine("|cffffffff" .. L["DPS"],
"|cffffffff" .. NanamiDPS.round(entry._sum / math.max(entry._ctime, 1), 1))
tooltip:AddDoubleLine("|cffffffff" .. L["Active Time"],
"|cffffffff" .. NanamiDPS.formatTime(entry._ctime))
NanamiDPS.Tooltip:ShowSpellDetail(playerName, entry.spells, nil, entry._sum, nil, tooltip)
end
function DamageDone:GetReportLines(segment, count)
local bars = self:GetBars(segment)
local lines = {}
count = count or 5
for i = 1, math.min(count, table.getn(bars)) do
local d = bars[i]
table.insert(lines, string.format("%d. %s - %s (%.1f%%)",
i, d.name, NanamiDPS.formatNumber(d.value), d.percent))
end
return lines
end
NanamiDPS:RegisterModule("DamageDone", DamageDone)

69
Modules/DamageTaken.lua Normal file
View File

@@ -0,0 +1,69 @@
local NanamiDPS = NanamiDPS
local DataStore = NanamiDPS.DataStore
local L = NanamiDPS.L
local DamageTaken = {}
function DamageTaken:GetName()
return L["Damage Taken"]
end
function DamageTaken:GetBars(segment)
if not segment or not segment.data or not segment.data.damageTaken then return {} end
local bars = {}
for name, entry in pairs(segment.data.damageTaken) do
local class = DataStore:GetClass(name)
local r, g, b = NanamiDPS.GetClassColor(class)
if not NanamiDPS.validClasses[class] then
r, g, b = NanamiDPS.str2rgb(name)
r = r * 0.6 + 0.4
g = g * 0.6 + 0.4
b = b * 0.6 + 0.4
end
table.insert(bars, {
id = name,
name = name,
value = entry._sum or 0,
class = class,
r = r, g = g, b = b,
})
end
table.sort(bars, function(a, b) return a.value > b.value end)
local total = 0
for _, bar in ipairs(bars) do total = total + bar.value end
for _, bar in ipairs(bars) do
bar.percent = total > 0 and (bar.value / total * 100) or 0
end
return bars
end
function DamageTaken:GetTooltip(playerName, segment, tooltip)
if not segment or not segment.data.damageTaken[playerName] then return end
local entry = segment.data.damageTaken[playerName]
tooltip:AddLine("|cffffd100" .. playerName)
tooltip:AddDoubleLine("|cffffffff" .. L["Damage Taken"], "|cffffffff" .. NanamiDPS.formatNumber(entry._sum))
tooltip:AddDoubleLine("|cffffffff" .. L["Active Time"],
"|cffffffff" .. NanamiDPS.formatTime(entry._ctime))
NanamiDPS.Tooltip:ShowSpellDetail(playerName, entry.spells, nil, entry._sum, nil, tooltip)
end
function DamageTaken:GetReportLines(segment, count)
local bars = self:GetBars(segment)
local lines = {}
count = count or 5
for i = 1, math.min(count, table.getn(bars)) do
local d = bars[i]
table.insert(lines, string.format("%d. %s - %s (%.1f%%)",
i, d.name, NanamiDPS.formatNumber(d.value), d.percent))
end
return lines
end
NanamiDPS:RegisterModule("DamageTaken", DamageTaken)

94
Modules/Deaths.lua Normal file
View File

@@ -0,0 +1,94 @@
local NanamiDPS = NanamiDPS
local DataStore = NanamiDPS.DataStore
local L = NanamiDPS.L
local Deaths = {}
function Deaths:GetName()
return L["Deaths"]
end
function Deaths:GetBars(segment)
if not segment or not segment.data or not segment.data.deaths then return {} end
local bars = {}
for name, entry in pairs(segment.data.deaths) do
local class = DataStore:GetClass(name)
local r, g, b = NanamiDPS.GetClassColor(class)
if not NanamiDPS.validClasses[class] then
r, g, b = NanamiDPS.str2rgb(name)
r = r * 0.6 + 0.4
g = g * 0.6 + 0.4
b = b * 0.6 + 0.4
end
table.insert(bars, {
id = name,
name = name,
value = entry._sum or 0,
class = class,
r = r, g = g, b = b,
valueText = tostring(entry._sum or 0) .. "x",
events = entry.events,
})
end
table.sort(bars, function(a, b) return a.value > b.value end)
return bars
end
function Deaths:GetTooltip(playerName, segment, tooltip)
if not segment or not segment.data.deaths[playerName] then return end
local entry = segment.data.deaths[playerName]
tooltip:AddLine("|cffffd100" .. playerName .. " - " .. L["Deaths"])
tooltip:AddDoubleLine("|cffffffff" .. L["Deaths"], "|cffffffff" .. (entry._sum or 0))
if entry.events and table.getn(entry.events) > 0 then
local lastDeath = entry.events[table.getn(entry.events)]
tooltip:AddLine(" ")
tooltip:AddLine("|cffffd100" .. L["Last Events"] .. " (" .. (lastDeath.timeStr or "") .. "):")
if lastDeath.events then
local startIdx = math.max(1, table.getn(lastDeath.events) - 9)
for i = startIdx, table.getn(lastDeath.events) do
local evt = lastDeath.events[i]
if evt then
local timeAgo = ""
if lastDeath.time and evt.time then
timeAgo = string.format("-%.1fs", lastDeath.time - evt.time)
end
if evt.type == "damage" then
local line = string.format("|cffff4444-%s|r %s (%s)",
NanamiDPS.formatNumber(math.abs(evt.amount or 0)),
evt.spell or "?",
evt.source or "?")
tooltip:AddDoubleLine("|cffaaaaaa" .. timeAgo, line)
elseif evt.type == "heal" then
local line = string.format("|cff44ff44+%s|r %s (%s)",
NanamiDPS.formatNumber(evt.amount or 0),
evt.spell or "?",
evt.source or "?")
tooltip:AddDoubleLine("|cffaaaaaa" .. timeAgo, line)
end
end
end
end
end
end
function Deaths:GetReportLines(segment, count)
local bars = self:GetBars(segment)
local lines = {}
count = count or 5
for i = 1, math.min(count, table.getn(bars)) do
local d = bars[i]
table.insert(lines, string.format("%d. %s - %dx deaths", i, d.name, d.value))
end
return lines
end
NanamiDPS:RegisterModule("Deaths", Deaths)

78
Modules/Dispels.lua Normal file
View File

@@ -0,0 +1,78 @@
local NanamiDPS = NanamiDPS
local DataStore = NanamiDPS.DataStore
local L = NanamiDPS.L
local Dispels = {}
function Dispels:GetName()
return L["Dispels"]
end
function Dispels:GetBars(segment)
if not segment or not segment.data or not segment.data.dispels then return {} end
local bars = {}
for name, entry in pairs(segment.data.dispels) do
local class = DataStore:GetClass(name)
local r, g, b = NanamiDPS.GetClassColor(class)
if not NanamiDPS.validClasses[class] then
r, g, b = NanamiDPS.str2rgb(name)
r = r * 0.6 + 0.4
g = g * 0.6 + 0.4
b = b * 0.6 + 0.4
end
table.insert(bars, {
id = name,
name = name,
value = entry._sum or 0,
class = class,
r = r, g = g, b = b,
valueText = tostring(entry._sum or 0),
})
end
table.sort(bars, function(a, b) return a.value > b.value end)
local total = 0
for _, bar in ipairs(bars) do total = total + bar.value end
for _, bar in ipairs(bars) do
bar.percent = total > 0 and (bar.value / total * 100) or 0
end
return bars
end
function Dispels:GetTooltip(playerName, segment, tooltip)
if not segment or not segment.data.dispels[playerName] then return end
local entry = segment.data.dispels[playerName]
tooltip:AddLine("|cffffd100" .. playerName .. " - " .. L["Dispels"])
tooltip:AddDoubleLine("|cffffffff" .. L["Dispels"], "|cffffffff" .. (entry._sum or 0))
if entry.spells then
tooltip:AddLine(" ")
tooltip:AddLine("|cffffd100" .. L["Details"] .. ":")
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
tooltip:AddDoubleLine("|cffffffff" .. s.spell, "|cffffffff" .. s.count)
end
end
end
function Dispels:GetReportLines(segment, count)
local bars = self:GetBars(segment)
local lines = {}
count = count or 5
for i = 1, math.min(count, table.getn(bars)) do
local d = bars[i]
table.insert(lines, string.format("%d. %s - %d dispels", i, d.name, d.value))
end
return lines
end
NanamiDPS:RegisterModule("Dispels", Dispels)

View File

@@ -0,0 +1,90 @@
local NanamiDPS = NanamiDPS
local DataStore = NanamiDPS.DataStore
local L = NanamiDPS.L
local EnemyDamageDone = {}
function EnemyDamageDone:GetName()
return L["Enemy Damage Done"]
end
function EnemyDamageDone:GetBars(segment)
if not segment or not segment.data or not segment.data.damageTaken then return {} end
-- Aggregate damage by source across all targets
local bySource = {}
for targetName, entry in pairs(segment.data.damageTaken) do
if entry.spells then
for spell, amount in pairs(entry.spells) do
-- Extract real source from spell name if available
-- Otherwise group by spell
if not bySource[spell] then
bySource[spell] = 0
end
bySource[spell] = bySource[spell] + amount
end
end
end
local bars = {}
for spell, total in pairs(bySource) do
local r, g, b = NanamiDPS.str2rgb(spell)
r = r * 0.5 + 0.5
g = g * 0.3 + 0.2
b = b * 0.3 + 0.2
table.insert(bars, {
id = spell,
name = spell,
value = total,
r = r, g = g, b = b,
})
end
table.sort(bars, function(a, b) return a.value > b.value end)
local grandTotal = 0
for _, bar in ipairs(bars) do grandTotal = grandTotal + bar.value end
for _, bar in ipairs(bars) do
bar.percent = grandTotal > 0 and (bar.value / grandTotal * 100) or 0
end
return bars
end
function EnemyDamageDone:GetTooltip(spellName, segment, tooltip)
tooltip:AddLine("|cffffd100" .. (spellName or L["Unknown"]))
-- Find all targets hit by this spell
if segment and segment.data.damageTaken then
local targets = {}
for targetName, entry in pairs(segment.data.damageTaken) do
if entry.spells and entry.spells[spellName] then
table.insert(targets, { name = targetName, amount = entry.spells[spellName] })
end
end
table.sort(targets, function(a, b) return a.amount > b.amount end)
if table.getn(targets) > 0 then
tooltip:AddLine(" ")
tooltip:AddLine("|cffffd100" .. L["Targets"] .. ":")
for _, t in ipairs(targets) do
tooltip:AddDoubleLine("|cffffffff" .. t.name, "|cffffffff" .. NanamiDPS.formatNumber(t.amount))
end
end
end
end
function EnemyDamageDone:GetReportLines(segment, count)
local bars = self:GetBars(segment)
local lines = {}
count = count or 5
for i = 1, math.min(count, table.getn(bars)) do
local d = bars[i]
table.insert(lines, string.format("%d. %s - %s (%.1f%%)",
i, d.name, NanamiDPS.formatNumber(d.value), d.percent))
end
return lines
end
NanamiDPS:RegisterModule("EnemyDamageDone", EnemyDamageDone)

80
Modules/HPS.lua Normal file
View File

@@ -0,0 +1,80 @@
local NanamiDPS = NanamiDPS
local DataStore = NanamiDPS.DataStore
local L = NanamiDPS.L
local HPS = {}
function HPS:GetName()
return L["HPS"]
end
function HPS:GetBars(segment)
if not segment or not segment.data or not segment.data.healing then return {} end
local bars = {}
for name, entry in pairs(segment.data.healing) do
local class = DataStore:GetClass(name)
local r, g, b = NanamiDPS.GetClassColor(class)
if not NanamiDPS.validClasses[class] then
r, g, b = NanamiDPS.str2rgb(name)
r = r * 0.6 + 0.4
g = g * 0.6 + 0.4
b = b * 0.6 + 0.4
end
local effectiveVal = entry._esum or entry._sum
local hps = effectiveVal / math.max(entry._ctime, 1)
table.insert(bars, {
id = name,
name = name,
value = hps,
class = class,
r = r, g = g, b = b,
valueText = NanamiDPS.round(hps, 1) .. " HPS",
effectiveHeal = effectiveVal,
totalHeal = entry._sum,
ctime = entry._ctime,
})
end
table.sort(bars, function(a, b) return a.value > b.value end)
local best = bars[1] and bars[1].value or 0
for _, bar in ipairs(bars) do
bar.percent = best > 0 and (bar.value / best * 100) or 0
end
return bars
end
function HPS:GetTooltip(playerName, segment, tooltip)
if not segment or not segment.data.healing[playerName] then return end
local entry = segment.data.healing[playerName]
local effectiveVal = entry._esum or entry._sum
local hps = effectiveVal / math.max(entry._ctime, 1)
tooltip:AddLine("|cffffd100" .. playerName)
tooltip:AddDoubleLine("|cffffffff" .. L["HPS"], "|cffffffff" .. NanamiDPS.round(hps, 1))
tooltip:AddDoubleLine("|cffffffff" .. L["Healing Done"], "|cffffffff" .. NanamiDPS.formatNumber(effectiveVal))
tooltip:AddDoubleLine("|cffaaaaaa" .. L["Overheal"],
"|cffcc8888+" .. NanamiDPS.formatNumber(entry._sum - effectiveVal))
tooltip:AddDoubleLine("|cffffffff" .. L["Active Time"],
"|cffffffff" .. NanamiDPS.formatTime(entry._ctime))
NanamiDPS.Tooltip:ShowSpellDetail(playerName, entry.spells, entry.effective, entry._sum, effectiveVal, tooltip)
end
function HPS:GetReportLines(segment, count)
local bars = self:GetBars(segment)
local lines = {}
count = count or 5
for i = 1, math.min(count, table.getn(bars)) do
local d = bars[i]
table.insert(lines, string.format("%d. %s - %.1f HPS (%s)",
i, d.name, d.value, NanamiDPS.formatNumber(d.effectiveHeal)))
end
return lines
end
NanamiDPS:RegisterModule("HPS", HPS)

119
Modules/HealingBySpell.lua Normal file
View File

@@ -0,0 +1,119 @@
local NanamiDPS = NanamiDPS
local DataStore = NanamiDPS.DataStore
local L = NanamiDPS.L
local HealingBySpell = {}
function HealingBySpell:GetName()
return L["Healing by Spell"]
end
function HealingBySpell:GetBars(segment)
if not segment or not segment.data or not segment.data.healing then return {} end
local spellTotals = {}
local spellEffective = {}
for name, entry in pairs(segment.data.healing) do
if entry.spells then
for spell, amount in pairs(entry.spells) do
spellTotals[spell] = (spellTotals[spell] or 0) + amount
if entry.effective and entry.effective[spell] then
spellEffective[spell] = (spellEffective[spell] or 0) + entry.effective[spell]
end
end
end
end
local bars = {}
for spell, total in pairs(spellTotals) do
local effective = spellEffective[spell] or total
local overheal = total - effective
local r, g, b = NanamiDPS.str2rgb(spell)
r = r * 0.4 + 0.3
g = g * 0.5 + 0.4
b = b * 0.4 + 0.3
local valText = NanamiDPS.formatNumber(effective)
if overheal > 0 then
valText = valText .. " |cffcc8888+" .. NanamiDPS.formatNumber(overheal)
end
table.insert(bars, {
id = spell,
name = spell,
value = effective,
r = r, g = g, b = b,
valueText = valText,
totalHeal = total,
effectiveHeal = effective,
overheal = overheal,
})
end
table.sort(bars, function(a, b) return a.value > b.value end)
local grandTotal = 0
for _, bar in ipairs(bars) do grandTotal = grandTotal + bar.value end
for _, bar in ipairs(bars) do
bar.percent = grandTotal > 0 and (bar.value / grandTotal * 100) or 0
end
return bars
end
function HealingBySpell:GetTooltip(spellName, segment, tooltip)
tooltip:AddLine("|cffffd100" .. (spellName or L["Unknown"]))
if segment and segment.data.healing then
local users = {}
local totalAmount = 0
local totalEffective = 0
for name, entry in pairs(segment.data.healing) do
if entry.spells and entry.spells[spellName] then
local eff = (entry.effective and entry.effective[spellName]) or entry.spells[spellName]
table.insert(users, {
name = name,
amount = entry.spells[spellName],
effective = eff,
})
totalAmount = totalAmount + entry.spells[spellName]
totalEffective = totalEffective + eff
end
end
table.sort(users, function(a, b) return a.effective > b.effective end)
tooltip:AddDoubleLine("|cffffffff" .. L["Healing Done"], "|cffffffff" .. NanamiDPS.formatNumber(totalEffective))
if totalAmount - totalEffective > 0 then
tooltip:AddDoubleLine("|cffaaaaaa" .. L["Overheal"],
"|cffcc8888+" .. NanamiDPS.formatNumber(totalAmount - totalEffective))
end
if table.getn(users) > 0 then
tooltip:AddLine(" ")
tooltip:AddLine("|cffffd100" .. L["Players"] .. ":")
for _, u in ipairs(users) do
local oh = u.amount - u.effective
local rightStr = NanamiDPS.formatNumber(u.effective)
if oh > 0 then
rightStr = rightStr .. " |cffcc8888+" .. NanamiDPS.formatNumber(oh)
end
tooltip:AddDoubleLine("|cffffffff" .. u.name, "|cffffffff" .. rightStr)
end
end
end
end
function HealingBySpell:GetReportLines(segment, count)
local bars = self:GetBars(segment)
local lines = {}
count = count or 5
for i = 1, math.min(count, table.getn(bars)) do
local d = bars[i]
table.insert(lines, string.format("%d. %s - %s (%.1f%%)",
i, d.name, NanamiDPS.formatNumber(d.value), d.percent))
end
return lines
end
NanamiDPS:RegisterModule("HealingBySpell", HealingBySpell)

83
Modules/HealingDone.lua Normal file
View File

@@ -0,0 +1,83 @@
local NanamiDPS = NanamiDPS
local DataStore = NanamiDPS.DataStore
local L = NanamiDPS.L
local HealingDone = {}
function HealingDone:GetName()
return L["Healing Done"]
end
function HealingDone:GetBars(segment)
if not segment or not segment.data or not segment.data.healing then return {} end
local bars = {}
for name, entry in pairs(segment.data.healing) do
local class = DataStore:GetClass(name)
local r, g, b = NanamiDPS.GetClassColor(class)
if not NanamiDPS.validClasses[class] then
r, g, b = NanamiDPS.str2rgb(name)
r = r * 0.6 + 0.4
g = g * 0.6 + 0.4
b = b * 0.6 + 0.4
end
local effectiveVal = entry._esum or entry._sum
table.insert(bars, {
id = name,
name = name,
value = effectiveVal,
totalHeal = entry._sum,
effectiveHeal = effectiveVal,
overheal = entry._sum - effectiveVal,
class = class,
r = r, g = g, b = b,
})
end
table.sort(bars, function(a, b) return a.value > b.value end)
local total = 0
for _, bar in ipairs(bars) do total = total + bar.value end
for _, bar in ipairs(bars) do
bar.percent = total > 0 and (bar.value / total * 100) or 0
bar.valueText = NanamiDPS.formatNumber(bar.effectiveHeal)
if bar.overheal > 0 then
bar.valueText = bar.valueText .. " |cffcc8888+" .. NanamiDPS.formatNumber(bar.overheal)
end
end
return bars
end
function HealingDone:GetTooltip(playerName, segment, tooltip)
if not segment or not segment.data.healing[playerName] then return end
local entry = segment.data.healing[playerName]
local effectiveVal = entry._esum or entry._sum
local overhealVal = entry._sum - effectiveVal
tooltip:AddLine("|cffffd100" .. playerName)
tooltip:AddDoubleLine("|cffffffff" .. L["Healing Done"], "|cffffffff" .. NanamiDPS.formatNumber(effectiveVal))
tooltip:AddDoubleLine("|cffaaaaaa" .. L["Overheal"], "|cffcc8888+" .. NanamiDPS.formatNumber(overhealVal))
tooltip:AddDoubleLine("|cffffffff" .. L["HPS"],
"|cffffffff" .. NanamiDPS.round(effectiveVal / math.max(entry._ctime, 1), 1))
tooltip:AddDoubleLine("|cffffffff" .. L["Active Time"],
"|cffffffff" .. NanamiDPS.formatTime(entry._ctime))
NanamiDPS.Tooltip:ShowSpellDetail(playerName, entry.spells, entry.effective, entry._sum, effectiveVal, tooltip)
end
function HealingDone:GetReportLines(segment, count)
local bars = self:GetBars(segment)
local lines = {}
count = count or 5
for i = 1, math.min(count, table.getn(bars)) do
local d = bars[i]
local oh = d.overheal > 0 and (" [+" .. NanamiDPS.formatNumber(d.overheal) .. "]") or ""
table.insert(lines, string.format("%d. %s - %s%s (%.1f%%)",
i, d.name, NanamiDPS.formatNumber(d.effectiveHeal), oh, d.percent))
end
return lines
end
NanamiDPS:RegisterModule("HealingDone", HealingDone)

78
Modules/Interrupts.lua Normal file
View File

@@ -0,0 +1,78 @@
local NanamiDPS = NanamiDPS
local DataStore = NanamiDPS.DataStore
local L = NanamiDPS.L
local Interrupts = {}
function Interrupts:GetName()
return L["Interrupts"]
end
function Interrupts:GetBars(segment)
if not segment or not segment.data or not segment.data.interrupts then return {} end
local bars = {}
for name, entry in pairs(segment.data.interrupts) do
local class = DataStore:GetClass(name)
local r, g, b = NanamiDPS.GetClassColor(class)
if not NanamiDPS.validClasses[class] then
r, g, b = NanamiDPS.str2rgb(name)
r = r * 0.6 + 0.4
g = g * 0.6 + 0.4
b = b * 0.6 + 0.4
end
table.insert(bars, {
id = name,
name = name,
value = entry._sum or 0,
class = class,
r = r, g = g, b = b,
valueText = tostring(entry._sum or 0),
})
end
table.sort(bars, function(a, b) return a.value > b.value end)
local total = 0
for _, bar in ipairs(bars) do total = total + bar.value end
for _, bar in ipairs(bars) do
bar.percent = total > 0 and (bar.value / total * 100) or 0
end
return bars
end
function Interrupts:GetTooltip(playerName, segment, tooltip)
if not segment or not segment.data.interrupts[playerName] then return end
local entry = segment.data.interrupts[playerName]
tooltip:AddLine("|cffffd100" .. playerName .. " - " .. L["Interrupts"])
tooltip:AddDoubleLine("|cffffffff" .. L["Interrupts"], "|cffffffff" .. (entry._sum or 0))
if entry.spells then
tooltip:AddLine(" ")
tooltip:AddLine("|cffffd100" .. L["Details"] .. ":")
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
tooltip:AddDoubleLine("|cffffffff" .. s.spell, "|cffffffff" .. s.count)
end
end
end
function Interrupts:GetReportLines(segment, count)
local bars = self:GetBars(segment)
local lines = {}
count = count or 5
for i = 1, math.min(count, table.getn(bars)) do
local d = bars[i]
table.insert(lines, string.format("%d. %s - %d interrupts", i, d.name, d.value))
end
return lines
end
NanamiDPS:RegisterModule("Interrupts", Interrupts)

100
Modules/Overhealing.lua Normal file
View File

@@ -0,0 +1,100 @@
local NanamiDPS = NanamiDPS
local DataStore = NanamiDPS.DataStore
local L = NanamiDPS.L
local Overhealing = {}
function Overhealing:GetName()
return L["Overhealing"]
end
function Overhealing:GetBars(segment)
if not segment or not segment.data or not segment.data.healing then return {} end
local bars = {}
for name, entry in pairs(segment.data.healing) do
local class = DataStore:GetClass(name)
local r, g, b = NanamiDPS.GetClassColor(class)
if not NanamiDPS.validClasses[class] then
r, g, b = NanamiDPS.str2rgb(name)
r = r * 0.6 + 0.4
g = g * 0.6 + 0.4
b = b * 0.6 + 0.4
end
local effectiveVal = entry._esum or entry._sum
local overhealVal = entry._sum - effectiveVal
local overhealPct = entry._sum > 0 and (overhealVal / entry._sum * 100) or 0
if overhealVal > 0 then
table.insert(bars, {
id = name,
name = name,
value = overhealVal,
class = class,
r = r * 0.7 + 0.3, g = g * 0.5, b = b * 0.5,
valueText = NanamiDPS.formatNumber(overhealVal) .. " (" .. NanamiDPS.round(overhealPct, 1) .. "%)",
overhealPct = overhealPct,
totalHeal = entry._sum,
effectiveHeal = effectiveVal,
})
end
end
table.sort(bars, function(a, b) return a.value > b.value end)
local total = 0
for _, bar in ipairs(bars) do total = total + bar.value end
for _, bar in ipairs(bars) do
bar.percent = total > 0 and (bar.value / total * 100) or 0
end
return bars
end
function Overhealing:GetTooltip(playerName, segment, tooltip)
if not segment or not segment.data.healing[playerName] then return end
local entry = segment.data.healing[playerName]
local effectiveVal = entry._esum or entry._sum
local overhealVal = entry._sum - effectiveVal
local overhealPct = entry._sum > 0 and NanamiDPS.round(overhealVal / entry._sum * 100, 1) or 0
tooltip:AddLine("|cffffd100" .. playerName)
tooltip:AddDoubleLine("|cffcc8888" .. L["Overhealing"], "|cffcc8888" .. NanamiDPS.formatNumber(overhealVal))
tooltip:AddDoubleLine("|cffcc8888" .. L["Overheal %"], "|cffcc8888" .. overhealPct .. "%")
tooltip:AddDoubleLine("|cffffffff" .. L["Healing Done"], "|cffffffff" .. NanamiDPS.formatNumber(entry._sum))
tooltip:AddDoubleLine("|cffffffff" .. L["Effective"], "|cffffffff" .. NanamiDPS.formatNumber(effectiveVal))
if entry.spells and entry.effective then
tooltip:AddLine(" ")
tooltip:AddLine("|cffffd100" .. L["Details"] .. ":")
local sorted = {}
for spell, amount in pairs(entry.spells) do
table.insert(sorted, { spell = spell, amount = amount })
end
table.sort(sorted, function(a, b) return a.amount > b.amount end)
for _, s in ipairs(sorted) do
local eff = entry.effective[s.spell] or s.amount
local oh = s.amount - eff
if oh > 0 then
local pct = s.amount > 0 and NanamiDPS.round(oh / s.amount * 100, 1) or 0
tooltip:AddDoubleLine("|cffffffff" .. s.spell,
"|cffcc8888+" .. NanamiDPS.formatNumber(oh) .. " (" .. pct .. "%)")
end
end
end
end
function Overhealing:GetReportLines(segment, count)
local bars = self:GetBars(segment)
local lines = {}
count = count or 5
for i = 1, math.min(count, table.getn(bars)) do
local d = bars[i]
table.insert(lines, string.format("%d. %s - %s (%.1f%%)",
i, d.name, NanamiDPS.formatNumber(d.value), d.overhealPct))
end
return lines
end
NanamiDPS:RegisterModule("Overhealing", Overhealing)

110
Modules/ThreatEstimate.lua Normal file
View File

@@ -0,0 +1,110 @@
local NanamiDPS = NanamiDPS
local DataStore = NanamiDPS.DataStore
local L = NanamiDPS.L
local ThreatEstimate = {}
function ThreatEstimate:GetName()
return L["Threat (Est.)"]
end
local function ResolvePetOwner(name)
local stored = DataStore:GetClass(name)
if stored and not NanamiDPS.validClasses[stored] and stored ~= "__other__" then
local ownerClass = DataStore:GetClass(stored)
if ownerClass and NanamiDPS.validClasses[ownerClass] then
return stored, ownerClass
end
end
return nil, nil
end
function ThreatEstimate:GetBars(segment)
if not segment or not segment.data or not segment.data.threat then return {} end
local bars = {}
for name, entry in pairs(segment.data.threat) do
local class = DataStore:GetClass(name)
local r, g, b = NanamiDPS.GetClassColor(class)
local displayName = name
local ownerName, ownerClass = ResolvePetOwner(name)
if NanamiDPS.validClasses[class] then
-- player: use class color as-is
elseif ownerName and ownerClass then
r, g, b = NanamiDPS.GetClassColor(ownerClass)
r, g, b = r * 0.7, g * 0.7, b * 0.7
displayName = name .. " <" .. ownerName .. ">"
else
r, g, b = NanamiDPS.str2rgb(name)
r = r * 0.6 + 0.4
g = g * 0.6 + 0.4
b = b * 0.6 + 0.4
end
table.insert(bars, {
id = name,
name = displayName,
value = entry._sum or 0,
class = ownerClass or class,
r = r, g = g, b = b,
})
end
table.sort(bars, function(a, b) return a.value > b.value end)
local best = bars[1] and bars[1].value or 0
for _, bar in ipairs(bars) do
bar.percent = best > 0 and (bar.value / best * 100) or 0
end
return bars
end
function ThreatEstimate:GetTooltip(playerName, segment, tooltip)
if not segment or not segment.data.threat[playerName] then return end
local entry = segment.data.threat[playerName]
local ownerName, ownerClass = ResolvePetOwner(playerName)
tooltip:AddLine("|cffffd100" .. playerName)
if ownerName then
tooltip:AddDoubleLine("|cffffffff" .. L["Owner"], "|cffffffff" .. ownerName)
end
tooltip:AddDoubleLine("|cffffffff" .. L["Threat (Est.)"], "|cffffffff" .. NanamiDPS.formatNumber(entry._sum))
tooltip:AddLine(" ")
if ownerName then
tooltip:AddLine("|cffaaaaaa" .. L["Threat Note"])
else
local dmgEntry = segment.data.damage[playerName]
local healEntry = segment.data.healing[playerName]
if dmgEntry then
tooltip:AddDoubleLine("|cffffffff" .. L["Damage Done"],
"|cffffffff" .. NanamiDPS.formatNumber(dmgEntry._sum))
end
if healEntry then
tooltip:AddDoubleLine("|cffffffff" .. L["Healing Done"],
"|cffffffff" .. NanamiDPS.formatNumber(healEntry._sum) .. " (x0.5)")
end
tooltip:AddLine("|cffaaaaaa" .. L["Threat Note"])
end
end
function ThreatEstimate:GetReportLines(segment, count)
local bars = self:GetBars(segment)
local lines = {}
count = count or 5
for i = 1, math.min(count, table.getn(bars)) do
local d = bars[i]
table.insert(lines, string.format("%d. %s - %s (%.1f%%)",
i, d.name, NanamiDPS.formatNumber(d.value), d.percent))
end
return lines
end
NanamiDPS:RegisterModule("ThreatEstimate", ThreatEstimate)

34
Nanami-DPS.toc Normal file
View File

@@ -0,0 +1,34 @@
## Interface: 11200
## Title: |cffFF88AANanami|r DPS
## Author: Nanami
## Notes: Comprehensive combat data meter with Nanami UI integration
## Notes-zhCN: Nanami 综合战斗数据监测插件
## Version: 1.0.0
## Dependencies: Nanami-UI
## SavedVariablesPerCharacter: NanamiDPS_DB
Core.lua
Locale.lua
Utils.lua
DataStore.lua
Parser.lua
ParserVanilla.lua
Modules\DamageDone.lua
Modules\DamageTaken.lua
Modules\DPS.lua
Modules\HealingDone.lua
Modules\HPS.lua
Modules\Overhealing.lua
Modules\Deaths.lua
Modules\Dispels.lua
Modules\Interrupts.lua
Modules\ThreatEstimate.lua
Modules\Activity.lua
Modules\EnemyDamageDone.lua
Modules\DamageBySpell.lua
Modules\HealingBySpell.lua
BarDisplay.lua
Tooltip.lua
Window.lua
DetailView.lua
Options.lua

679
Options.lua Normal file
View File

@@ -0,0 +1,679 @@
local NanamiDPS = NanamiDPS
local L = NanamiDPS.L
local DataStore = NanamiDPS.DataStore
local Window = NanamiDPS.Window
local Options = {}
NanamiDPS.Options = Options
local optionsFrame = nil
local activeTabPage = nil
-----------------------------------------------------------------------
-- UI builder helpers
-----------------------------------------------------------------------
local function ApplyThemeColors(frame, category)
local A = SFrames.ActiveTheme
if not A then return end
if category == "header" and A.headerBg then
frame:SetBackdropColor(A.headerBg[1], A.headerBg[2], A.headerBg[3], A.headerBg[4] or 0.98)
elseif category == "panel" and A.panelBg then
frame:SetBackdropColor(A.panelBg[1], A.panelBg[2], A.panelBg[3], A.panelBg[4] or 0.9)
elseif category == "button" and A.buttonBg then
frame:SetBackdropColor(A.buttonBg[1], A.buttonBg[2], A.buttonBg[3], A.buttonBg[4] or 0.9)
if A.buttonBorder then
frame:SetBackdropBorderColor(A.buttonBorder[1], A.buttonBorder[2], A.buttonBorder[3], A.buttonBorder[4] or 0.8)
end
elseif category == "section" and A.sectionBg then
frame:SetBackdropColor(A.sectionBg[1], A.sectionBg[2], A.sectionBg[3], 0.5)
end
end
local function CreateSectionHeader(parent, text, yOffset)
local A = SFrames.ActiveTheme
local header = CreateFrame("Frame", nil, parent)
header:SetHeight(20)
header:SetPoint("TOPLEFT", parent, "TOPLEFT", 0, yOffset)
header:SetPoint("TOPRIGHT", parent, "TOPRIGHT", 0, yOffset)
header:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
tile = false, edgeSize = 0,
})
if A and A.accent then
header:SetBackdropColor(A.accent[1], A.accent[2], A.accent[3], 0.12)
else
header:SetBackdropColor(0.3, 0.3, 0.3, 0.12)
end
local accent = header:CreateTexture(nil, "ARTWORK")
accent:SetTexture("Interface\\Buttons\\WHITE8X8")
accent:SetWidth(3)
accent:SetHeight(14)
accent:SetPoint("LEFT", header, "LEFT", 2, 0)
if A and A.accent then
accent:SetVertexColor(A.accent[1], A.accent[2], A.accent[3], 0.9)
else
accent:SetVertexColor(0.7, 0.7, 0.7, 0.9)
end
local label = SFrames:CreateFontString(header, 10, "LEFT")
label:SetPoint("LEFT", accent, "RIGHT", 6, 0)
if A and A.sectionTitle then
label:SetTextColor(A.sectionTitle[1], A.sectionTitle[2], A.sectionTitle[3])
end
label:SetText(text)
return header
end
local function CreateStyledCheckbox(parent, label, x, y, getValue, setValue)
local A = SFrames.ActiveTheme
local container = CreateFrame("Frame", nil, parent)
container:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
container:SetWidth(240)
container:SetHeight(20)
container:EnableMouse(true)
local check = CreateFrame("CheckButton", nil, container)
check:SetWidth(14)
check:SetHeight(14)
check:SetPoint("LEFT", container, "LEFT", 0, 0)
check:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
edgeFile = "Interface\\Buttons\\WHITE8X8",
tile = false, edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 },
})
if A and A.checkBg then
check:SetBackdropColor(A.checkBg[1], A.checkBg[2], A.checkBg[3], A.checkBg[4] or 0.9)
else
check:SetBackdropColor(0.12, 0.06, 0.09, 0.9)
end
if A and A.checkBorder then
check:SetBackdropBorderColor(A.checkBorder[1], A.checkBorder[2], A.checkBorder[3], A.checkBorder[4] or 0.8)
else
check:SetBackdropBorderColor(0.45, 0.3, 0.38, 0.8)
end
local checkMark = check:CreateTexture(nil, "OVERLAY")
checkMark:SetTexture("Interface\\Buttons\\WHITE8X8")
checkMark:SetWidth(8)
checkMark:SetHeight(8)
checkMark:SetPoint("CENTER", 0, 0)
if A and A.checkFill then
checkMark:SetVertexColor(A.checkFill[1], A.checkFill[2], A.checkFill[3], A.checkFill[4] or 1)
else
checkMark:SetVertexColor(0.7, 0.7, 0.7, 1)
end
local checked = getValue() and true or false
check:SetChecked(checked)
checkMark:SetAlpha(checked and 1 or 0)
local text = SFrames:CreateFontString(container, 10, "LEFT")
text:SetPoint("LEFT", check, "RIGHT", 6, 0)
text:SetText(label)
if A and A.text then
text:SetTextColor(A.text[1], A.text[2], A.text[3])
end
local function ToggleCheck()
local val = not check.isChecked
check.isChecked = val
check:SetChecked(val)
checkMark:SetAlpha(val and 1 or 0)
setValue(val)
Window:RefreshAll(true)
end
check.isChecked = checked
check:SetScript("OnClick", ToggleCheck)
container:SetScript("OnMouseUp", ToggleCheck)
container:SetScript("OnEnter", function()
if A and A.checkHoverBorder then
check:SetBackdropBorderColor(A.checkHoverBorder[1], A.checkHoverBorder[2], A.checkHoverBorder[3], A.checkHoverBorder[4] or 0.9)
else
check:SetBackdropBorderColor(0.7, 0.7, 0.7, 0.9)
end
end)
container:SetScript("OnLeave", function()
if A and A.checkBorder then
check:SetBackdropBorderColor(A.checkBorder[1], A.checkBorder[2], A.checkBorder[3], A.checkBorder[4] or 0.8)
else
check:SetBackdropBorderColor(0.45, 0.3, 0.38, 0.8)
end
end)
return container
end
local function CreateStyledSlider(parent, label, x, y, minVal, maxVal, step, getValue, setValue, formatFn)
local A = SFrames.ActiveTheme
local container = CreateFrame("Frame", nil, parent)
container:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
container:SetWidth(260)
container:SetHeight(36)
local titleText = SFrames:CreateFontString(container, 9, "LEFT")
titleText:SetPoint("TOPLEFT", container, "TOPLEFT", 0, 0)
titleText:SetText(label)
if A and A.text then
titleText:SetTextColor(A.text[1], A.text[2], A.text[3])
end
local slider = CreateFrame("Slider", nil, container)
slider:SetPoint("TOPLEFT", container, "TOPLEFT", 0, -14)
slider:SetWidth(200)
slider:SetHeight(12)
slider:SetOrientation("HORIZONTAL")
slider:SetMinMaxValues(minVal, maxVal)
slider:SetValueStep(step)
slider:SetValue(getValue())
slider:EnableMouseWheel(1)
slider:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
edgeFile = "Interface\\Buttons\\WHITE8X8",
tile = false, edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 },
})
if A and A.sliderTrack then
slider:SetBackdropColor(A.sliderTrack[1], A.sliderTrack[2], A.sliderTrack[3], A.sliderTrack[4] or 0.9)
else
slider:SetBackdropColor(0.12, 0.06, 0.09, 0.9)
end
if A and A.buttonBorder then
slider:SetBackdropBorderColor(A.buttonBorder[1], A.buttonBorder[2], A.buttonBorder[3], A.buttonBorder[4] or 0.8)
else
slider:SetBackdropBorderColor(0.35, 0.25, 0.3, 0.8)
end
slider:SetThumbTexture("Interface\\Buttons\\WHITE8X8")
local thumb = slider:GetThumbTexture()
thumb:SetWidth(8)
thumb:SetHeight(12)
if A and A.sliderThumb then
thumb:SetVertexColor(A.sliderThumb[1], A.sliderThumb[2], A.sliderThumb[3], A.sliderThumb[4] or 0.95)
else
thumb:SetVertexColor(0.7, 0.7, 0.7, 0.95)
end
local valText = SFrames:CreateFontString(container, 10, "LEFT")
valText:SetPoint("LEFT", slider, "RIGHT", 10, 0)
valText:SetWidth(40)
local function UpdateDisplay(val)
if formatFn then
valText:SetText(formatFn(val))
else
valText:SetText(tostring(math.floor(val + 0.5)))
end
end
UpdateDisplay(getValue())
if A and A.accent then
valText:SetTextColor(A.accent[1], A.accent[2], A.accent[3], 1)
end
slider:SetScript("OnValueChanged", function()
local val = this:GetValue()
UpdateDisplay(val)
setValue(val)
end)
slider:SetScript("OnMouseUp", function()
Window:RefreshAll(true)
end)
slider:SetScript("OnMouseWheel", function()
local val = slider:GetValue()
if arg1 > 0 then
slider:SetValue(math.min(val + step, maxVal))
else
slider:SetValue(math.max(val - step, minVal))
end
end)
return container
end
local function CreateActionButton(parent, text, x, y, width, onClick)
local A = SFrames.ActiveTheme
local btn = CreateFrame("Button", nil, parent)
btn:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
btn:SetWidth(width or 120)
btn:SetHeight(22)
btn:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
edgeFile = "Interface\\Buttons\\WHITE8X8",
tile = false, edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 },
})
if A and A.buttonBg then
btn:SetBackdropColor(A.buttonBg[1], A.buttonBg[2], A.buttonBg[3], A.buttonBg[4] or 0.9)
else
btn:SetBackdropColor(0.2, 0.1, 0.15, 0.9)
end
if A and A.buttonBorder then
btn:SetBackdropBorderColor(A.buttonBorder[1], A.buttonBorder[2], A.buttonBorder[3], A.buttonBorder[4] or 0.8)
else
btn:SetBackdropBorderColor(0.5, 0.35, 0.42, 0.8)
end
local label = SFrames:CreateFontString(btn, 10, "CENTER")
label:SetAllPoints()
label:SetText(text)
btn:SetScript("OnClick", onClick)
btn:SetScript("OnEnter", function()
if A and A.buttonHoverBg then
this:SetBackdropColor(A.buttonHoverBg[1], A.buttonHoverBg[2], A.buttonHoverBg[3], A.buttonHoverBg[4] or 0.3)
else
this:SetBackdropColor(0.3, 0.3, 0.3, 0.2)
end
if A and A.accent then
this:SetBackdropBorderColor(A.accent[1], A.accent[2], A.accent[3], 0.9)
else
this:SetBackdropBorderColor(0.7, 0.7, 0.7, 0.9)
end
end)
btn:SetScript("OnLeave", function()
if A and A.buttonBg then
this:SetBackdropColor(A.buttonBg[1], A.buttonBg[2], A.buttonBg[3], A.buttonBg[4] or 0.9)
else
this:SetBackdropColor(0.2, 0.1, 0.15, 0.9)
end
if A and A.buttonBorder then
this:SetBackdropBorderColor(A.buttonBorder[1], A.buttonBorder[2], A.buttonBorder[3], A.buttonBorder[4] or 0.8)
else
this:SetBackdropBorderColor(0.5, 0.35, 0.42, 0.8)
end
end)
return btn
end
-----------------------------------------------------------------------
-- Tab system
-----------------------------------------------------------------------
local function CreateTab(parent, tabIndex, text, tabBar, contentParent, totalTabs)
local A = SFrames.ActiveTheme
local tabWidth = math.floor((parent:GetWidth() - 2) / totalTabs)
local tab = CreateFrame("Button", nil, tabBar)
tab:SetWidth(tabWidth)
tab:SetHeight(20)
if tabIndex == 1 then
tab:SetPoint("TOPLEFT", tabBar, "TOPLEFT", 1, 0)
else
tab:SetPoint("LEFT", tabBar.tabs[tabIndex - 1], "RIGHT", 0, 0)
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(text)
if A and A.tabText then
label:SetTextColor(A.tabText[1], A.tabText[2], A.tabText[3])
end
tab.label = label
tab.tabIndex = tabIndex
tab.page = CreateFrame("Frame", nil, contentParent)
tab.page:SetAllPoints(contentParent)
tab.page:Hide()
tab:SetScript("OnClick", function()
Options:SwitchTab(parent, tabIndex)
end)
tab:SetScript("OnEnter", function()
if activeTabPage ~= this.page 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 activeTabPage ~= this.page 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)
return tab
end
function Options:SwitchTab(frame, tabIndex)
local A = SFrames.ActiveTheme
local tabBar = frame.tabBar
if not tabBar or not tabBar.tabs then return end
for _, tab in ipairs(tabBar.tabs) do
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
tab.page:Hide()
end
local active = tabBar.tabs[tabIndex]
if active then
if A and A.tabActiveBg then
active:SetBackdropColor(A.tabActiveBg[1], A.tabActiveBg[2], A.tabActiveBg[3], A.tabActiveBg[4] or 0.3)
else
active:SetBackdropColor(0.3, 0.3, 0.3, 0.2)
end
if A and A.tabActiveText then
active.label:SetTextColor(A.tabActiveText[1], A.tabActiveText[2], A.tabActiveText[3])
else
active.label:SetTextColor(1, 0.85, 0.9)
end
active.page:Show()
activeTabPage = active.page
end
end
-----------------------------------------------------------------------
-- Build the options panel
-----------------------------------------------------------------------
function Options:CreatePanel()
if optionsFrame then return optionsFrame end
local A = SFrames.ActiveTheme
optionsFrame = CreateFrame("Frame", "NanamiDPSOptions", UIParent)
optionsFrame:SetWidth(340)
optionsFrame:SetHeight(460)
optionsFrame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
optionsFrame:SetMovable(true)
optionsFrame:EnableMouse(true)
optionsFrame:SetClampedToScreen(true)
optionsFrame:SetFrameStrata("DIALOG")
SFrames:CreateRoundBackdrop(optionsFrame)
-- Title bar
local titleBar = CreateFrame("Frame", nil, optionsFrame)
titleBar:SetHeight(24)
titleBar:SetPoint("TOPLEFT", optionsFrame, "TOPLEFT", 4, -4)
titleBar:SetPoint("TOPRIGHT", optionsFrame, "TOPRIGHT", -4, -4)
titleBar:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
tile = false, edgeSize = 0,
})
ApplyThemeColors(titleBar, "header")
if not (A and A.headerBg) then
titleBar:SetBackdropColor(0.06, 0.06, 0.08, 0.98)
end
titleBar:EnableMouse(true)
titleBar:RegisterForDrag("LeftButton")
titleBar:SetScript("OnDragStart", function() optionsFrame:StartMoving() end)
titleBar:SetScript("OnDragStop", function() optionsFrame:StopMovingOrSizing() end)
local titleText = SFrames:CreateFontString(titleBar, 11, "CENTER")
titleText:SetPoint("LEFT", titleBar, "LEFT", 8, 0)
titleText:SetPoint("RIGHT", titleBar, "RIGHT", -28, 0)
local accentHex = (A and A.accentHex) or "ffFF88AA"
titleText:SetText("|c" .. accentHex .. "Nanami|r DPS - " .. L["Settings"])
local verText = SFrames:CreateFontString(titleBar, 8, "RIGHT")
verText:SetPoint("RIGHT", titleBar, "RIGHT", -24, -4)
verText:SetText("|cff888888v" .. NanamiDPS.version)
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() optionsFrame:Hide() end)
closeBtn:SetScript("OnEnter", function()
closeIcon:SetVertexColor(1, 0.3, 0.3)
end)
closeBtn:SetScript("OnLeave", function()
closeIcon:SetVertexColor(1, 1, 1)
end)
-- Tab bar
local tabBar = CreateFrame("Frame", nil, optionsFrame)
tabBar:SetHeight(20)
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 = {}
optionsFrame.tabBar = tabBar
local contentArea = CreateFrame("Frame", nil, optionsFrame)
contentArea:SetPoint("TOPLEFT", tabBar, "BOTTOMLEFT", 12, -8)
contentArea:SetPoint("BOTTOMRIGHT", optionsFrame, "BOTTOMRIGHT", -12, 14)
local tabLabels = { L["Display Settings"], L["Data Settings"], L["Window Settings"] }
for i, name in ipairs(tabLabels) do
local tab = CreateTab(optionsFrame, i, name, tabBar, contentArea, table.getn(tabLabels))
tabBar.tabs[i] = tab
end
local cfg = NanamiDPS.config or {}
---------------------------------------------------------------
-- Tab 1: Display Settings
---------------------------------------------------------------
local page1 = tabBar.tabs[1].page
local y = 0
CreateSectionHeader(page1, L["Bar Height"], y)
y = y - 24
CreateStyledSlider(page1, "", 8, y, 10, 30, 1,
function() return cfg.barHeight or 16 end,
function(v) cfg.barHeight = math.floor(v + 0.5) end)
y = y - 34
CreateSectionHeader(page1, L["Bar Spacing"], y)
y = y - 24
CreateStyledSlider(page1, "", 8, y, 0, 5, 1,
function() return cfg.barSpacing or 1 end,
function(v) cfg.barSpacing = math.floor(v + 0.5) end)
y = y - 34
CreateSectionHeader(page1, L["Font Size"], y)
y = y - 24
CreateStyledSlider(page1, "", 8, y, 7, 16, 1,
function() return cfg.fontSize or 10 end,
function(v) cfg.fontSize = math.floor(v + 0.5) end)
y = y - 34
CreateSectionHeader(page1, L["Backdrop Alpha"], y)
y = y - 24
CreateStyledSlider(page1, "", 8, y, 0, 100, 5,
function() return (cfg.backdropAlpha or 0.92) * 100 end,
function(v) cfg.backdropAlpha = v / 100 end,
function(v) return math.floor(v + 0.5) .. "%" end)
y = y - 40
CreateStyledCheckbox(page1, L["Show Class Icons"], 8, y,
function() return cfg.showClassIcons end,
function(v) cfg.showClassIcons = v end)
---------------------------------------------------------------
-- Tab 2: Data Settings
---------------------------------------------------------------
local page2 = tabBar.tabs[2].page
y = 0
CreateSectionHeader(page2, L["Data Settings"], y)
y = y - 28
CreateStyledCheckbox(page2, L["Track All Units"], 8, y,
function() return cfg.trackAllUnits end,
function(v) cfg.trackAllUnits = v end)
y = y - 26
CreateStyledCheckbox(page2, L["Merge Pets"], 8, y,
function() return cfg.mergePets end,
function(v) cfg.mergePets = v end)
y = y - 36
CreateSectionHeader(page2, L["Max Segments"], y)
y = y - 24
CreateStyledSlider(page2, "", 8, y, 3, 30, 1,
function() return cfg.maxSegments or 10 end,
function(v) cfg.maxSegments = math.floor(v + 0.5) end)
y = y - 50
CreateSectionHeader(page2, L["Reset"], y)
y = y - 28
CreateActionButton(page2, "|cffff4444" .. L["Reset All Data"], 8, y, 150, function()
local dialog = StaticPopupDialogs["NANAMI_DPS_CONFIRM"]
dialog.text = L["Reset Data?"]
dialog.OnAccept = function() DataStore:ResetAll() end
StaticPopup_Show("NANAMI_DPS_CONFIRM")
end)
---------------------------------------------------------------
-- Tab 3: Window Settings
---------------------------------------------------------------
local page3 = tabBar.tabs[3].page
y = 0
CreateSectionHeader(page3, L["Window Settings"], y)
y = y - 28
CreateStyledCheckbox(page3, L["Lock Windows"], 8, y,
function() return cfg.locked end,
function(v) cfg.locked = v end)
y = y - 36
CreateSectionHeader(page3, L["Create New Window"], y)
y = y - 28
CreateActionButton(page3, L["Create New Window"], 8, y, 150, function()
Window:CreateNewWindow()
end)
-- Activate first tab
self:SwitchTab(optionsFrame, 1)
optionsFrame:Hide()
return optionsFrame
end
function Options:Toggle()
local panel = self:CreatePanel()
if panel:IsShown() then
panel:Hide()
else
panel:Show()
end
end
-----------------------------------------------------------------------
-- Slash commands
-----------------------------------------------------------------------
SLASH_NANAMIDPS1 = "/nanami"
SLASH_NANAMIDPS2 = "/ndps"
SlashCmdList["NANAMIDPS"] = function(msg)
msg = string.lower(msg or "")
msg = NanamiDPS.trim(msg)
if msg == "" or msg == "toggle" then
Window:ToggleVisibility()
elseif msg == "reset" then
DataStore:ResetAll()
local chatHex = (SFrames.ActiveTheme and SFrames.ActiveTheme.accentHex) or "ffFF88AA"
DEFAULT_CHAT_FRAME:AddMessage("|c" .. chatHex .. "Nanami DPS|r: " .. L["Data Reset"])
elseif msg == "config" or msg == "options" or msg == "settings" then
Options:Toggle()
elseif msg == "lock" then
local cfg = NanamiDPS.config or {}
cfg.locked = not cfg.locked
local chatHex = (SFrames.ActiveTheme and SFrames.ActiveTheme.accentHex) or "ffFF88AA"
DEFAULT_CHAT_FRAME:AddMessage("|c" .. chatHex .. "Nanami DPS|r: " .. (cfg.locked and L["Windows Locked"] or L["Windows Unlocked"]))
elseif msg == "report" then
local win = NanamiDPS.windows and NanamiDPS.windows[1]
if win then
local mod = NanamiDPS.modules[win.activeModuleName]
local seg = DataStore:GetSegment(win.segmentIndex or 1)
if mod and mod.GetReportLines then
local lines = mod:GetReportLines(seg, 5)
local modName = mod:GetName()
local segName = (win.segmentIndex == 0) and L["Total"] or L["Current"]
local chatHex = (SFrames.ActiveTheme and SFrames.ActiveTheme.accentHex) or "ffFF88AA"
DEFAULT_CHAT_FRAME:AddMessage("|c" .. chatHex .. "Nanami DPS|r - " .. segName .. " " .. modName .. ":")
for _, line in ipairs(lines) do
DEFAULT_CHAT_FRAME:AddMessage(" " .. line)
end
end
end
elseif string.find(msg, "^report ") then
local _, _, channel, countStr = string.find(msg, "^report%s+(%a+)%s*(%d*)")
local count = tonumber(countStr) or 5
local win = NanamiDPS.windows and NanamiDPS.windows[1]
if win and channel then
local mod = NanamiDPS.modules[win.activeModuleName]
local seg = DataStore:GetSegment(win.segmentIndex or 1)
if mod and mod.GetReportLines then
local lines = mod:GetReportLines(seg, count)
local modName = mod:GetName()
local segName = (win.segmentIndex == 0) and L["Total"] or L["Current"]
channel = string.upper(channel)
SendChatMessage("Nanami DPS - " .. segName .. " " .. modName .. ":", channel)
for _, line in ipairs(lines) do
SendChatMessage(line, channel)
end
end
end
elseif msg == "new" or msg == "newwindow" then
Window:CreateNewWindow()
else
local chatHex = (SFrames.ActiveTheme and SFrames.ActiveTheme.accentHex) or "ffFF88AA"
DEFAULT_CHAT_FRAME:AddMessage("|c" .. chatHex .. "Nanami DPS|r " .. L["Commands"] .. ":")
DEFAULT_CHAT_FRAME:AddMessage(" /ndps toggle - " .. L["Show/Hide"])
DEFAULT_CHAT_FRAME:AddMessage(" /ndps reset - " .. L["Reset All Data"])
DEFAULT_CHAT_FRAME:AddMessage(" /ndps config - " .. L["Open Settings"])
DEFAULT_CHAT_FRAME:AddMessage(" /ndps lock - " .. L["Lock/Unlock"])
DEFAULT_CHAT_FRAME:AddMessage(" /ndps report - " .. L["Report to chat"])
DEFAULT_CHAT_FRAME:AddMessage(" /ndps report [channel] [count]")
DEFAULT_CHAT_FRAME:AddMessage(" /ndps new - " .. L["Create New Window"])
end
end
NanamiDPS:RegisterCallback("INIT", "Options", function()
Options:CreatePanel()
end)

452
Parser.lua Normal file
View File

@@ -0,0 +1,452 @@
local NanamiDPS = NanamiDPS
local DataStore = NanamiDPS.DataStore
local Parser = CreateFrame("Frame")
NanamiDPS.Parser = Parser
local validUnits = { ["player"] = true }
for i = 1, 4 do validUnits["party" .. i] = true end
for i = 1, 40 do validUnits["raid" .. i] = true end
local validPets = { ["pet"] = true }
for i = 1, 4 do validPets["partypet" .. i] = true end
for i = 1, 40 do validPets["raidpet" .. i] = true end
Parser.validUnits = validUnits
Parser.validPets = validPets
-----------------------------------------------------------------------
-- Spell threat coefficients (vanilla / classic values, max rank)
-- Reference: classic-warrior wiki, warcrafttavern.com
-- bonus = fixed threat added ON TOP of damage
-- mult = multiplier applied to the DAMAGE portion (default 1.0)
-----------------------------------------------------------------------
local spellThreatData = {
["Heroic Strike"] = { bonus = 173 },
["Shield Slam"] = { bonus = 254 },
["Revenge"] = { bonus = 270, mult = 2.25 },
["Shield Bash"] = { bonus = 156, mult = 1.5 },
["Cleave"] = { bonus = 100 },
["Execute"] = { mult = 1.25 },
["Hamstring"] = { bonus = 135, mult = 1.25 },
["Thunder Clap"] = { mult = 2.5 },
["Overpower"] = { mult = 0.75 },
["Disarm"] = { bonus = 99 },
["Sunder Armor"] = { bonus = 261 },
["Maul"] = { mult = 1.75 },
["Swipe"] = { mult = 1.75 },
["Faerie Fire (Feral)"] = { bonus = 108 },
["Mind Blast"] = { mult = 2.0 },
["Holy Nova"] = { mult = 0 },
["Earth Shock"] = { mult = 2.0 },
["Searing Pain"] = { mult = 2.0 },
["Distracting Shot"] = { bonus = 600 },
["Scorpid Poison"] = { bonus = 5 },
["Intimidation"] = { bonus = 580 },
["Life Tap"] = { mult = 0 },
["Counterspell"] = { bonus = 300 },
["Mocking Blow"] = { bonus = 250 },
}
do
local cnAliases = {
["\232\139\177\229\139\135\230\137\147\229\135\187"] = "Heroic Strike",
["\231\155\190\231\137\140\231\140\155\229\135\187"] = "Shield Slam",
["\229\164\141\228\187\135"] = "Revenge",
["\231\155\190\229\135\187"] = "Shield Bash",
["\233\161\186\229\138\136\230\150\169"] = "Cleave",
["\230\150\169\230\157\128"] = "Execute",
["\230\150\173\231\173\139"] = "Hamstring",
["\233\155\183\233\156\134\228\184\128\229\135\187"] = "Thunder Clap",
["\229\142\139\229\136\182"] = "Overpower",
["\231\188\180\230\162\176"] = "Disarm",
["\231\160\180\231\148\178\230\148\187\229\135\187"] = "Sunder Armor",
["\230\167\152\229\135\187"] = "Maul",
["\230\140\165\229\135\187"] = "Swipe",
["\231\178\190\231\129\181\228\185\139\231\129\171(\233\135\142\233\135\145)"] = "Faerie Fire (Feral)",
["\229\191\131\231\129\181\233\156\135\231\136\134"] = "Mind Blast",
["\231\165\158\229\156\163\230\150\176\230\152\159"] = "Holy Nova",
["\229\156\176\233\156\135\230\156\175"] = "Earth Shock",
["\231\129\188\231\131\173\228\185\139\231\151\155"] = "Searing Pain",
["\230\137\176\228\185\177\229\176\132\229\135\187"] = "Distracting Shot",
["\232\157\157\230\175\146"] = "Scorpid Poison",
["\232\131\129\232\191\171"] = "Intimidation",
["\231\148\159\229\145\189\229\136\134\230\181\129"] = "Life Tap",
["\229\143\141\229\136\182\233\173\148\230\179\149"] = "Counterspell",
["\229\152\178\229\188\132\230\137\147\229\135\187"] = "Mocking Blow",
}
for cn, en in pairs(cnAliases) do
if spellThreatData[en] then
spellThreatData[cn] = spellThreatData[en]
end
end
end
local classThreatMod = {
ROGUE = 0.8,
}
-----------------------------------------------------------------------
-- Stance / form / buff threat modifier detection
-- Scans UnitBuff textures to detect known threat-altering states.
-- Cached per-unit for 2 seconds to avoid excessive buff scanning.
-----------------------------------------------------------------------
local stanceScanPatterns = {
{ pat = "DefensiveStance", mod = 1.3 },
{ pat = "OffensiveStance", mod = 0.8 },
{ pat = "Racial_Avatar", mod = 0.8 },
{ pat = "BerserkStance", mod = 0.8 },
{ pat = "BearForm", mod = 1.3 },
{ pat = "CatForm", mod = 0.8 },
}
local buffScanPatterns = {
{ pat = "SealOfSalvation", mod = 0.7 },
}
local function ScanUnitThreatMod(unit)
if not unit or not UnitExists(unit) then return 1.0 end
local stanceMod = 1.0
local buffMod = 1.0
for i = 1, 32 do
local texture = UnitBuff(unit, i)
if not texture then break end
for _, s in ipairs(stanceScanPatterns) do
if string.find(texture, s.pat) then
stanceMod = s.mod
end
end
for _, b in ipairs(buffScanPatterns) do
if string.find(texture, b.pat) then
buffMod = buffMod * b.mod
end
end
end
return stanceMod * buffMod
end
local stanceModCache = {}
local STANCE_CACHE_TTL = 2
function Parser:GetUnitThreatMod(name)
local now = GetTime()
local cached = stanceModCache[name]
if cached and (now - cached.time) < STANCE_CACHE_TTL then
return cached.mod
end
local unit = self:UnitByName(name)
local mod = ScanUnitThreatMod(unit)
stanceModCache[name] = { mod = mod, time = now }
return mod
end
function Parser:CalculateSpellThreat(source, spell, damage)
if not damage or damage <= 0 then return 0 end
local threat = damage
local coeff = spell and spellThreatData[spell]
if coeff then
threat = damage * (coeff.mult or 1.0) + (coeff.bonus or 0)
end
local class = DataStore:GetClass(source)
if class and classThreatMod[class] then
threat = threat * classThreatMod[class]
end
threat = threat * self:GetUnitThreatMod(source)
return math.max(threat, 0)
end
function Parser:CalculateHealThreat(source, amount)
if not amount or amount <= 0 then return 0 end
local threat = amount * 0.5
local class = DataStore:GetClass(source)
if class and classThreatMod[class] then
threat = threat * classThreatMod[class]
end
threat = threat * self:GetUnitThreatMod(source)
return threat
end
local unit_cache = {}
function Parser:UnitByName(name)
if unit_cache[name] and UnitName(unit_cache[name]) == name then
return unit_cache[name]
end
for unit in pairs(validUnits) do
if UnitName(unit) == name then
unit_cache[name] = unit
return unit
end
end
for unit in pairs(validPets) do
if UnitName(unit) == name then
unit_cache[name] = unit
return unit
end
end
return nil
end
function Parser:ScanName(name)
if not name then return nil end
for unit in pairs(validUnits) do
if UnitExists(unit) and UnitName(unit) == name then
if UnitIsPlayer(unit) then
local _, class = UnitClass(unit)
DataStore:SetClass(name, class)
return "PLAYER"
end
end
end
-- SuperWoW pet format: "PetName (OwnerName)"
local match, _, owner = string.find(name, "%((.*)%)", 1)
if match and owner then
if Parser:ScanName(owner) == "PLAYER" then
DataStore:SetClass(name, owner)
return "PET"
end
end
for unit in pairs(validPets) do
if UnitExists(unit) and UnitName(unit) == name then
if strsub(unit, 0, 3) == "pet" then
DataStore:SetClass(name, UnitName("player"))
elseif strsub(unit, 0, 8) == "partypet" then
DataStore:SetClass(name, UnitName("party" .. strsub(unit, 9)))
elseif strsub(unit, 0, 7) == "raidpet" then
DataStore:SetClass(name, UnitName("raid" .. strsub(unit, 8)))
end
return "PET"
end
end
if NanamiDPS.config and NanamiDPS.config.trackAllUnits then
DataStore:SetClass(name, DataStore:GetClass(name) or "__other__")
return "OTHER"
end
return nil
end
function Parser:ResolveSource(source)
source = NanamiDPS.trim(source)
local sourceType = self:ScanName(source)
if not sourceType then return nil, nil end
if NanamiDPS.config and NanamiDPS.config.mergePets and sourceType == "PET" then
local ownerClass = DataStore:GetClass(source)
if ownerClass and ownerClass ~= "__other__" and NanamiDPS.validClasses[DataStore:GetClass(ownerClass)] then
return ownerClass, "Pet: " .. source
elseif ownerClass and ownerClass ~= "__other__" then
return ownerClass, "Pet: " .. source
end
end
return source, nil
end
function Parser:ProcessDamage(source, spell, target, amount, school)
if type(source) ~= "string" or not tonumber(amount) then return end
source = NanamiDPS.trim(source)
amount = tonumber(amount)
if source == target then return end
local resolvedSource, petPrefix = self:ResolveSource(source)
if not resolvedSource then return end
local finalSpell = petPrefix and (petPrefix .. ": " .. (spell or "")) or spell
if not DataStore.inCombat then
DataStore:StartCombat()
end
DataStore:AddDamage(resolvedSource, finalSpell, target, amount, school)
local targetType = self:ScanName(target)
if targetType == "PLAYER" then
DataStore:AddDamageTaken(target, finalSpell, resolvedSource, amount, school)
end
DataStore:AddThreat(source, self:CalculateSpellThreat(source, spell, amount))
DataStore:UpdateActivity(resolvedSource, GetTime())
if targetType == "PLAYER" then
self:FeedDeathLog(target, {
time = GetTime(),
source = resolvedSource,
spell = finalSpell,
amount = -amount,
type = "damage",
hp = self:GetUnitHP(target),
})
end
self:ThrottledRefresh()
end
function Parser:ProcessHealing(source, spell, target, amount, school)
if type(source) ~= "string" or not tonumber(amount) then return end
source = NanamiDPS.trim(source)
amount = tonumber(amount)
local resolvedSource, petPrefix = self:ResolveSource(source)
if not resolvedSource then return end
local finalSpell = petPrefix and (petPrefix .. ": " .. (spell or "")) or spell
local effective = amount
local unitstr = self:UnitByName(target)
if unitstr then
effective = math.min(UnitHealthMax(unitstr) - UnitHealth(unitstr), amount)
end
if not DataStore.inCombat then
DataStore:StartCombat()
end
DataStore:AddHealing(resolvedSource, finalSpell, target, amount, effective)
DataStore:AddThreat(source, self:CalculateHealThreat(source, amount))
DataStore:UpdateActivity(resolvedSource, GetTime())
local targetType = self:ScanName(target)
if targetType == "PLAYER" then
self:FeedDeathLog(target, {
time = GetTime(),
source = resolvedSource,
spell = finalSpell,
amount = effective,
type = "heal",
hp = self:GetUnitHP(target),
})
end
self:ThrottledRefresh()
end
function Parser:ProcessDeath(name)
if not name then return end
local nameType = self:ScanName(name)
if nameType ~= "PLAYER" then return end
local log = self:FlushDeathLog(name)
DataStore:AddDeath(name, {
time = GetTime(),
timeStr = date("%H:%M:%S"),
events = log,
})
self:ThrottledRefresh()
end
function Parser:ProcessDispel(source, spell, target, aura)
if type(source) ~= "string" then return end
source = NanamiDPS.trim(source)
local resolvedSource = self:ResolveSource(source)
if not resolvedSource then return end
DataStore:AddDispel(resolvedSource, spell or NanamiDPS.L["Unknown"], target, aura)
DataStore:UpdateActivity(resolvedSource, GetTime())
self:ThrottledRefresh()
end
function Parser:ProcessInterrupt(source, spell, target, interrupted)
if type(source) ~= "string" then return end
source = NanamiDPS.trim(source)
local resolvedSource = self:ResolveSource(source)
if not resolvedSource then return end
DataStore:AddInterrupt(resolvedSource, spell or NanamiDPS.L["Unknown"], target, interrupted)
DataStore:UpdateActivity(resolvedSource, GetTime())
self:ThrottledRefresh()
end
function Parser:GetUnitHP(name)
local unit = self:UnitByName(name)
if unit then
return UnitHealth(unit), UnitHealthMax(unit)
end
return 0, 0
end
-- Throttle refresh callbacks to avoid excessive UI updates
Parser.lastRefreshTime = 0
local REFRESH_INTERVAL = 0.25
function Parser:ThrottledRefresh()
local now = GetTime()
if now - self.lastRefreshTime >= REFRESH_INTERVAL then
self.lastRefreshTime = now
NanamiDPS:FireCallback("refresh")
end
end
-- Death log buffer: last 15 seconds of events per player
Parser.deathLogBuffer = {}
local DEATH_LOG_WINDOW = 15
function Parser:FeedDeathLog(name, entry)
if not self.deathLogBuffer[name] then
self.deathLogBuffer[name] = {}
end
table.insert(self.deathLogBuffer[name], entry)
-- Trim old entries
local now = GetTime()
local buf = self.deathLogBuffer[name]
while table.getn(buf) > 0 and (now - buf[1].time) > DEATH_LOG_WINDOW do
table.remove(buf, 1)
end
end
function Parser:FlushDeathLog(name)
local log = self.deathLogBuffer[name] or {}
self.deathLogBuffer[name] = {}
return log
end
-- Combat state detection
local combatFrame = CreateFrame("Frame", "NanamiDPSCombatState", UIParent)
combatFrame:RegisterEvent("PLAYER_REGEN_DISABLED")
combatFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
local pendingCombatEnd = false
local combatEndTimer = 0
combatFrame:SetScript("OnEvent", function()
if event == "PLAYER_REGEN_DISABLED" then
pendingCombatEnd = false
if not DataStore.inCombat then
DataStore:StartCombat()
end
elseif event == "PLAYER_REGEN_ENABLED" then
pendingCombatEnd = true
combatEndTimer = GetTime() + 2
end
end)
combatFrame:SetScript("OnUpdate", function()
if (this.tick or 1) > GetTime() then return end
this.tick = GetTime() + 1
if pendingCombatEnd and GetTime() >= combatEndTimer then
pendingCombatEnd = false
DataStore:StopCombat()
end
end)

465
ParserVanilla.lua Normal file
View File

@@ -0,0 +1,465 @@
local NanamiDPS = NanamiDPS
local Parser = NanamiDPS.Parser
-- Pattern utility functions (from ShaguDPS)
local sanitize_cache = {}
local function sanitize(pattern)
if not sanitize_cache[pattern] then
local ret = pattern
ret = gsub(ret, "([%+%-%*%(%)%?%[%]%^])", "%%%1")
ret = gsub(ret, "%d%$", "")
ret = gsub(ret, "(%%%a)", "%(%1+%)")
ret = gsub(ret, "%%s%+", ".+")
ret = gsub(ret, "%(.%+%)%(%%d%+%)", "%(.-%)%(%%d%+%)")
sanitize_cache[pattern] = ret
end
return sanitize_cache[pattern]
end
local capture_cache = {}
local function captures(pat)
local r = capture_cache
if not r[pat] then
r[pat] = { nil, nil, nil, nil, nil }
for a, b, c, d, e in string.gfind(gsub(pat, "%((.+)%)", "%1"), gsub(pat, "%d%$", "%%(.-)$")) do
r[pat][1] = tonumber(a)
r[pat][2] = tonumber(b)
r[pat][3] = tonumber(c)
r[pat][4] = tonumber(d)
r[pat][5] = tonumber(e)
end
end
return r[pat][1], r[pat][2], r[pat][3], r[pat][4], r[pat][5]
end
local ra, rb, rc, rd, re, a, b, c, d, e, match, num, va, vb, vc, vd, ve
local function cfind(str, pat)
a, b, c, d, e = captures(pat)
match, num, va, vb, vc, vd, ve = string.find(str, sanitize(pat))
ra = e == 1 and ve or d == 1 and vd or c == 1 and vc or b == 1 and vb or va
rb = e == 2 and ve or d == 2 and vd or c == 2 and vc or a == 2 and va or vb
rc = e == 3 and ve or d == 3 and vd or a == 3 and va or b == 3 and vb or vc
rd = e == 4 and ve or a == 4 and va or c == 4 and vc or b == 4 and vb or vd
re = a == 5 and va or d == 5 and vd or c == 5 and vc or b == 5 and vb or ve
return match, num, ra, rb, rc, rd, re
end
-----------------------------------------------------------------------
-- Combat log string groups
-----------------------------------------------------------------------
local combatlog_strings = {
-- Damage: melee
["Hit Damage (self vs. other)"] = {
COMBATHITSELFOTHER, COMBATHITSCHOOLSELFOTHER,
COMBATHITCRITSELFOTHER, COMBATHITCRITSCHOOLSELFOTHER,
},
["Hit Damage (other vs. self)"] = {
COMBATHITOTHERSELF, COMBATHITCRITOTHERSELF,
COMBATHITSCHOOLOTHERSELF, COMBATHITCRITSCHOOLOTHERSELF,
},
["Hit Damage (other vs. other)"] = {
COMBATHITOTHEROTHER, COMBATHITCRITOTHEROTHER,
COMBATHITSCHOOLOTHEROTHER, COMBATHITCRITSCHOOLOTHEROTHER,
},
-- Damage: spell
["Spell Damage (self vs. self/other)"] = {
SPELLLOGSCHOOLSELFSELF, SPELLLOGCRITSCHOOLSELFSELF,
SPELLLOGSELFSELF, SPELLLOGCRITSELFSELF,
SPELLLOGSCHOOLSELFOTHER, SPELLLOGCRITSCHOOLSELFOTHER,
SPELLLOGSELFOTHER, SPELLLOGCRITSELFOTHER,
},
["Spell Damage (other vs. self)"] = {
SPELLLOGSCHOOLOTHERSELF, SPELLLOGCRITSCHOOLOTHERSELF,
SPELLLOGOTHERSELF, SPELLLOGCRITOTHERSELF,
},
["Spell Damage (other vs. other)"] = {
SPELLLOGSCHOOLOTHEROTHER, SPELLLOGCRITSCHOOLOTHEROTHER,
SPELLLOGOTHEROTHER, SPELLLOGCRITOTHEROTHER,
},
-- Damage: shields
["Shield Damage (self vs. other)"] = {
DAMAGESHIELDSELFOTHER,
},
["Shield Damage (other vs. self/other)"] = {
DAMAGESHIELDOTHERSELF, DAMAGESHIELDOTHEROTHER,
},
-- Damage: periodic
["Periodic Damage (self/other vs. other)"] = {
PERIODICAURADAMAGESELFOTHER, PERIODICAURADAMAGEOTHEROTHER,
},
["Periodic Damage (self/other vs. self)"] = {
PERIODICAURADAMAGESELFSELF, PERIODICAURADAMAGEOTHERSELF,
},
-- Healing
["Heal (self vs. self/other)"] = {
HEALEDCRITSELFSELF, HEALEDSELFSELF,
HEALEDCRITSELFOTHER, HEALEDSELFOTHER,
},
["Heal (other vs. self/other)"] = {
HEALEDCRITOTHERSELF, HEALEDOTHERSELF,
HEALEDCRITOTHEROTHER, HEALEDOTHEROTHER,
},
["Periodic Heal (self/other vs. other)"] = {
PERIODICAURAHEALSELFOTHER, PERIODICAURAHEALOTHEROTHER,
},
["Periodic Heal (other vs. self/other)"] = {
PERIODICAURAHEALSELFSELF, PERIODICAURAHEALOTHERSELF,
},
}
-----------------------------------------------------------------------
-- Event -> pattern mapping
-----------------------------------------------------------------------
local combatlog_events = {
-- Damage: melee
["CHAT_MSG_COMBAT_SELF_HITS"] = combatlog_strings["Hit Damage (self vs. other)"],
["CHAT_MSG_COMBAT_CREATURE_VS_SELF_HITS"] = combatlog_strings["Hit Damage (other vs. self)"],
["CHAT_MSG_COMBAT_PARTY_HITS"] = combatlog_strings["Hit Damage (other vs. other)"],
["CHAT_MSG_COMBAT_FRIENDLYPLAYER_HITS"] = combatlog_strings["Hit Damage (other vs. other)"],
["CHAT_MSG_COMBAT_HOSTILEPLAYER_HITS"] = combatlog_strings["Hit Damage (other vs. other)"],
["CHAT_MSG_COMBAT_CREATURE_VS_CREATURE_HITS"] = combatlog_strings["Hit Damage (other vs. other)"],
["CHAT_MSG_COMBAT_CREATURE_VS_PARTY_HITS"] = combatlog_strings["Hit Damage (other vs. other)"],
["CHAT_MSG_COMBAT_PET_HITS"] = combatlog_strings["Hit Damage (other vs. other)"],
-- Damage: spell
["CHAT_MSG_SPELL_SELF_DAMAGE"] = combatlog_strings["Spell Damage (self vs. self/other)"],
["CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE"] = combatlog_strings["Spell Damage (other vs. self)"],
["CHAT_MSG_SPELL_PARTY_DAMAGE"] = combatlog_strings["Spell Damage (other vs. other)"],
["CHAT_MSG_SPELL_FRIENDLYPLAYER_DAMAGE"] = combatlog_strings["Spell Damage (other vs. other)"],
["CHAT_MSG_SPELL_HOSTILEPLAYER_DAMAGE"] = combatlog_strings["Spell Damage (other vs. other)"],
["CHAT_MSG_SPELL_CREATURE_VS_CREATURE_DAMAGE"] = combatlog_strings["Spell Damage (other vs. other)"],
["CHAT_MSG_SPELL_CREATURE_VS_PARTY_DAMAGE"] = combatlog_strings["Spell Damage (other vs. other)"],
["CHAT_MSG_SPELL_PET_DAMAGE"] = combatlog_strings["Spell Damage (other vs. other)"],
-- Damage: shields
["CHAT_MSG_SPELL_DAMAGESHIELDS_ON_SELF"] = combatlog_strings["Shield Damage (self vs. other)"],
["CHAT_MSG_SPELL_DAMAGESHIELDS_ON_OTHERS"] = combatlog_strings["Shield Damage (other vs. self/other)"],
-- Damage: periodic
["CHAT_MSG_SPELL_PERIODIC_PARTY_DAMAGE"] = combatlog_strings["Periodic Damage (self/other vs. other)"],
["CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE"] = combatlog_strings["Periodic Damage (self/other vs. other)"],
["CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_DAMAGE"] = combatlog_strings["Periodic Damage (self/other vs. other)"],
["CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE"] = combatlog_strings["Periodic Damage (self/other vs. other)"],
["CHAT_MSG_SPELL_PERIODIC_SELF_DAMAGE"] = combatlog_strings["Periodic Damage (self/other vs. self)"],
-- Healing
["CHAT_MSG_SPELL_SELF_BUFF"] = combatlog_strings["Heal (self vs. self/other)"],
["CHAT_MSG_SPELL_FRIENDLYPLAYER_BUFF"] = combatlog_strings["Heal (other vs. self/other)"],
["CHAT_MSG_SPELL_HOSTILEPLAYER_BUFF"] = combatlog_strings["Heal (other vs. self/other)"],
["CHAT_MSG_SPELL_PARTY_BUFF"] = combatlog_strings["Heal (other vs. self/other)"],
["CHAT_MSG_SPELL_PERIODIC_PARTY_BUFFS"] = combatlog_strings["Periodic Heal (self/other vs. other)"],
["CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_BUFFS"] = combatlog_strings["Periodic Heal (self/other vs. other)"],
["CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_BUFFS"] = combatlog_strings["Periodic Heal (self/other vs. other)"],
["CHAT_MSG_SPELL_PERIODIC_SELF_BUFFS"] = combatlog_strings["Periodic Heal (other vs. self/other)"],
}
-----------------------------------------------------------------------
-- Pattern -> parser function mapping
-----------------------------------------------------------------------
local combatlog_parser = {
-- Spell damage: self vs self
[SPELLLOGSCHOOLSELFSELF] = function(d, attack, value, school)
return d.source, attack, d.target, value, school, "damage"
end,
[SPELLLOGCRITSCHOOLSELFSELF] = function(d, attack, value, school)
return d.source, attack, d.target, value, school, "damage"
end,
[SPELLLOGSELFSELF] = function(d, attack, value)
return d.source, attack, d.target, value, d.school, "damage"
end,
[SPELLLOGCRITSELFSELF] = function(d, attack, value)
return d.source, attack, d.target, value, d.school, "damage"
end,
[PERIODICAURADAMAGESELFSELF] = function(d, value, school, attack)
return d.source, attack, d.target, value, school, "damage"
end,
-- Spell damage: self vs other
[SPELLLOGSCHOOLSELFOTHER] = function(d, attack, target, value, school)
return d.source, attack, target, value, school, "damage"
end,
[SPELLLOGCRITSCHOOLSELFOTHER] = function(d, attack, target, value, school)
return d.source, attack, target, value, school, "damage"
end,
[SPELLLOGSELFOTHER] = function(d, attack, target, value)
return d.source, attack, target, value, d.school, "damage"
end,
[SPELLLOGCRITSELFOTHER] = function(d, attack, target, value)
return d.source, attack, target, value, d.school, "damage"
end,
[PERIODICAURADAMAGESELFOTHER] = function(d, target, value, school, attack)
return d.source, attack, target, value, school, "damage"
end,
-- Melee damage: self vs other
[COMBATHITSELFOTHER] = function(d, target, value)
return d.source, d.attack, target, value, d.school, "damage"
end,
[COMBATHITCRITSELFOTHER] = function(d, target, value)
return d.source, d.attack, target, value, d.school, "damage"
end,
[COMBATHITSCHOOLSELFOTHER] = function(d, target, value, school)
return d.source, d.attack, target, value, school, "damage"
end,
[COMBATHITCRITSCHOOLSELFOTHER] = function(d, target, value, school)
return d.source, d.attack, target, value, school, "damage"
end,
-- Shield damage: self vs other
[DAMAGESHIELDSELFOTHER] = function(d, value, school, target)
return d.source, "Reflect (" .. school .. ")", target, value, school, "damage"
end,
-- Spell damage: other vs self
[SPELLLOGSCHOOLOTHERSELF] = function(d, source, attack, value, school)
return source, attack, d.target, value, school, "damage"
end,
[SPELLLOGCRITSCHOOLOTHERSELF] = function(d, source, attack, value, school)
return source, attack, d.target, value, school, "damage"
end,
[SPELLLOGOTHERSELF] = function(d, source, attack, value)
return source, attack, d.target, value, d.school, "damage"
end,
[SPELLLOGCRITOTHERSELF] = function(d, source, attack, value)
return source, attack, d.target, value, d.school, "damage"
end,
[PERIODICAURADAMAGEOTHERSELF] = function(d, value, school, source, attack)
return source, attack, d.target, value, school, "damage"
end,
-- Melee damage: other vs self
[COMBATHITOTHERSELF] = function(d, source, value)
return source, d.attack, d.target, value, d.school, "damage"
end,
[COMBATHITCRITOTHERSELF] = function(d, source, value)
return source, d.attack, d.target, value, d.school, "damage"
end,
[COMBATHITSCHOOLOTHERSELF] = function(d, source, value, school)
return source, d.attack, d.target, value, school, "damage"
end,
[COMBATHITCRITSCHOOLOTHERSELF] = function(d, source, value, school)
return source, d.attack, d.target, value, school, "damage"
end,
-- Spell damage: other vs other
[SPELLLOGSCHOOLOTHEROTHER] = function(d, source, attack, target, value, school)
return source, attack, target, value, school, "damage"
end,
[SPELLLOGCRITSCHOOLOTHEROTHER] = function(d, source, attack, target, value, school)
return source, attack, target, value, school, "damage"
end,
[SPELLLOGOTHEROTHER] = function(d, source, attack, target, value)
return source, attack, target, value, d.school, "damage"
end,
[SPELLLOGCRITOTHEROTHER] = function(d, source, attack, target, value, school)
return source, attack, target, value, school, "damage"
end,
[PERIODICAURADAMAGEOTHEROTHER] = function(d, target, value, school, source, attack)
return source, attack, target, value, school, "damage"
end,
-- Melee damage: other vs other
[COMBATHITOTHEROTHER] = function(d, source, target, value)
return source, d.attack, target, value, d.school, "damage"
end,
[COMBATHITCRITOTHEROTHER] = function(d, source, target, value)
return source, d.attack, target, value, d.school, "damage"
end,
[COMBATHITSCHOOLOTHEROTHER] = function(d, source, target, value, school)
return source, d.attack, target, value, school, "damage"
end,
[COMBATHITCRITSCHOOLOTHEROTHER] = function(d, source, target, value, school)
return source, d.attack, target, value, school, "damage"
end,
-- Shield damage: other
[DAMAGESHIELDOTHERSELF] = function(d, source, value, school)
return source, "Reflect (" .. school .. ")", d.target, value, school, "damage"
end,
[DAMAGESHIELDOTHEROTHER] = function(d, source, value, school, target)
return source, "Reflect (" .. school .. ")", target, value, school, "damage"
end,
-- Healing: other vs self
[HEALEDCRITOTHERSELF] = function(d, source, spell, value)
return source, spell, d.target, value, d.school, "heal"
end,
[HEALEDOTHERSELF] = function(d, source, spell, value)
return source, spell, d.target, value, d.school, "heal"
end,
[PERIODICAURAHEALOTHERSELF] = function(d, value, source, spell)
return source, spell, d.target, value, d.school, "heal"
end,
-- Healing: self vs self
[HEALEDCRITSELFSELF] = function(d, spell, value)
return d.source, spell, d.target, value, d.school, "heal"
end,
[HEALEDSELFSELF] = function(d, spell, value)
return d.source, spell, d.target, value, d.school, "heal"
end,
[PERIODICAURAHEALSELFSELF] = function(d, value, spell)
return d.source, spell, d.target, value, d.school, "heal"
end,
-- Healing: self vs other
[HEALEDCRITSELFOTHER] = function(d, spell, target, value)
return d.source, spell, target, value, d.school, "heal"
end,
[HEALEDSELFOTHER] = function(d, spell, target, value)
return d.source, spell, target, value, d.school, "heal"
end,
[PERIODICAURAHEALSELFOTHER] = function(d, target, value, spell)
return d.source, spell, target, value, d.school, "heal"
end,
-- Healing: other vs other
[HEALEDCRITOTHEROTHER] = function(d, source, spell, target, value)
return source, spell, target, value, d.school, "heal"
end,
[HEALEDOTHEROTHER] = function(d, source, spell, target, value)
return source, spell, target, value, d.school, "heal"
end,
[PERIODICAURAHEALOTHEROTHER] = function(d, target, value, source, spell)
return source, spell, target, value, d.school, "heal"
end,
}
-----------------------------------------------------------------------
-- Register combat log events
-----------------------------------------------------------------------
for evt in pairs(combatlog_events) do
Parser:RegisterEvent(evt)
end
-- Preload patterns
for pattern in pairs(combatlog_parser) do
sanitize(pattern)
end
-----------------------------------------------------------------------
-- Death events
-----------------------------------------------------------------------
Parser:RegisterEvent("CHAT_MSG_COMBAT_FRIENDLY_DEATH")
Parser:RegisterEvent("CHAT_MSG_COMBAT_HOSTILE_DEATH")
-----------------------------------------------------------------------
-- Interrupt patterns
-----------------------------------------------------------------------
local interrupt_strings = {}
if SPELLINTERRUPTSELFOTHER then
table.insert(interrupt_strings, SPELLINTERRUPTSELFOTHER)
end
if SPELLINTERRUPTOTHERSELF then
table.insert(interrupt_strings, SPELLINTERRUPTOTHERSELF)
end
if SPELLINTERRUPTOTHEROTHER then
table.insert(interrupt_strings, SPELLINTERRUPTOTHEROTHER)
end
local interrupt_parser = {}
if SPELLINTERRUPTSELFOTHER then
interrupt_parser[SPELLINTERRUPTSELFOTHER] = function(d, spell, target, interrupted)
return d.source, spell, target, interrupted
end
end
if SPELLINTERRUPTOTHERSELF then
interrupt_parser[SPELLINTERRUPTOTHERSELF] = function(d, source, spell, interrupted)
return source, spell, d.target, interrupted
end
end
if SPELLINTERRUPTOTHEROTHER then
interrupt_parser[SPELLINTERRUPTOTHEROTHER] = function(d, source, spell, target, interrupted)
return source, spell, target, interrupted
end
end
for pat in pairs(interrupt_parser) do
sanitize(pat)
end
-----------------------------------------------------------------------
-- Dispel patterns
-----------------------------------------------------------------------
local dispel_strings = {}
local dispel_parser = {}
if AURADISPELSELF2 then
table.insert(dispel_strings, AURADISPELSELF2)
dispel_parser[AURADISPELSELF2] = function(d, spell, aura)
return d.source, spell, d.target, aura
end
end
if AURADISPELOTHER2 then
table.insert(dispel_strings, AURADISPELOTHER2)
dispel_parser[AURADISPELOTHER2] = function(d, source, spell, target, aura)
return source, spell, target, aura
end
end
for pat in pairs(dispel_parser) do
sanitize(pat)
end
-----------------------------------------------------------------------
-- Main event handler
-----------------------------------------------------------------------
local defaults = {}
local absorb = ABSORB_TRAILER and sanitize(ABSORB_TRAILER) or nil
local resist = RESIST_TRAILER and sanitize(RESIST_TRAILER) or nil
local empty, physical, autohit = "", "physical", "Auto Hit"
local player = UnitName("player")
Parser:SetScript("OnEvent", function()
if not arg1 then return end
-- Death events
if event == "CHAT_MSG_COMBAT_FRIENDLY_DEATH" or event == "CHAT_MSG_COMBAT_HOSTILE_DEATH" then
if UNITDIESOTHER then
local deathPat = sanitize(UNITDIESOTHER)
local _, _, deadName = string.find(arg1, deathPat)
if deadName then
Parser:ProcessDeath(deadName)
end
end
if UNITDIESSELF then
local selfPat = sanitize(UNITDIESSELF)
if string.find(arg1, selfPat) then
Parser:ProcessDeath(player)
end
end
return
end
-- Strip absorb/resist suffixes
if absorb then arg1 = string.gsub(arg1, absorb, empty) end
if resist then arg1 = string.gsub(arg1, resist, empty) end
defaults.source = player
defaults.target = player
defaults.school = physical
defaults.attack = autohit
defaults.spell = UNKNOWN or "Unknown"
defaults.value = 0
-- Check interrupt patterns first (they share spell damage events)
for _, pat in ipairs(interrupt_strings) do
local result, num, a1, a2, a3, a4, a5 = cfind(arg1, pat)
if result then
local src, spell, tgt, interrupted = interrupt_parser[pat](defaults, a1, a2, a3, a4, a5)
Parser:ProcessInterrupt(src, spell, tgt, interrupted)
return
end
end
-- Check dispel patterns
for _, pat in ipairs(dispel_strings) do
local result, num, a1, a2, a3, a4, a5 = cfind(arg1, pat)
if result then
local src, spell, tgt, aura = dispel_parser[pat](defaults, a1, a2, a3, a4, a5)
Parser:ProcessDispel(src, spell, tgt, aura)
return
end
end
-- Standard combat log patterns
local patterns = combatlog_events[event]
if not patterns then return end
for _, pattern in pairs(patterns) do
local result, num, a1, a2, a3, a4, a5 = cfind(arg1, pattern)
if result then
local source, spell, target, value, school, dtype = combatlog_parser[pattern](defaults, a1, a2, a3, a4, a5)
if dtype == "damage" then
Parser:ProcessDamage(source, spell, target, value, school)
elseif dtype == "heal" then
Parser:ProcessHealing(source, spell, target, value, school)
end
return
end
end
end)

69
Tooltip.lua Normal file
View File

@@ -0,0 +1,69 @@
local NanamiDPS = NanamiDPS
local DataStore = NanamiDPS.DataStore
local L = NanamiDPS.L
local Tooltip = {}
NanamiDPS.Tooltip = Tooltip
function Tooltip:ShowBar(bar, window)
if not bar or not bar.barData then return end
local data = bar.barData
local moduleName = window.activeModuleName
local mod = NanamiDPS.modules[moduleName]
GameTooltip:SetOwner(bar, "ANCHOR_RIGHT")
if mod and mod.GetTooltip then
local seg = DataStore:GetSegment(window.segmentIndex or 1)
mod:GetTooltip(data.id or data.name, seg, GameTooltip)
else
GameTooltip:AddLine(data.name or L["Unknown"])
if data.value then
GameTooltip:AddDoubleLine("|cffffffff" .. L["Total Amount"], "|cffffffff" .. NanamiDPS.formatNumber(data.value))
end
if data.perSecond then
GameTooltip:AddDoubleLine("|cffffffff" .. L["Per Second"], "|cffffffff" .. NanamiDPS.round(data.perSecond, 1))
end
end
GameTooltip:AddLine(" ")
GameTooltip:AddLine("|cff888888" .. L["Click to view details"])
GameTooltip:Show()
end
function Tooltip:Hide()
GameTooltip:Hide()
end
function Tooltip:ShowSpellDetail(playerName, spells, effectiveSpells, totalSum, effectiveSum, tooltip)
tooltip = tooltip or GameTooltip
if not spells then return end
tooltip:AddLine(" ")
tooltip:AddLine("|cffffd100" .. L["Details"] .. ":")
local sorted = {}
for spell, amount in pairs(spells) do
table.insert(sorted, { spell = spell, amount = amount })
end
table.sort(sorted, function(a, b) return a.amount > b.amount end)
for _, entry in ipairs(sorted) do
local pct = totalSum > 0 and NanamiDPS.round(entry.amount / totalSum * 100, 1) or 0
local rightStr
if effectiveSpells and effectiveSpells[entry.spell] then
local eff = effectiveSpells[entry.spell]
local overheal = entry.amount - eff
rightStr = string.format("|cffcc8888+%s |cffffffff%s (%.1f%%)",
NanamiDPS.formatNumber(overheal),
NanamiDPS.formatNumber(eff), pct)
else
rightStr = string.format("|cffffffff%s (%.1f%%)",
NanamiDPS.formatNumber(entry.amount), pct)
end
tooltip:AddDoubleLine("|cffffffff" .. entry.spell, rightStr)
end
end

87
Utils.lua Normal file
View File

@@ -0,0 +1,87 @@
local NanamiDPS = NanamiDPS
function NanamiDPS.round(input, places)
if not places then places = 0 end
if type(input) == "number" and type(places) == "number" then
local pow = 1
for i = 1, places do pow = pow * 10 end
return floor(input * pow + 0.5) / pow
end
return 0
end
function NanamiDPS.trim(str)
if not str then return "" end
return gsub(str, "^%s*(.-)%s*$", "%1")
end
function NanamiDPS.spairs(t, order)
local keys = {}
for k in pairs(t) do
table.insert(keys, k)
end
if order then
table.sort(keys, function(a, b) return order(t, a, b) end)
else
table.sort(keys)
end
local i = 0
return function()
i = i + 1
if keys[i] then
return keys[i], t[keys[i]]
end
end
end
local rgbcache = {}
function NanamiDPS.str2rgb(text)
if not text then return 1, 1, 1 end
if rgbcache[text] then return unpack(rgbcache[text]) end
local counter = 1
local l = string.len(text)
for i = 1, l, 3 do
counter = mod(counter * 8161, 4294967279) +
(string.byte(text, i) * 16776193) +
((string.byte(text, i + 1) or (l - i + 256)) * 8372226) +
((string.byte(text, i + 2) or (l - i + 256)) * 3932164)
end
local hash = mod(mod(counter, 4294967291), 16777216)
local r = (hash - (mod(hash, 65536))) / 65536
local g = ((hash - r * 65536) - (mod((hash - r * 65536), 256))) / 256
local b = hash - r * 65536 - g * 256
rgbcache[text] = { r / 255, g / 255, b / 255 }
return unpack(rgbcache[text])
end
function NanamiDPS.formatNumber(num)
if not num then return "0" end
if num >= 1000000 then
return NanamiDPS.round(num / 1000000, 1) .. "M"
elseif num >= 1000 then
return NanamiDPS.round(num / 1000, 1) .. "K"
end
return tostring(math.floor(num))
end
function NanamiDPS.formatTime(seconds)
if not seconds or seconds <= 0 then return "0:00" end
local m = math.floor(seconds / 60)
local s = math.floor(seconds - m * 60)
if s < 10 then
return m .. ":0" .. s
end
return m .. ":" .. s
end
function NanamiDPS.GetClassColor(class)
if class and RAID_CLASS_COLORS and RAID_CLASS_COLORS[class] then
return RAID_CLASS_COLORS[class].r, RAID_CLASS_COLORS[class].g, RAID_CLASS_COLORS[class].b
end
return 0.6, 0.6, 0.6
end
NanamiDPS.validClasses = {
WARRIOR = true, MAGE = true, ROGUE = true, DRUID = true, HUNTER = true,
SHAMAN = true, PRIEST = true, WARLOCK = true, PALADIN = true,
}

1349
Window.lua Normal file

File diff suppressed because it is too large Load Diff