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)