385 lines
11 KiB
Lua
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)
|