Files
Nanami-DPS/ThreatDisplay.lua
2026-03-25 00:57:35 +08:00

385 lines
11 KiB
Lua

local NanamiDPS = NanamiDPS
local TE = NanamiDPS.ThreatEngine
local TC = NanamiDPS.ThreatCoefficients
local L = NanamiDPS.L
-------------------------------------------------------------------------------
-- ThreatDisplay: Visual warning system, nameplate anchoring, role views
--
-- 1. Full-screen edge glow when approaching OT threshold
-- 2. Sound warning at critical threat levels
-- 3. Nameplate threat indicators (SuperAPI dependent)
-- 4. Tank Mode / Healer Mode smart views
-------------------------------------------------------------------------------
local TD = CreateFrame("Frame", "NanamiDPSThreatDisplay", UIParent)
NanamiDPS.ThreatDisplay = TD
local _floor = math.floor
local _min = math.min
local _max = math.max
TD.warningState = "SAFE"
TD.lastWarnSound = 0
TD.lastGlowUpdate = 0
TD.nameplateFrames = {}
-------------------------------------------------------------------------------
-- Full-screen glow overlay
-------------------------------------------------------------------------------
local glowFrame = CreateFrame("Frame", "NanamiDPS_OTGlow", UIParent)
glowFrame:SetFrameStrata("FULLSCREEN_DIALOG")
glowFrame:SetAllPoints(UIParent)
glowFrame:EnableMouse(false)
glowFrame:Hide()
local glowTop = glowFrame:CreateTexture(nil, "OVERLAY")
glowTop:SetTexture(1, 0, 0)
glowTop:SetPoint("TOPLEFT", glowFrame, "TOPLEFT")
glowTop:SetPoint("TOPRIGHT", glowFrame, "TOPRIGHT")
glowTop:SetHeight(40)
local glowBottom = glowFrame:CreateTexture(nil, "OVERLAY")
glowBottom:SetTexture(1, 0, 0)
glowBottom:SetPoint("BOTTOMLEFT", glowFrame, "BOTTOMLEFT")
glowBottom:SetPoint("BOTTOMRIGHT", glowFrame, "BOTTOMRIGHT")
glowBottom:SetHeight(40)
local glowLeft = glowFrame:CreateTexture(nil, "OVERLAY")
glowLeft:SetTexture(1, 0, 0)
glowLeft:SetPoint("TOPLEFT", glowFrame, "TOPLEFT")
glowLeft:SetPoint("BOTTOMLEFT", glowFrame, "BOTTOMLEFT")
glowLeft:SetWidth(30)
local glowRight = glowFrame:CreateTexture(nil, "OVERLAY")
glowRight:SetTexture(1, 0, 0)
glowRight:SetPoint("TOPRIGHT", glowFrame, "TOPRIGHT")
glowRight:SetPoint("BOTTOMRIGHT", glowFrame, "BOTTOMRIGHT")
glowRight:SetWidth(30)
local glowAlpha = 0
local glowDir = 1
local GLOW_SPEED = 2.5
local GLOW_MAX = 0.55
local GLOW_MIN = 0.05
glowFrame:SetScript("OnUpdate", function()
local dt = arg1 or 0.016
glowAlpha = glowAlpha + glowDir * GLOW_SPEED * dt
if glowAlpha >= GLOW_MAX then
glowAlpha = GLOW_MAX
glowDir = -1
elseif glowAlpha <= GLOW_MIN then
glowAlpha = GLOW_MIN
glowDir = 1
end
glowTop:SetAlpha(glowAlpha)
glowBottom:SetAlpha(glowAlpha)
glowLeft:SetAlpha(glowAlpha * 0.7)
glowRight:SetAlpha(glowAlpha * 0.7)
end)
function TD:ShowGlow()
glowAlpha = GLOW_MIN
glowDir = 1
glowFrame:Show()
end
function TD:HideGlow()
glowFrame:Hide()
end
-------------------------------------------------------------------------------
-- Warning text overlay (center screen)
-------------------------------------------------------------------------------
local warnFrame = CreateFrame("Frame", "NanamiDPS_OTWarn", UIParent)
warnFrame:SetFrameStrata("HIGH")
warnFrame:SetWidth(300)
warnFrame:SetHeight(50)
warnFrame:SetPoint("TOP", UIParent, "TOP", 0, -180)
warnFrame:EnableMouse(false)
warnFrame:Hide()
local warnText = warnFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
warnText:SetAllPoints()
warnText:SetFont(STANDARD_TEXT_FONT, 22, "OUTLINE")
warnText:SetTextColor(1, 0.2, 0)
local warnFadeTimer = 0
warnFrame:SetScript("OnUpdate", function()
local dt = arg1 or 0.016
warnFadeTimer = warnFadeTimer - dt
if warnFadeTimer <= 0 then
warnFrame:Hide()
elseif warnFadeTimer < 1 then
warnFrame:SetAlpha(warnFadeTimer)
end
end)
function TD:FlashWarning(text)
warnText:SetText(text)
warnFrame:SetAlpha(1)
warnFadeTimer = 3.0
warnFrame:Show()
end
-------------------------------------------------------------------------------
-- Main update loop: assess OT danger and trigger warnings
-------------------------------------------------------------------------------
local WARNING_SOUND = "Sound\\Doodad\\BellTollAlliance.wav"
local WARN_COOLDOWN = 3.0
function TD:Update()
if not TE or not TE.inCombat then
self:HideGlow()
self.warningState = "SAFE"
return
end
local config = NanamiDPS.config
if not config then return end
if not config.otWarning then
self:HideGlow()
self.warningState = "SAFE"
return
end
local targetKey = TE:GetActiveTargetKey()
if not targetKey then
self:HideGlow()
self.warningState = "SAFE"
return
end
local otStatus = TE:GetOTStatus(targetKey)
if not otStatus or not otStatus.tankName then
self:HideGlow()
self.warningState = "SAFE"
return
end
local pct = otStatus.pct or 0
local now = GetTime()
if pct >= 90 then
if self.warningState ~= "CRITICAL" then
self.warningState = "CRITICAL"
if (now - self.lastWarnSound) >= WARN_COOLDOWN then
PlaySoundFile(WARNING_SOUND)
self.lastWarnSound = now
end
self:FlashWarning(L["OT Warning Critical"])
end
self:ShowGlow()
elseif pct >= 70 then
if self.warningState ~= "DANGER" then
self.warningState = "DANGER"
self:FlashWarning(L["OT Warning Danger"])
end
self:HideGlow()
elseif pct >= 50 then
self.warningState = "CAUTION"
self:HideGlow()
else
self.warningState = "SAFE"
self:HideGlow()
end
end
-------------------------------------------------------------------------------
-- Nameplate threat indicators (SuperAPI / pfUI integration)
-- Attaches small threat % text to enemy nameplates when available.
-------------------------------------------------------------------------------
local NP_UPDATE_INTERVAL = 0.5
TD.lastNPUpdate = 0
function TD:UpdateNameplates()
if not TE or not TE.inCombat then
self:HideAllNameplateIndicators()
return
end
local config = NanamiDPS.config
if not config or not config.nameplateThreat then return end
local now = GetTime()
if (now - self.lastNPUpdate) < NP_UPDATE_INTERVAL then return end
self.lastNPUpdate = now
local worldFrame = WorldFrame
if not worldFrame then return end
if not worldFrame.GetChildren then return end
local children = { worldFrame:GetChildren() }
for i = 1, table.getn(children) do
local child = children[i]
if child and child:IsVisible() and child.GetName and child:GetName() == nil then
self:ProcessNameplate(child)
end
end
end
function TD:HideAllNameplateIndicators()
for name, indicator in pairs(self.nameplateFrames) do
if indicator and indicator.SetText then
indicator:SetText("")
end
end
end
function TD:ProcessNameplate(frame)
if not frame or not frame:IsVisible() then return end
local regions = { frame:GetRegions() }
local nameRegion = nil
for _, region in pairs(regions) do
if region:GetObjectType() == "FontString" then
local text = region:GetText()
if text and text ~= "" then
nameRegion = region
break
end
end
end
if not nameRegion then return end
local npName = nameRegion:GetText()
if not npName then return end
local indicator = self.nameplateFrames[npName]
if not indicator then
indicator = frame:CreateFontString(nil, "OVERLAY")
indicator:SetFont(STANDARD_TEXT_FONT, 10, "OUTLINE")
indicator:SetPoint("TOP", nameRegion, "BOTTOM", 0, -2)
self.nameplateFrames[npName] = indicator
end
local playerName = TE.playerName
local targetKey = nil
for key, td in pairs(TE.targets) do
if td.players[playerName] then
targetKey = key
break
end
end
if not targetKey then
indicator:SetText("")
return
end
local td = TE.targets[targetKey]
if not td then
indicator:SetText("")
return
end
local pd = td.players[playerName]
if not pd then
indicator:SetText("")
return
end
local pct = pd.perc or 0
local r, g, b = 0.2, 1, 0.2
if pct >= 80 then
r, g, b = 1, 0.2, 0
elseif pct >= 50 then
r, g, b = 1, 1, 0
end
indicator:SetTextColor(r, g, b)
indicator:SetText(string.format("%.0f%%", pct))
end
-------------------------------------------------------------------------------
-- Tank Mode: Show which mobs have loose threat
-- Returns a summary table for UI consumption
-------------------------------------------------------------------------------
function TD:GetTankModeSummary()
if not TE then return {} end
local summary = {}
local myName = TE.playerName
for targetKey, td in pairs(TE.targets) do
if td.tankName and td.tankName ~= myName then
local myData = td.players[myName]
local tankData = td.players[td.tankName]
if myData and tankData then
table.insert(summary, {
targetKey = targetKey,
tankName = td.tankName,
myThreat = myData.threat,
tankThreat = tankData.threat,
gap = tankData.threat - myData.threat,
pct = tankData.threat > 0 and (myData.threat / tankData.threat * 100) or 0,
})
end
end
end
table.sort(summary, function(a, b) return a.pct > b.pct end)
return summary
end
-------------------------------------------------------------------------------
-- Healer Mode: Show which mobs don't have solid tank threat
-------------------------------------------------------------------------------
function TD:GetHealerModeSummary(masterTankName)
if not TE or not masterTankName then return {} end
local summary = {}
for targetKey, td in pairs(TE.targets) do
local tankData = td.players[masterTankName]
if not tankData or not td.tankName or td.tankName ~= masterTankName then
local topThreat = 0
local topName = ""
for name, pd in pairs(td.players) do
if pd.threat > topThreat then
topThreat = pd.threat
topName = name
end
end
table.insert(summary, {
targetKey = targetKey,
topThreatName = topName,
topThreat = topThreat,
tankThreat = tankData and tankData.threat or 0,
isLoose = td.tankName ~= masterTankName,
})
end
end
table.sort(summary, function(a, b) return a.tankThreat < b.tankThreat end)
return summary
end
-------------------------------------------------------------------------------
-- Hook into ThreatEngine update cycle
-------------------------------------------------------------------------------
NanamiDPS:RegisterCallback("threat_update", "ThreatDisplay", function()
TD:Update()
end)
TD:SetScript("OnUpdate", function()
if not TE or not TE.inCombat then
if TD.nameplateFrames then
TD:HideAllNameplateIndicators()
end
return
end
local config = NanamiDPS.config
if not config or not config.nameplateThreat then return end
local now = GetTime()
if (now - (TD.lastNPUpdate or 0)) >= NP_UPDATE_INTERVAL then
TD:UpdateNameplates()
end
end)