1607 lines
59 KiB
Lua
1607 lines
59 KiB
Lua
local NP = NanamiPlates
|
|
local Settings = NP.Settings
|
|
local Colors = NP.Colors
|
|
local THREAT_COLORS = NP.THREAT_COLORS
|
|
local SpellDB = NanamiPlates_SpellDB
|
|
local Scanner = NanamiPlates_Scanner
|
|
local Healthbar = NanamiPlates_Healthbar
|
|
|
|
local pairs = pairs
|
|
local ipairs = ipairs
|
|
local type = type
|
|
local tostring = tostring
|
|
local tonumber = tonumber
|
|
local string_find = string.find
|
|
local string_lower = string.lower
|
|
local string_format = string.format
|
|
local string_gfind = string.gfind
|
|
local string_gsub = string.gsub
|
|
local string_sub = string.sub
|
|
local math_floor = math.floor
|
|
local GetTime = GetTime
|
|
local UnitExists = UnitExists
|
|
local UnitName = UnitName
|
|
local UnitLevel = UnitLevel
|
|
local UnitClass = UnitClass
|
|
local UnitIsUnit = UnitIsUnit
|
|
local UnitCanAttack = UnitCanAttack
|
|
local UnitIsFriend = UnitIsFriend
|
|
local UnitIsEnemy = UnitIsEnemy
|
|
local UnitDebuff = UnitDebuff
|
|
local UnitGUID = UnitGUID
|
|
local CreateFrame = CreateFrame
|
|
|
|
local registry = NP.registry
|
|
local superwow_active = NP.superwow_active
|
|
local playerClass = NP.playerClass
|
|
local platecount = 0
|
|
local playerInCombat = false
|
|
|
|
local REGION_ORDER = { "border", "glow", "name", "level", "levelicon", "raidicon" }
|
|
local DEBUFF_UPDATE_INTERVAL = 0.1
|
|
|
|
-- Quest mob detection: scans the quest log for kill objectives
|
|
-- and checks pfQuest tooltip data for item drop mobs
|
|
local questMobCache = {}
|
|
local questCacheTimer = 0
|
|
local QUEST_CACHE_INTERVAL = 2
|
|
|
|
local questMonsterPattern
|
|
do
|
|
local raw = QUEST_MONSTERS_KILLED or "%s slain: %d/%d"
|
|
raw = string.gsub(raw, "([%(%)%.%+%-%?%[%]%^%$%%])", "%%%1")
|
|
raw = string.gsub(raw, "%%%%s", "(.+)")
|
|
raw = string.gsub(raw, "%%%%d", "(%%d+)")
|
|
questMonsterPattern = raw
|
|
end
|
|
|
|
local function RebuildQuestMobCache()
|
|
for k in pairs(questMobCache) do questMobCache[k] = nil end
|
|
|
|
local numEntries = GetNumQuestLogEntries()
|
|
if not numEntries or numEntries == 0 then return end
|
|
|
|
for qid = 1, numEntries do
|
|
local title, level, _, header = GetQuestLogTitle(qid)
|
|
if title and not header then
|
|
local objectives = GetNumQuestLeaderBoards(qid)
|
|
if objectives then
|
|
for i = 1, objectives do
|
|
local text, objType, done = GetQuestLogLeaderBoard(i, qid)
|
|
if text and objType == "monster" and not done then
|
|
local _, _, mobName, current, needed = string_find(text, questMonsterPattern)
|
|
if not mobName then
|
|
_, _, mobName, current, needed = string_find(text, "^(.+):%s*(%d+)/(%d+)")
|
|
end
|
|
if mobName and current and needed then
|
|
if not questMobCache[mobName] or not questMobCache[mobName].done then
|
|
questMobCache[mobName] = {
|
|
current = tonumber(current),
|
|
needed = tonumber(needed),
|
|
quest = title,
|
|
}
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function GetQuestMobInfo(plateName)
|
|
if questMobCache[plateName] then
|
|
return questMobCache[plateName]
|
|
end
|
|
if pfMap and pfMap.tooltips and pfMap.tooltips[plateName] then
|
|
if next(pfMap.tooltips[plateName]) then
|
|
return { tooltip = true }
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function GetRankNumber(rankStr)
|
|
if not rankStr then return 0 end
|
|
for num in string_gfind(rankStr, "(%d+)") do
|
|
return tonumber(num) or 0
|
|
end
|
|
return 0
|
|
end
|
|
|
|
local function ParseSpellName(spellString)
|
|
if not spellString then return nil, 0 end
|
|
for name, rank in string_gfind(spellString, "^(.+)%(Rank (%d+)%)$") do
|
|
return name, tonumber(rank) or 0
|
|
end
|
|
return spellString, 0
|
|
end
|
|
|
|
-- Direct spell tracking: immediately records effect on target at cast time.
|
|
-- Even if the spell is resisted, the debuff icon won't appear on the nameplate,
|
|
-- so no incorrect timer will be displayed.
|
|
local function ClearSingleCurseTracking(targetName, guid, curseName)
|
|
if not SpellDB or not curseName then return end
|
|
if SpellDB.objects and SpellDB.objects[targetName] then
|
|
for lvl, effects in pairs(SpellDB.objects[targetName]) do
|
|
if effects[curseName] then effects[curseName] = nil end
|
|
end
|
|
end
|
|
if guid and SpellDB.objects and SpellDB.objects[guid] then
|
|
for lvl, effects in pairs(SpellDB.objects[guid]) do
|
|
if effects[curseName] then effects[curseName] = nil end
|
|
end
|
|
end
|
|
if NanamiPlates_Auras and NanamiPlates_Auras.timers then
|
|
NanamiPlates_Auras.timers[targetName .. "_" .. curseName] = nil
|
|
if guid then NanamiPlates_Auras.timers[guid .. "_" .. curseName] = nil end
|
|
end
|
|
if SpellDB.ownerBoundCache then
|
|
if SpellDB.ownerBoundCache[targetName] then
|
|
SpellDB.ownerBoundCache[targetName][curseName] = nil
|
|
end
|
|
if guid and SpellDB.ownerBoundCache[guid] then
|
|
SpellDB.ownerBoundCache[guid][curseName] = nil
|
|
end
|
|
end
|
|
if SpellDB.recentCasts then
|
|
SpellDB.recentCasts[curseName] = nil
|
|
end
|
|
end
|
|
|
|
local function ClearCurseTracking(targetName, guid, exceptCurse)
|
|
if not SpellDB or not SpellDB.WARLOCK_CURSES then return end
|
|
for curse, _ in pairs(SpellDB.WARLOCK_CURSES) do
|
|
if curse ~= exceptCurse then
|
|
if SpellDB.objects and SpellDB.objects[targetName] then
|
|
for lvl, effects in pairs(SpellDB.objects[targetName]) do
|
|
if effects[curse] then effects[curse] = nil end
|
|
end
|
|
end
|
|
if guid and SpellDB.objects and SpellDB.objects[guid] then
|
|
for lvl, effects in pairs(SpellDB.objects[guid]) do
|
|
if effects[curse] then effects[curse] = nil end
|
|
end
|
|
end
|
|
if NanamiPlates_Auras and NanamiPlates_Auras.timers then
|
|
NanamiPlates_Auras.timers[targetName .. "_" .. curse] = nil
|
|
if guid then NanamiPlates_Auras.timers[guid .. "_" .. curse] = nil end
|
|
end
|
|
if SpellDB.ownerBoundCache then
|
|
if SpellDB.ownerBoundCache[targetName] then
|
|
SpellDB.ownerBoundCache[targetName][curse] = nil
|
|
end
|
|
if guid and SpellDB.ownerBoundCache[guid] then
|
|
SpellDB.ownerBoundCache[guid][curse] = nil
|
|
end
|
|
end
|
|
if SpellDB.recentCasts then
|
|
SpellDB.recentCasts[curse] = nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function TrackSpellCast(spellName, duration)
|
|
if not SpellDB or not spellName then return end
|
|
if not UnitExists("target") then return end
|
|
|
|
-- Translate localized spell name to English for DB consistency
|
|
local englishName = spellName
|
|
if SpellDB.localeMap and SpellDB.localeMap[spellName] then
|
|
englishName = SpellDB.localeMap[spellName]
|
|
elseif SpellDB.learnedLocale and SpellDB.learnedLocale[spellName] then
|
|
englishName = SpellDB.learnedLocale[spellName]
|
|
end
|
|
|
|
-- Record this cast for locale learning
|
|
if SpellDB.DEBUFFS and SpellDB.DEBUFFS[englishName] then
|
|
SpellDB.lastCastSpell = englishName
|
|
else
|
|
SpellDB.lastCastSpell = spellName
|
|
end
|
|
SpellDB.lastCastTime = GetTime()
|
|
|
|
-- Re-fetch duration with English name if original was localized
|
|
if englishName ~= spellName and (not duration or duration <= 0) then
|
|
duration = SpellDB:GetDuration(englishName, 0)
|
|
end
|
|
spellName = englishName
|
|
|
|
if not duration or duration <= 0 then return end
|
|
|
|
local targetName = UnitName("target")
|
|
local targetLevel = UnitLevel("target") or 0
|
|
local guid = UnitGUID and UnitGUID("target")
|
|
|
|
if SpellDB.WARLOCK_CURSES and SpellDB.WARLOCK_CURSES[spellName] then
|
|
if not SpellDB:HasMalediction() then
|
|
ClearCurseTracking(targetName, guid, spellName)
|
|
else
|
|
if spellName == "Curse of Doom" then
|
|
ClearCurseTracking(targetName, guid, spellName)
|
|
elseif spellName == SpellDB.WARLOCK_AGONY_CURSE then
|
|
-- Agony cast: clear only Doom tracking, keep other curses
|
|
ClearSingleCurseTracking(targetName, guid, "Curse of Doom")
|
|
elseif SpellDB.WARLOCK_LONG_CURSES and SpellDB.WARLOCK_LONG_CURSES[spellName] then
|
|
-- Long curse cast: clear other long curses + Doom, keep Agony
|
|
for curse, _ in pairs(SpellDB.WARLOCK_CURSES) do
|
|
if curse ~= spellName and curse ~= SpellDB.WARLOCK_AGONY_CURSE then
|
|
ClearSingleCurseTracking(targetName, guid, curse)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
SpellDB:AddPending(targetName, targetLevel, spellName, duration)
|
|
SpellDB:RefreshEffect(targetName, targetLevel, spellName, duration, true)
|
|
|
|
if guid then
|
|
SpellDB:RefreshEffect(guid, targetLevel, spellName, duration, true)
|
|
end
|
|
|
|
if SpellDB.OWNER_BOUND_DEBUFFS and SpellDB.OWNER_BOUND_DEBUFFS[spellName] then
|
|
SpellDB:TrackOwnerBoundDebuff(targetName, spellName, duration)
|
|
if guid then SpellDB:TrackOwnerBoundDebuff(guid, spellName, duration) end
|
|
end
|
|
|
|
if NanamiPlates_Auras and NanamiPlates_Auras.timers then
|
|
NanamiPlates_Auras.timers[targetName .. "_" .. spellName] = nil
|
|
if guid then NanamiPlates_Auras.timers[guid .. "_" .. spellName] = nil end
|
|
end
|
|
|
|
-- Malediction auto-apply: casting Recklessness/Shadow/Elements also applies max rank Agony
|
|
if SpellDB:HasMalediction() and SpellDB.MALEDICTION_AUTO_AGONY
|
|
and SpellDB.MALEDICTION_AUTO_AGONY[spellName] then
|
|
local agonyName = SpellDB.WARLOCK_AGONY_CURSE
|
|
local agonyDuration = SpellDB:GetDuration(agonyName, 0)
|
|
if agonyDuration and agonyDuration > 0 then
|
|
SpellDB:RefreshEffect(targetName, targetLevel, agonyName, agonyDuration, true)
|
|
if guid then
|
|
SpellDB:RefreshEffect(guid, targetLevel, agonyName, agonyDuration, true)
|
|
end
|
|
if SpellDB.OWNER_BOUND_DEBUFFS and SpellDB.OWNER_BOUND_DEBUFFS[agonyName] then
|
|
SpellDB:TrackOwnerBoundDebuff(targetName, agonyName, agonyDuration)
|
|
if guid then SpellDB:TrackOwnerBoundDebuff(guid, agonyName, agonyDuration) end
|
|
end
|
|
if NanamiPlates_Auras and NanamiPlates_Auras.timers then
|
|
NanamiPlates_Auras.timers[targetName .. "_" .. agonyName] = nil
|
|
if guid then NanamiPlates_Auras.timers[guid .. "_" .. agonyName] = nil end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Spell cast hooks
|
|
local Original_CastSpell = CastSpell
|
|
CastSpell = function(spellId, bookType)
|
|
if SpellDB and spellId and bookType then
|
|
local spellName, rank = GetSpellName(spellId, bookType)
|
|
if spellName and UnitExists("target") and UnitCanAttack("player", "target") then
|
|
local duration = SpellDB:GetDuration(spellName, rank)
|
|
TrackSpellCast(spellName, duration)
|
|
end
|
|
end
|
|
return Original_CastSpell(spellId, bookType)
|
|
end
|
|
|
|
local Original_CastSpellByName = CastSpellByName
|
|
CastSpellByName = function(spellString, onSelf)
|
|
if SpellDB and spellString then
|
|
local spellName, rank = ParseSpellName(spellString)
|
|
if spellName and UnitExists("target") and UnitCanAttack("player", "target") then
|
|
local duration = SpellDB:GetDuration(spellName, rank)
|
|
TrackSpellCast(spellName, duration)
|
|
end
|
|
end
|
|
return Original_CastSpellByName(spellString, onSelf)
|
|
end
|
|
|
|
local Original_UseAction = UseAction
|
|
UseAction = function(slot, checkCursor, onSelf)
|
|
if SpellDB and slot then
|
|
local actionTexture = GetActionTexture(slot)
|
|
if GetActionText(slot) == nil and actionTexture ~= nil then
|
|
local spellName, rank = SpellDB:ScanAction(slot)
|
|
if spellName then
|
|
if SpellDB.textureToSpell then
|
|
local existing = SpellDB.textureToSpell[actionTexture]
|
|
if not existing or not SpellDB.DEBUFFS[existing] then
|
|
SpellDB.textureToSpell[actionTexture] = spellName
|
|
end
|
|
-- If spellName is localized and existing is English, learn mapping
|
|
if existing and SpellDB.DEBUFFS[existing] and spellName ~= existing then
|
|
if SpellDB.LearnLocale then
|
|
SpellDB:LearnLocale(spellName, existing)
|
|
end
|
|
spellName = existing
|
|
end
|
|
end
|
|
local duration = SpellDB:GetDuration(spellName, rank)
|
|
TrackSpellCast(spellName, duration)
|
|
end
|
|
end
|
|
end
|
|
return Original_UseAction(slot, checkCursor, onSelf)
|
|
end
|
|
|
|
if SpellDB then SpellDB:InitScanner() end
|
|
|
|
local function DisableObject(obj)
|
|
if not obj then return end
|
|
obj:Hide()
|
|
obj:SetAlpha(0)
|
|
if obj.SetWidth then obj:SetWidth(0.001) end
|
|
if obj.SetHeight then obj:SetHeight(0.001) end
|
|
end
|
|
|
|
local BORDER_PAD = 2
|
|
|
|
local function CreateNanamiBackdrop(frame)
|
|
local bgR, bgG, bgB, bgA = NP.GetThemeColor("panelBg", 0.1, 0.06, 0.1, 0.85)
|
|
local brR, brG, brB, brA = NP.GetThemeColor("panelBorder", 0.55, 0.30, 0.42, 0.9)
|
|
|
|
frame:SetBackdrop({
|
|
bgFile = "Interface\\Buttons\\WHITE8X8",
|
|
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
|
tile = false, tileSize = 0, edgeSize = 10,
|
|
insets = { left = BORDER_PAD, right = BORDER_PAD, top = BORDER_PAD, bottom = BORDER_PAD }
|
|
})
|
|
frame:SetBackdropColor(bgR, bgG, bgB, bgA)
|
|
frame:SetBackdropBorderColor(brR, brG, brB, brA)
|
|
end
|
|
|
|
local ARROW_TEXTURE = "Interface\\AddOns\\Nanami-Plates\\img\\arrow"
|
|
local ARROW_TEXCOORDS = {
|
|
{0, 0.5, 0, 0.5},
|
|
{0.5, 1, 0, 0.5},
|
|
{0, 0.5, 0.5, 1},
|
|
{0.5, 1, 0.5, 1},
|
|
}
|
|
NP.ARROW_TEXCOORDS = ARROW_TEXCOORDS
|
|
|
|
local function SetArrowTexCoords(f)
|
|
if not f or not f.tex then return end
|
|
local style = Settings.targetArrowStyle or 1
|
|
local coords = ARROW_TEXCOORDS[style] or ARROW_TEXCOORDS[1]
|
|
if f.side == "RIGHT" then
|
|
f.tex:SetTexCoord(coords[2], coords[1], coords[3], coords[4])
|
|
else
|
|
f.tex:SetTexCoord(coords[1], coords[2], coords[3], coords[4])
|
|
end
|
|
end
|
|
|
|
local function UpdateArrowPosition(f, healthBG)
|
|
if not f or not healthBG then return end
|
|
local offset = Settings.targetArrowOffset or 0
|
|
f:ClearAllPoints()
|
|
if f.side == "LEFT" then
|
|
f:SetPoint("RIGHT", healthBG, "LEFT", -offset, 0)
|
|
else
|
|
f:SetPoint("LEFT", healthBG, "RIGHT", offset, 0)
|
|
end
|
|
end
|
|
|
|
local function CreateArrowIndicator(parent, healthBG, side)
|
|
local size = Settings.targetArrowSize or 24
|
|
|
|
local f = CreateFrame("Frame", nil, parent)
|
|
f:SetWidth(size)
|
|
f:SetHeight(size)
|
|
f:SetFrameLevel(parent:GetFrameLevel() + 10)
|
|
f.side = side
|
|
f.healthBG = healthBG
|
|
|
|
UpdateArrowPosition(f, healthBG)
|
|
|
|
local tex = f:CreateTexture(nil, "OVERLAY")
|
|
tex:SetTexture(ARROW_TEXTURE)
|
|
tex:SetAllPoints(f)
|
|
f.tex = tex
|
|
|
|
SetArrowTexCoords(f)
|
|
|
|
f:Hide()
|
|
return f
|
|
end
|
|
|
|
function NP.UpdateAllArrows()
|
|
local size = Settings.targetArrowSize or 24
|
|
for frame, np in pairs(registry) do
|
|
if np.targetArrowL then
|
|
np.targetArrowL:SetWidth(size)
|
|
np.targetArrowL:SetHeight(size)
|
|
SetArrowTexCoords(np.targetArrowL)
|
|
UpdateArrowPosition(np.targetArrowL, np.targetArrowL.healthBG)
|
|
end
|
|
if np.targetArrowR then
|
|
np.targetArrowR:SetWidth(size)
|
|
np.targetArrowR:SetHeight(size)
|
|
SetArrowTexCoords(np.targetArrowR)
|
|
UpdateArrowPosition(np.targetArrowR, np.targetArrowR.healthBG)
|
|
end
|
|
end
|
|
end
|
|
|
|
local function HandleNamePlate(frame)
|
|
if registry[frame] then return end
|
|
|
|
platecount = platecount + 1
|
|
local plateName = "NanamiPlate" .. platecount
|
|
|
|
local r1, r2, r3, r4, r5, r6 = frame:GetRegions()
|
|
local regions = {r1, r2, r3, r4, r5, r6}
|
|
|
|
local original = {}
|
|
for i, key in ipairs(REGION_ORDER) do
|
|
original[key] = regions[i]
|
|
end
|
|
|
|
local children = { frame:GetChildren() }
|
|
original.healthbar = children[1]
|
|
if children[2] and children[2].GetObjectType and children[2]:GetObjectType() == "StatusBar" then
|
|
original.castbar = children[2]
|
|
end
|
|
|
|
original.name = original.name or r3
|
|
original.level = original.level or r4
|
|
|
|
-- Set parent to 1x1 to fully disable engine anti-overlap (ShaguPlates technique)
|
|
-- Custom stacking in ResolveStacking() handles overlap instead
|
|
frame:SetHeight(1)
|
|
frame:SetWidth(1)
|
|
|
|
-- Create overlay
|
|
local np = CreateFrame("Button", plateName, frame)
|
|
np:SetAllPoints(frame)
|
|
np:SetFrameLevel(frame:GetFrameLevel() + 1)
|
|
np:EnableMouse(false)
|
|
local rawStrata = np:GetFrameStrata()
|
|
local VALID_STRATA = { BACKGROUND=1, LOW=1, MEDIUM=1, HIGH=1, DIALOG=1, FULLSCREEN=1, FULLSCREEN_DIALOG=1, TOOLTIP=1 }
|
|
np._defaultStrata = VALID_STRATA[rawStrata] and rawStrata or "BACKGROUND"
|
|
|
|
np.original = original
|
|
|
|
-- Health bar background frame (also contains mana bar)
|
|
local healthBG = CreateFrame("Frame", nil, np)
|
|
healthBG:SetHeight(Settings.healthbarHeight + BORDER_PAD * 2)
|
|
healthBG:SetWidth(Settings.healthbarWidth + BORDER_PAD * 2)
|
|
healthBG:SetPoint("CENTER", np, "CENTER", 0, Settings.nameplateYOffset or 0)
|
|
CreateNanamiBackdrop(healthBG)
|
|
np.healthBG = healthBG
|
|
|
|
-- Health bar (single anchor + explicit size, so mana can fit below)
|
|
local health = CreateFrame("StatusBar", plateName .. "Health", healthBG)
|
|
health:SetStatusBarTexture(NP.GetTexture())
|
|
health:SetPoint("TOPLEFT", healthBG, "TOPLEFT", BORDER_PAD, -BORDER_PAD)
|
|
health:SetWidth(Settings.healthbarWidth)
|
|
health:SetHeight(Settings.healthbarHeight)
|
|
health:SetMinMaxValues(0, 1)
|
|
health:SetValue(1)
|
|
np.health = health
|
|
|
|
local healthBGTex = health:CreateTexture(nil, "BACKGROUND")
|
|
healthBGTex:SetAllPoints(health)
|
|
healthBGTex:SetTexture(NP.GetTexture())
|
|
healthBGTex:SetVertexColor(0.15, 0.15, 0.15, 0.8)
|
|
np.healthBGTex = healthBGTex
|
|
|
|
-- Target highlight: additive StatusBar overlay that brightens only the filled portion
|
|
local targetGlow = CreateFrame("StatusBar", nil, healthBG)
|
|
targetGlow:SetAllPoints(health)
|
|
targetGlow:SetStatusBarTexture(NP.GetTexture())
|
|
targetGlow:SetFrameLevel(health:GetFrameLevel() + 1)
|
|
targetGlow:SetMinMaxValues(0, 1)
|
|
targetGlow:SetValue(1)
|
|
targetGlow:SetAlpha(0.35)
|
|
targetGlow:Hide()
|
|
np.targetGlow = targetGlow
|
|
local glowRegions = { targetGlow:GetRegions() }
|
|
for _, reg in ipairs(glowRegions) do
|
|
if reg and reg.SetBlendMode then reg:SetBlendMode("ADD") end
|
|
end
|
|
|
|
-- Mana/Energy/Rage bar (inside healthBG, below health bar)
|
|
local manaBar = CreateFrame("StatusBar", plateName .. "Mana", healthBG)
|
|
manaBar:SetStatusBarTexture(NP.GetTexture())
|
|
manaBar:SetPoint("TOPLEFT", health, "BOTTOMLEFT", 0, -1)
|
|
manaBar:SetWidth(Settings.healthbarWidth)
|
|
manaBar:SetHeight(Settings.manabarHeight or 3)
|
|
manaBar:SetMinMaxValues(0, 1)
|
|
manaBar:SetValue(1)
|
|
manaBar:SetStatusBarColor(0, 0, 1, 1)
|
|
manaBar:Hide()
|
|
np.manaBar = manaBar
|
|
|
|
local manaBGTex = manaBar:CreateTexture(nil, "BACKGROUND")
|
|
manaBGTex:SetAllPoints(manaBar)
|
|
manaBGTex:SetTexture(NP.GetTexture())
|
|
manaBGTex:SetVertexColor(0.05, 0.05, 0.1, 0.8)
|
|
|
|
-- Thin separator line between health and mana
|
|
local brR, brG, brB = NP.GetThemeColor("panelBorder", 0.55, 0.30, 0.42, 0.9)
|
|
local manaSep = healthBG:CreateTexture(nil, "ARTWORK")
|
|
manaSep:SetTexture("Interface\\Buttons\\WHITE8X8")
|
|
manaSep:SetHeight(1)
|
|
manaSep:SetPoint("TOPLEFT", health, "BOTTOMLEFT", 0, 0)
|
|
manaSep:SetPoint("TOPRIGHT", health, "BOTTOMRIGHT", 0, 0)
|
|
manaSep:SetVertexColor(brR, brG, brB, 0.5)
|
|
manaSep:Hide()
|
|
np.manaSep = manaSep
|
|
|
|
-- Health text
|
|
local healthText = health:CreateFontString(nil, "OVERLAY")
|
|
healthText:SetFont(NP.GetFont(), Settings.healthFontSize, NP.GetFontOutline())
|
|
healthText:SetPoint("TOPRIGHT", health, "TOPRIGHT", -2, 0)
|
|
healthText:SetPoint("BOTTOMRIGHT", health, "BOTTOMRIGHT", -2, 0)
|
|
healthText:SetJustifyV("MIDDLE")
|
|
healthText:SetJustifyH("RIGHT")
|
|
healthText:SetTextColor(1, 1, 1, 1)
|
|
np.healthText = healthText
|
|
|
|
-- Threat percentage text
|
|
local threatPctText = health:CreateFontString(nil, "OVERLAY")
|
|
threatPctText:SetFont(NP.GetFont(), Settings.healthFontSize, NP.GetFontOutline())
|
|
threatPctText:SetPoint("CENTER", health, "CENTER", 0, 0)
|
|
threatPctText:SetTextColor(1, 1, 1, 1)
|
|
threatPctText:SetText("")
|
|
np.threatPctText = threatPctText
|
|
|
|
-- Name text
|
|
local name = np:CreateFontString(nil, "OVERLAY")
|
|
name:SetFont(NP.GetFont(), Settings.nameFontSize, NP.GetFontOutline())
|
|
name:SetPoint("BOTTOM", healthBG, "TOP", 0, 2)
|
|
name:SetTextColor(1, 1, 1, 1)
|
|
np.name = name
|
|
|
|
-- Level text (on health bar so it renders above the bar fill)
|
|
local level = health:CreateFontString(nil, "OVERLAY")
|
|
level:SetFont(NP.GetFont(), Settings.levelFontSize, NP.GetFontOutline())
|
|
level:SetPoint("TOPLEFT", health, "TOPLEFT", 2, 0)
|
|
level:SetPoint("BOTTOMLEFT", health, "BOTTOMLEFT", 2, 0)
|
|
level:SetJustifyV("MIDDLE")
|
|
level:SetJustifyH("LEFT")
|
|
level:SetTextColor(1, 1, 0.6, 1)
|
|
np.level = level
|
|
|
|
-- Raid icon
|
|
local raidIcon = np:CreateTexture(nil, "OVERLAY")
|
|
raidIcon:SetTexture("Interface\\TargetingFrame\\UI-RaidTargetingIcons")
|
|
raidIcon:SetWidth(16)
|
|
raidIcon:SetHeight(16)
|
|
raidIcon:SetPoint("LEFT", healthBG, "RIGHT", 3, 0)
|
|
raidIcon:Hide()
|
|
np.raidIcon = raidIcon
|
|
|
|
-- Castbar background (anchors dynamically below healthBG)
|
|
local castbarBG = CreateFrame("Frame", nil, np)
|
|
castbarBG:SetHeight(Settings.castbarHeight + 2)
|
|
castbarBG:SetWidth(Settings.healthbarWidth + 2)
|
|
castbarBG:SetPoint("TOP", healthBG, "BOTTOM", 0, -1)
|
|
CreateNanamiBackdrop(castbarBG)
|
|
castbarBG:Hide()
|
|
np.castbarBG = castbarBG
|
|
|
|
-- Castbar
|
|
local castbar = CreateFrame("StatusBar", plateName .. "Castbar", castbarBG)
|
|
castbar:SetStatusBarTexture(NP.GetTexture())
|
|
castbar:SetPoint("TOPLEFT", castbarBG, "TOPLEFT", 1, -1)
|
|
castbar:SetPoint("BOTTOMRIGHT", castbarBG, "BOTTOMRIGHT", -1, 1)
|
|
castbar:SetMinMaxValues(0, 1)
|
|
castbar:SetValue(0)
|
|
castbar:SetStatusBarColor(Settings.castbarColor[1], Settings.castbarColor[2], Settings.castbarColor[3], Settings.castbarColor[4])
|
|
np.castbar = castbar
|
|
|
|
local castbarBGTex = castbar:CreateTexture(nil, "BACKGROUND")
|
|
castbarBGTex:SetAllPoints(castbar)
|
|
castbarBGTex:SetTexture(NP.GetTexture())
|
|
castbarBGTex:SetVertexColor(0.1, 0.1, 0.1, 0.8)
|
|
|
|
local castbarText = castbar:CreateFontString(nil, "OVERLAY")
|
|
castbarText:SetFont(NP.GetFont(), Settings.healthFontSize - 1, NP.GetFontOutline())
|
|
castbarText:SetPoint("CENTER", castbar, "CENTER", 0, 0)
|
|
castbarText:SetTextColor(1, 1, 1, 1)
|
|
np.castbarText = castbarText
|
|
|
|
local castbarTimer = castbar:CreateFontString(nil, "OVERLAY")
|
|
castbarTimer:SetFont(NP.GetFont(), Settings.healthFontSize - 1, NP.GetFontOutline())
|
|
castbarTimer:SetPoint("RIGHT", castbar, "RIGHT", -2, 0)
|
|
castbarTimer:SetTextColor(1, 1, 1, 1)
|
|
np.castbarTimer = castbarTimer
|
|
|
|
-- Castbar icon (anchored to castbar, not healthbar, to avoid chevron overlap)
|
|
local castbarIcon = castbarBG:CreateTexture(nil, "OVERLAY")
|
|
castbarIcon:SetWidth(Settings.castbarHeight + 8)
|
|
castbarIcon:SetHeight(Settings.castbarHeight + 8)
|
|
castbarIcon:SetPoint("RIGHT", castbarBG, "LEFT", -2, 0)
|
|
castbarIcon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
|
|
castbarIcon:Hide()
|
|
np.castbarIcon = castbarIcon
|
|
|
|
-- Create aura frames (filled in by Auras module)
|
|
if NanamiPlates_Auras then
|
|
NanamiPlates_Auras:CreateDebuffFrames(np)
|
|
end
|
|
|
|
-- Create combo point frames (filled in by ComboPoints module)
|
|
if NanamiPlates_ComboPoints then
|
|
NanamiPlates_ComboPoints:CreateComboPointFrames(np)
|
|
end
|
|
|
|
-- Quest mob indicator icon (yellow "!" to the left of the name)
|
|
local questIcon = np:CreateTexture(nil, "OVERLAY")
|
|
questIcon:SetTexture("Interface\\GossipFrame\\AvailableQuestIcon")
|
|
questIcon:SetWidth(12)
|
|
questIcon:SetHeight(12)
|
|
questIcon:SetPoint("RIGHT", name, "LEFT", -1, 0)
|
|
questIcon:Hide()
|
|
np.questIcon = questIcon
|
|
|
|
-- Quest progress text (e.g. "3/8" to the right of the name)
|
|
local questText = np:CreateFontString(nil, "OVERLAY")
|
|
questText:SetFont(NP.GetFont(), Settings.nameFontSize - 1, NP.GetFontOutline())
|
|
questText:SetPoint("LEFT", name, "RIGHT", 2, 0)
|
|
questText:SetTextColor(1, 0.82, 0, 1)
|
|
questText:Hide()
|
|
np.questText = questText
|
|
|
|
-- Target arrow indicators >> [healthbar] <<
|
|
np.targetArrowL = CreateArrowIndicator(np, healthBG, "LEFT")
|
|
np.targetArrowR = CreateArrowIndicator(np, healthBG, "RIGHT")
|
|
|
|
-- Hide original elements thoroughly (keep healthbar color intact for unit type detection)
|
|
for _, key in ipairs(REGION_ORDER) do
|
|
if original[key] then DisableObject(original[key]) end
|
|
end
|
|
if original.healthbar then
|
|
local hbRegions = { original.healthbar:GetRegions() }
|
|
for _, reg in ipairs(hbRegions) do
|
|
if reg and reg.Hide then reg:Hide() end
|
|
if reg and reg.SetAlpha then reg:SetAlpha(0) end
|
|
end
|
|
original.healthbar:ClearAllPoints()
|
|
original.healthbar:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 0, 0)
|
|
original.healthbar:SetWidth(0.001)
|
|
original.healthbar:SetHeight(0.001)
|
|
end
|
|
if original.castbar then
|
|
original.castbar:SetAlpha(0.001)
|
|
local cbRegions = { original.castbar:GetRegions() }
|
|
for _, reg in ipairs(cbRegions) do
|
|
if reg and reg.SetAlpha then reg:SetAlpha(0) end
|
|
end
|
|
end
|
|
|
|
-- Tracking state
|
|
np.isTarget = false
|
|
np.lastDebuffUpdate = 0
|
|
np.unitstr = nil
|
|
np.plateName = nil
|
|
np._lastFriendly = nil
|
|
np._lastManaShowing = nil
|
|
np._lastFriendly_resize = nil
|
|
|
|
frame.nameplate = np
|
|
registry[frame] = np
|
|
|
|
-- OnShow: reset and re-hide original elements
|
|
local oldOnShow = frame:GetScript("OnShow")
|
|
frame:SetScript("OnShow", function()
|
|
if oldOnShow then oldOnShow() end
|
|
this:SetHeight(1)
|
|
this:SetWidth(1)
|
|
local nameplate = this.nameplate
|
|
if nameplate then
|
|
Healthbar.ResetCache(nameplate)
|
|
nameplate.isTarget = false
|
|
nameplate._isTargetStrata = nil
|
|
nameplate.lastDebuffUpdate = 0
|
|
nameplate.unitstr = nil
|
|
nameplate._lastFriendly = nil
|
|
nameplate._lastManaShowing = nil
|
|
nameplate._lastFriendly_resize = nil
|
|
nameplate._lastAlpha = nil
|
|
nameplate._stackY = nil
|
|
nameplate._appliedOffY = nil
|
|
nameplate.healthBG:ClearAllPoints()
|
|
nameplate.healthBG:SetPoint("CENTER", nameplate, "CENTER", 0, Settings.nameplateYOffset or 0)
|
|
if nameplate._defaultStrata then
|
|
nameplate:SetFrameStrata(nameplate._defaultStrata)
|
|
end
|
|
nameplate.castbarBG:Hide()
|
|
nameplate.castbarIcon:Hide()
|
|
nameplate.manaBar:Hide()
|
|
if nameplate.manaSep then nameplate.manaSep:Hide() end
|
|
if nameplate.targetArrowL then nameplate.targetArrowL:Hide() end
|
|
if nameplate.targetArrowR then nameplate.targetArrowR:Hide() end
|
|
if nameplate.comboPoints then
|
|
for i = 1, 5 do
|
|
if nameplate.comboPoints[i] then nameplate.comboPoints[i]:Hide() end
|
|
end
|
|
end
|
|
if nameplate.questIcon then nameplate.questIcon:Hide() end
|
|
if nameplate.questText then nameplate.questText:Hide() end
|
|
|
|
-- Re-hide original elements (Blizzard resets them on show)
|
|
local orig = nameplate.original
|
|
for _, key in ipairs(REGION_ORDER) do
|
|
if orig[key] then
|
|
orig[key]:Hide()
|
|
if orig[key].SetAlpha then orig[key]:SetAlpha(0) end
|
|
end
|
|
end
|
|
if orig.healthbar then
|
|
orig.healthbar:SetWidth(0.001)
|
|
orig.healthbar:SetHeight(0.001)
|
|
local hbRegions = { orig.healthbar:GetRegions() }
|
|
for _, reg in ipairs(hbRegions) do
|
|
if reg and reg.Hide then reg:Hide() end
|
|
end
|
|
end
|
|
if orig.castbar then
|
|
orig.castbar:SetAlpha(0.001)
|
|
local cbRegions = { orig.castbar:GetRegions() }
|
|
for _, reg in ipairs(cbRegions) do
|
|
if reg and reg.SetAlpha then reg:SetAlpha(0) end
|
|
end
|
|
end
|
|
|
|
if Healthbar.ShouldSkipNameplate(this, nameplate, nameplate.original, Settings) then
|
|
nameplate:Hide()
|
|
else
|
|
nameplate:Show()
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- OnHide: cleanup
|
|
local oldOnHide = frame:GetScript("OnHide")
|
|
frame:SetScript("OnHide", function()
|
|
if oldOnHide then oldOnHide() end
|
|
local nameplate = this.nameplate
|
|
if nameplate then
|
|
nameplate.castbarBG:Hide()
|
|
nameplate.castbarIcon:Hide()
|
|
nameplate.manaBar:Hide()
|
|
if nameplate.manaSep then nameplate.manaSep:Hide() end
|
|
if nameplate.targetArrowL then nameplate.targetArrowL:Hide() end
|
|
if nameplate.targetArrowR then nameplate.targetArrowR:Hide() end
|
|
if nameplate.questIcon then nameplate.questIcon:Hide() end
|
|
if nameplate.questText then nameplate.questText:Hide() end
|
|
end
|
|
end)
|
|
end
|
|
|
|
local function ShortenNumber(n)
|
|
if n >= 1000000 then
|
|
return string_format("%.1fM", n / 1000000)
|
|
elseif n >= 10000 then
|
|
return string_format("%.1fK", n / 1000)
|
|
else
|
|
return tostring(n)
|
|
end
|
|
end
|
|
|
|
local function FormatHealthText(current, max, format)
|
|
if not format or format == 0 then return "" end
|
|
if max == 0 then return "" end
|
|
local pct = math_floor(current / max * 100 + 0.5)
|
|
if format == 1 then return pct .. "%" end
|
|
if format == 2 then return ShortenNumber(current) end
|
|
if format == 3 then return ShortenNumber(current) .. " (" .. pct .. "%)" end
|
|
if format == 4 then return ShortenNumber(current) .. "/" .. ShortenNumber(max) end
|
|
if format == 5 then return ShortenNumber(current) .. "/" .. ShortenNumber(max) .. " " .. pct .. "%" end
|
|
return pct .. "%"
|
|
end
|
|
|
|
local function GetLevelDiffColor(unitLevel)
|
|
local playerLevel = UnitLevel("player") or 60
|
|
local diff = unitLevel - playerLevel
|
|
if diff >= 5 then return 1, 0, 0
|
|
elseif diff >= 3 then return 1, 0.5, 0
|
|
elseif diff >= -2 then return 1, 1, 0
|
|
elseif diff >= -4 - math_floor(playerLevel / 10) then return 0.25, 0.75, 0.25
|
|
else return 0.5, 0.5, 0.5
|
|
end
|
|
end
|
|
|
|
local function HideOriginalElements(original)
|
|
for _, key in ipairs(REGION_ORDER) do
|
|
local obj = original[key]
|
|
if obj and obj.IsShown and obj:IsShown() then
|
|
obj:Hide()
|
|
if obj.SetAlpha then obj:SetAlpha(0) end
|
|
end
|
|
end
|
|
if original.castbar and original.castbar:GetAlpha() > 0.01 then
|
|
original.castbar:SetAlpha(0.001)
|
|
end
|
|
end
|
|
|
|
local function UpdateNamePlate(frame)
|
|
local np = frame.nameplate
|
|
if not np then return end
|
|
if not np:IsShown() then return end
|
|
|
|
local original = np.original
|
|
if not original or not original.healthbar then return end
|
|
|
|
-- Keep parent 1x1 to disable engine anti-overlap entirely
|
|
if frame:GetHeight() > 1 or frame:GetWidth() > 1 then
|
|
frame:SetHeight(1)
|
|
frame:SetWidth(1)
|
|
end
|
|
|
|
-- Continuously enforce hiding of original elements
|
|
HideOriginalElements(original)
|
|
|
|
-- Unit name
|
|
local plateName = ""
|
|
if original.name and original.name.GetText then
|
|
plateName = original.name:GetText() or ""
|
|
end
|
|
np.plateName = plateName
|
|
np.name:SetText(plateName)
|
|
|
|
-- SuperWoW unit detection
|
|
local unitstr = nil
|
|
local hasValidGUID = false
|
|
if superwow_active and frame.GetName then
|
|
unitstr = frame:GetName(1)
|
|
if unitstr and UnitExists(unitstr) then
|
|
hasValidGUID = true
|
|
else
|
|
unitstr = nil
|
|
end
|
|
end
|
|
np.unitstr = unitstr
|
|
|
|
-- Unit type detection
|
|
local isHostile, isNeutral, isFriendly, origR, origG, origB = Healthbar.DetectUnitType(np, original)
|
|
Healthbar.CheckUnitChange(np, plateName, isNeutral)
|
|
|
|
-- Override neutral→hostile when the mob has entered combat (e.g. after player attacks it)
|
|
if (isNeutral or np.wasNeutral) and unitstr and hasValidGUID
|
|
and UnitAffectingCombat and UnitAffectingCombat(unitstr) then
|
|
isHostile = true
|
|
isNeutral = false
|
|
isFriendly = false
|
|
np.wasNeutral = false
|
|
end
|
|
|
|
local isFriendlyStyle = isFriendly
|
|
|
|
-- Tapped detection: mob tagged by another player outside our group (unlootable)
|
|
local isTapped = false
|
|
if unitstr and hasValidGUID and UnitIsTapped then
|
|
if UnitIsTapped(unitstr) and (not UnitIsTappedByPlayer or not UnitIsTappedByPlayer(unitstr)) then
|
|
isTapped = true
|
|
end
|
|
end
|
|
-- Color fallback: hostile/neutral/friendly all have B≈0; tapped grey has B>0
|
|
if not isTapped and not isHostile and not isNeutral and origB > 0.1 and origR > 0.15 then
|
|
isTapped = true
|
|
end
|
|
if isTapped then
|
|
isFriendlyStyle = false
|
|
end
|
|
|
|
-- Health values
|
|
local hp = original.healthbar:GetValue() or 0
|
|
local _, hpmax = original.healthbar:GetMinMaxValues()
|
|
hpmax = hpmax or 1
|
|
if hpmax == 0 then hpmax = 1 end
|
|
np.health:SetMinMaxValues(0, hpmax)
|
|
np.health:SetValue(hp)
|
|
if np.targetGlow then
|
|
np.targetGlow:SetMinMaxValues(0, hpmax)
|
|
np.targetGlow:SetValue(hp)
|
|
end
|
|
|
|
-- Dimensions (only update when style changes)
|
|
local hHeight, hWidth, hFontSize, lFontSize, nFontSize, hTextFormat
|
|
if isFriendlyStyle then
|
|
hHeight = Settings.friendHealthbarHeight
|
|
hWidth = Settings.friendHealthbarWidth
|
|
hFontSize = Settings.friendHealthFontSize
|
|
lFontSize = Settings.friendLevelFontSize
|
|
nFontSize = Settings.friendNameFontSize
|
|
hTextFormat = Settings.friendHealthTextFormat
|
|
else
|
|
hHeight = Settings.healthbarHeight
|
|
hWidth = Settings.healthbarWidth
|
|
hFontSize = Settings.healthFontSize
|
|
lFontSize = Settings.levelFontSize
|
|
nFontSize = Settings.nameFontSize
|
|
hTextFormat = Settings.healthTextFormat
|
|
end
|
|
|
|
if np._lastFriendly ~= isFriendlyStyle then
|
|
np._lastFriendly = isFriendlyStyle
|
|
np.health:SetWidth(hWidth)
|
|
np.health:SetHeight(hHeight)
|
|
np.name:SetFont(NP.GetFont(), nFontSize, NP.GetFontOutline())
|
|
np.level:SetFont(NP.GetFont(), lFontSize, NP.GetFontOutline())
|
|
np.healthText:SetFont(NP.GetFont(), hFontSize, NP.GetFontOutline())
|
|
end
|
|
|
|
-- Health text
|
|
np.healthText:SetText(FormatHealthText(hp, hpmax, hTextFormat))
|
|
|
|
-- Level text and classification
|
|
local unitLevel = nil
|
|
local isBoss = original.levelicon and original.levelicon.IsShown and original.levelicon:IsShown()
|
|
local levelText = (original.level and original.level.GetText) and original.level:GetText() or nil
|
|
|
|
if levelText then
|
|
unitLevel = tonumber(levelText)
|
|
end
|
|
|
|
local classification = "normal"
|
|
if isBoss then
|
|
classification = "worldboss"
|
|
elseif isTarget and UnitExists("target") and UnitClassification then
|
|
classification = UnitClassification("target") or "normal"
|
|
elseif unitstr and UnitExists(unitstr) and UnitClassification then
|
|
classification = UnitClassification(unitstr) or "normal"
|
|
elseif original.border and original.border.GetVertexColor then
|
|
local br, bg, bb = original.border:GetVertexColor()
|
|
if br and bg and bb and br > 0.6 and bg > 0.5 and bb < 0.3 then
|
|
classification = "elite"
|
|
end
|
|
end
|
|
|
|
if classification == "worldboss" then
|
|
np.level:SetText("??")
|
|
np.level:SetTextColor(1, 0, 0)
|
|
elseif levelText then
|
|
if classification == "rareelite" then
|
|
np.level:SetText(levelText .. "+")
|
|
np.level:SetTextColor(0.8, 0.8, 0.8)
|
|
elseif classification == "elite" then
|
|
np.level:SetText(levelText .. "+")
|
|
if unitLevel then
|
|
local lr, lg, lb = GetLevelDiffColor(unitLevel)
|
|
np.level:SetTextColor(lr, lg, lb)
|
|
else
|
|
np.level:SetTextColor(1, 1, 0.6)
|
|
end
|
|
elseif classification == "rare" then
|
|
np.level:SetText(levelText)
|
|
np.level:SetTextColor(0.8, 0.8, 0.8)
|
|
else
|
|
np.level:SetText(levelText)
|
|
if unitLevel then
|
|
local lr, lg, lb = GetLevelDiffColor(unitLevel)
|
|
np.level:SetTextColor(lr, lg, lb)
|
|
else
|
|
np.level:SetTextColor(1, 1, 0.6)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Raid icon
|
|
if original.raidicon and original.raidicon.IsShown and original.raidicon:IsShown() then
|
|
local ux, uy = original.raidicon:GetTexCoord()
|
|
np.raidIcon:SetTexCoord(ux, ux + 0.25, uy, uy + 0.25)
|
|
np.raidIcon:Show()
|
|
else
|
|
np.raidIcon:Hide()
|
|
end
|
|
|
|
-- Quest mob indicator
|
|
if Settings.showQuestIcon and not isFriendlyStyle then
|
|
local questInfo = GetQuestMobInfo(plateName)
|
|
if questInfo then
|
|
if not np.questIcon:IsShown() then np.questIcon:Show() end
|
|
if questInfo.current and questInfo.needed then
|
|
np.questText:SetText(questInfo.current .. "/" .. questInfo.needed)
|
|
if not np.questText:IsShown() then np.questText:Show() end
|
|
else
|
|
if np.questText:IsShown() then np.questText:Hide() end
|
|
end
|
|
else
|
|
if np.questIcon:IsShown() then np.questIcon:Hide() end
|
|
if np.questText:IsShown() then np.questText:Hide() end
|
|
end
|
|
else
|
|
if np.questIcon:IsShown() then np.questIcon:Hide() end
|
|
if np.questText:IsShown() then np.questText:Hide() end
|
|
end
|
|
|
|
-- Health bar color
|
|
local barR, barG, barB = origR, origG, origB
|
|
|
|
if isTapped then
|
|
barR, barG, barB = Colors.tapped[1], Colors.tapped[2], Colors.tapped[3]
|
|
elseif isHostile then
|
|
barR, barG, barB = Colors.hostile[1], Colors.hostile[2], Colors.hostile[3]
|
|
elseif isNeutral or Healthbar.WasNeutral(np) then
|
|
barR, barG, barB = Colors.neutral[1], Colors.neutral[2], Colors.neutral[3]
|
|
elseif isFriendly then
|
|
barR, barG, barB = Colors.friendly[1], Colors.friendly[2], Colors.friendly[3]
|
|
else
|
|
barR, barG, barB = Colors.hostile[1], Colors.hostile[2], Colors.hostile[3]
|
|
end
|
|
|
|
-- Class colors for players
|
|
if unitstr and hasValidGUID and UnitIsPlayer(unitstr) then
|
|
local _, unitClass = UnitClass(unitstr)
|
|
if unitClass and Colors.class[unitClass] then
|
|
barR = Colors.class[unitClass][1]
|
|
barG = Colors.class[unitClass][2]
|
|
barB = Colors.class[unitClass][3]
|
|
end
|
|
end
|
|
|
|
-- Threat coloring (overrides basic colors for hostile mobs)
|
|
if isHostile and not isTapped then
|
|
local threatColor = nil
|
|
local mobGUID = unitstr
|
|
local threatPctValue = nil
|
|
local threatPctRole = NP.playerRole or "DPS"
|
|
|
|
if NanamiPlates_Threat then
|
|
local hasData, playerHasAggro, otherName, otherPct = NanamiPlates_Threat.GetTWTankModeThreat(mobGUID, plateName)
|
|
|
|
if hasData then
|
|
local role = threatPctRole
|
|
if role == "TANK" then
|
|
if playerHasAggro then
|
|
threatColor = THREAT_COLORS.TANK.AGGRO
|
|
threatPctValue = 100
|
|
elseif otherPct and otherPct > 70 then
|
|
threatColor = THREAT_COLORS.TANK.LOSING_AGGRO
|
|
threatPctValue = otherPct
|
|
elseif otherName and NP.TANK_CLASSES and NP.GetPlayerClassByName then
|
|
local otherClass = NP.GetPlayerClassByName(otherName)
|
|
if otherClass and NP.TANK_CLASSES[otherClass] then
|
|
threatColor = THREAT_COLORS.TANK.OTHER_TANK
|
|
else
|
|
threatColor = THREAT_COLORS.TANK.NO_AGGRO
|
|
end
|
|
threatPctValue = otherPct or 0
|
|
else
|
|
threatColor = THREAT_COLORS.TANK.NO_AGGRO
|
|
threatPctValue = otherPct or 0
|
|
end
|
|
else
|
|
if playerHasAggro then
|
|
threatColor = THREAT_COLORS.DPS.AGGRO
|
|
threatPctValue = 100
|
|
elseif otherPct and otherPct > 70 then
|
|
threatColor = THREAT_COLORS.DPS.HIGH_THREAT
|
|
threatPctValue = otherPct
|
|
else
|
|
threatColor = THREAT_COLORS.DPS.NO_AGGRO
|
|
threatPctValue = otherPct or 0
|
|
end
|
|
end
|
|
end
|
|
|
|
if not hasData and NanamiDPS and NanamiDPS.ThreatEngine then
|
|
local TE = NanamiDPS.ThreatEngine
|
|
if TE.inCombat and unitstr then
|
|
local data = TE:QueryUnitThreat(unitstr)
|
|
if not data and plateName then
|
|
local tk = TE.GetTargetKey and TE.GetTargetKey(unitstr)
|
|
if tk then
|
|
local t, isTk, pct = TE:QueryNameThreat(tk, UnitName("player"))
|
|
if t > 0 then
|
|
data = { pct = pct, isTanking = isTk, threat = t,
|
|
tankThreat = 0, secondPct = 0, secondName = nil }
|
|
end
|
|
end
|
|
end
|
|
if data then
|
|
local role = threatPctRole
|
|
if role == "TANK" then
|
|
if data.isTanking then
|
|
threatColor = THREAT_COLORS.TANK.AGGRO
|
|
threatPctValue = data.secondPct or 0
|
|
else
|
|
threatColor = THREAT_COLORS.TANK.NO_AGGRO
|
|
threatPctValue = data.pct or 0
|
|
end
|
|
else
|
|
if data.isTanking then
|
|
threatColor = THREAT_COLORS.DPS.AGGRO
|
|
threatPctValue = 100
|
|
elseif data.pct and data.pct > 70 then
|
|
threatColor = THREAT_COLORS.DPS.HIGH_THREAT
|
|
threatPctValue = data.pct
|
|
else
|
|
threatColor = THREAT_COLORS.DPS.NO_AGGRO
|
|
threatPctValue = data.pct or 0
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Stun override
|
|
if unitstr and hasValidGUID then
|
|
for _, stunEffect in ipairs(NP.STUN_EFFECTS) do
|
|
local data = SpellDB and SpellDB:FindEffectData(unitstr, 0, stunEffect)
|
|
if data and data.start and data.duration then
|
|
if data.start + data.duration > GetTime() then
|
|
threatColor = THREAT_COLORS.STUN
|
|
threatPctValue = nil
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if threatColor then
|
|
barR, barG, barB = threatColor[1], threatColor[2], threatColor[3]
|
|
end
|
|
|
|
if np.threatPctText then
|
|
if threatPctValue and threatColor then
|
|
np.threatPctText:SetText(string.format("%.0f%%", threatPctValue))
|
|
np.threatPctText:SetTextColor(threatColor[1], threatColor[2], threatColor[3], 1)
|
|
np.threatPctText:Show()
|
|
else
|
|
np.threatPctText:SetText("")
|
|
np.threatPctText:Hide()
|
|
end
|
|
end
|
|
else
|
|
if np.threatPctText then
|
|
np.threatPctText:SetText("")
|
|
np.threatPctText:Hide()
|
|
end
|
|
end
|
|
|
|
np.health:SetStatusBarColor(barR, barG, barB, 1)
|
|
|
|
-- Sync target glow with final bar color (must happen here, not in UpdateTarget,
|
|
-- because the bar color may have changed this frame due to combat/threat)
|
|
if np.targetGlow and np.targetGlow:IsShown() then
|
|
np.targetGlow:SetStatusBarColor(barR, barG, barB, 1)
|
|
local lum = 0.299 * barR + 0.587 * barG + 0.114 * barB
|
|
local glowAlpha = 0.75
|
|
if lum > 0.5 then
|
|
glowAlpha = 0.75 * math.max(0.15, (1.0 - lum) * 2)
|
|
end
|
|
np.targetGlow:SetAlpha(glowAlpha)
|
|
end
|
|
|
|
-- Target detection
|
|
local isTarget = false
|
|
if UnitExists("target") then
|
|
if unitstr and hasValidGUID then
|
|
isTarget = UnitIsUnit(unitstr, "target")
|
|
else
|
|
isTarget = (UnitName("target") == plateName) and (not UnitIsFriend("player", "target") == not isFriendly)
|
|
end
|
|
end
|
|
np.isTarget = isTarget
|
|
|
|
-- Target nameplate always on top via FrameStrata
|
|
if isTarget and not np._isTargetStrata then
|
|
np._isTargetStrata = true
|
|
np:SetFrameStrata("HIGH")
|
|
elseif not isTarget and np._isTargetStrata then
|
|
np._isTargetStrata = false
|
|
np:SetFrameStrata(np._defaultStrata)
|
|
end
|
|
|
|
-- Target highlight
|
|
if NanamiPlates_Target then
|
|
NanamiPlates_Target.UpdateTarget(np, isTarget)
|
|
end
|
|
|
|
-- Non-target alpha (only update on change)
|
|
local wantAlpha = (UnitExists("target") and not isTarget) and (Settings.nonTargetAlpha or 0.6) or 1
|
|
if np._lastAlpha ~= wantAlpha then
|
|
np._lastAlpha = wantAlpha
|
|
np:SetAlpha(wantAlpha)
|
|
end
|
|
|
|
-- Mana/Energy/Rage bar update (inside healthBG)
|
|
local manaShowing = false
|
|
local manaH = Settings.manabarHeight or 3
|
|
if Settings.showManaBar and unitstr and hasValidGUID and UnitMana and UnitManaMax then
|
|
local manaMax = UnitManaMax(unitstr)
|
|
if manaMax and manaMax > 0 then
|
|
local manaCur = UnitMana(unitstr) or 0
|
|
local powerType = UnitPowerType and UnitPowerType(unitstr) or 0
|
|
local powerColor = Colors.power[powerType] or Colors.power[0]
|
|
|
|
if not np.manaBar:IsShown() then
|
|
np.manaBar:SetWidth(hWidth)
|
|
np.manaBar:SetHeight(manaH)
|
|
np.manaBar:Show()
|
|
np.manaSep:Show()
|
|
end
|
|
np.manaBar:SetMinMaxValues(0, manaMax)
|
|
np.manaBar:SetValue(manaCur)
|
|
np.manaBar:SetStatusBarColor(powerColor[1], powerColor[2], powerColor[3], 1)
|
|
manaShowing = true
|
|
end
|
|
end
|
|
if not manaShowing and np.manaBar:IsShown() then
|
|
np.manaBar:Hide()
|
|
np.manaSep:Hide()
|
|
end
|
|
|
|
-- Resize healthBG only when mana visibility or style changes
|
|
local needResize = (np._lastManaShowing ~= manaShowing) or (np._lastFriendly_resize ~= isFriendlyStyle)
|
|
if needResize then
|
|
np._lastManaShowing = manaShowing
|
|
np._lastFriendly_resize = isFriendlyStyle
|
|
if manaShowing then
|
|
np.healthBG:SetHeight(hHeight + manaH + 1 + BORDER_PAD * 2)
|
|
else
|
|
np.healthBG:SetHeight(hHeight + BORDER_PAD * 2)
|
|
end
|
|
np.healthBG:SetWidth(hWidth + BORDER_PAD * 2)
|
|
|
|
local cbHeight = isFriendlyStyle and (Settings.friendCastbarHeight or 6) or (Settings.castbarHeight or 10)
|
|
np.castbarBG:SetWidth(hWidth + 2)
|
|
np.castbarBG:SetHeight(cbHeight + 2)
|
|
end
|
|
|
|
local castShowing = false
|
|
local casting = nil
|
|
local castNow = GetTime()
|
|
|
|
-- Method 1: castDB by GUID (UNIT_CASTEVENT, unitstr IS the GUID from GetName(1))
|
|
local castDB = NP.castDB
|
|
if hasValidGUID and unitstr and castDB[unitstr] then
|
|
local cast = castDB[unitstr]
|
|
if cast.startTime + (cast.duration / 1000) > castNow then
|
|
casting = cast
|
|
else
|
|
castDB[unitstr] = nil
|
|
end
|
|
end
|
|
|
|
-- Method 2: UnitCastingInfo / UnitChannelInfo (SuperWoW 1.5+)
|
|
if not casting and superwow_active and hasValidGUID and unitstr then
|
|
if UnitCastingInfo then
|
|
local spell, _, _, texture, startTime, endTime = UnitCastingInfo(unitstr)
|
|
if spell then
|
|
casting = {
|
|
spell = spell,
|
|
startTime = startTime / 1000,
|
|
duration = endTime - startTime,
|
|
icon = texture
|
|
}
|
|
end
|
|
end
|
|
if not casting and UnitChannelInfo then
|
|
local spell, _, _, texture, startTime, endTime = UnitChannelInfo(unitstr)
|
|
if spell then
|
|
casting = {
|
|
spell = spell,
|
|
startTime = startTime / 1000,
|
|
duration = endTime - startTime,
|
|
icon = texture,
|
|
channel = true
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Method 3: Fallback to original Blizzard castbar visibility
|
|
if not casting and original.castbar then
|
|
local origCBVisible = original.castbar.IsVisible and original.castbar:IsVisible()
|
|
if not origCBVisible then
|
|
origCBVisible = original.castbar.IsShown and original.castbar:IsShown()
|
|
end
|
|
if origCBVisible then
|
|
local castVal = original.castbar:GetValue() or 0
|
|
local castMin, castMax = original.castbar:GetMinMaxValues()
|
|
if castMax and castMax > 0 then
|
|
np.castbar:SetMinMaxValues(castMin, castMax)
|
|
np.castbar:SetValue(castVal)
|
|
np.castbarText:SetText("")
|
|
np.castbarTimer:SetText("")
|
|
np.castbarIcon:Hide()
|
|
np.castbarBG:Show()
|
|
castShowing = true
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Render castbar from Method 1 or 2
|
|
if casting and casting.spell and not castShowing then
|
|
local start = casting.startTime
|
|
local duration = casting.duration
|
|
if castNow < start + (duration / 1000) then
|
|
np.castbar:SetMinMaxValues(0, duration)
|
|
if casting.channel then
|
|
np.castbar:SetValue(duration - (castNow - start) * 1000)
|
|
else
|
|
np.castbar:SetValue((castNow - start) * 1000)
|
|
end
|
|
np.castbarText:SetText(casting.spell)
|
|
local timeLeft = (start + (duration / 1000)) - castNow
|
|
np.castbarTimer:SetText(string_format("%.1f", timeLeft))
|
|
|
|
if casting.icon then
|
|
np.castbarIcon:SetTexture(casting.icon)
|
|
np.castbarIcon:Show()
|
|
else
|
|
np.castbarIcon:Hide()
|
|
end
|
|
|
|
np.castbarBG:Show()
|
|
castShowing = true
|
|
end
|
|
end
|
|
|
|
if not castShowing then
|
|
np.castbarBG:Hide()
|
|
np.castbarIcon:Hide()
|
|
end
|
|
|
|
-- Debuff update (throttled)
|
|
local now = GetTime()
|
|
if now - np.lastDebuffUpdate >= DEBUFF_UPDATE_INTERVAL then
|
|
np.lastDebuffUpdate = now
|
|
local numDebuffs = 0
|
|
|
|
-- Debuff anchor: below castbar if visible, else below healthBG (which now includes mana)
|
|
if castShowing then
|
|
np.debuffAnchor = np.castbarBG
|
|
else
|
|
np.debuffAnchor = np.healthBG
|
|
end
|
|
|
|
if NanamiPlates_Auras and np.debuffs then
|
|
numDebuffs = NanamiPlates_Auras:UpdateDebuffs(np, unitstr, plateName, isTarget, hasValidGUID, superwow_active)
|
|
NanamiPlates_Auras:UpdateDebuffPositions(np, numDebuffs)
|
|
end
|
|
|
|
-- Combo points
|
|
if NanamiPlates_ComboPoints then
|
|
local numPoints = NanamiPlates_ComboPoints:UpdateComboPoints(np, isTarget)
|
|
NanamiPlates_ComboPoints:UpdateComboPointPositions(np, numDebuffs)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Nameplate vertical stacking resolution
|
|
-- Engine anti-overlap is fully disabled (parent set to 1x1 like ShaguPlates),
|
|
-- so we resolve overlaps ourselves with a stable algorithm.
|
|
local stackPool = {}
|
|
local stackN = 0
|
|
local STACK_SMOOTH = 0.15
|
|
|
|
local function ResolveStacking()
|
|
if not Settings.enableStacking then
|
|
-- Stacking disabled: decay any residual offsets from when it was enabled
|
|
for frame, np in pairs(registry) do
|
|
if np._stackY and np._stackY ~= 0 then
|
|
local new = np._stackY * (1 - STACK_SMOOTH * 3)
|
|
if new > -0.5 and new < 0.5 then new = 0 end
|
|
np._stackY = new
|
|
np._appliedOffY = (Settings.nameplateYOffset or 0) + new
|
|
np.healthBG:ClearAllPoints()
|
|
np.healthBG:SetPoint("CENTER", np, "CENTER", 0, np._appliedOffY)
|
|
end
|
|
end
|
|
return
|
|
end
|
|
|
|
stackN = 0
|
|
for frame, np in pairs(registry) do
|
|
if frame:IsShown() and np:IsShown() then
|
|
local cx, cy = frame:GetCenter()
|
|
if cx and cy then
|
|
stackN = stackN + 1
|
|
if not stackPool[stackN] then stackPool[stackN] = {} end
|
|
local e = stackPool[stackN]
|
|
e.np = np
|
|
e.cx = cx
|
|
e.cy = cy
|
|
e.name = np.plateName or ""
|
|
e.stackTarget = 0
|
|
end
|
|
end
|
|
end
|
|
|
|
if stackN < 1 then return end
|
|
|
|
-- Single plate: decay any residual stacking offset
|
|
if stackN < 2 then
|
|
local np = stackPool[1].np
|
|
if np._stackY and np._stackY ~= 0 then
|
|
local new = np._stackY * (1 - STACK_SMOOTH * 3)
|
|
if new > -0.5 and new < 0.5 then new = 0 end
|
|
np._stackY = new
|
|
np._appliedOffY = (Settings.nameplateYOffset or 0) + new
|
|
np.healthBG:ClearAllPoints()
|
|
np.healthBG:SetPoint("CENTER", np, "CENTER", 0, np._appliedOffY)
|
|
end
|
|
return
|
|
end
|
|
|
|
-- Stable bubble sort: Y descending (higher on screen first), name as tiebreaker
|
|
for i = stackN, 2, -1 do
|
|
for j = 1, i - 1 do
|
|
local a = stackPool[j]
|
|
local b = stackPool[j + 1]
|
|
local ay = math_floor(a.cy)
|
|
local by = math_floor(b.cy)
|
|
if by > ay or (by == ay and b.name < a.name) then
|
|
stackPool[j] = b
|
|
stackPool[j + 1] = a
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Resolve vertical overlaps: push lower plates down when horizontally close
|
|
local minSep = (Settings.healthbarHeight or 12) + BORDER_PAD * 2
|
|
+ (Settings.nameFontSize or 9) + 6
|
|
local overlapW = (Settings.healthbarWidth or 120) * 0.8
|
|
|
|
for i = 1, stackN do
|
|
for j = i + 1, stackN do
|
|
local a = stackPool[i]
|
|
local b = stackPool[j]
|
|
local dx = a.cx - b.cx
|
|
if dx < 0 then dx = -dx end
|
|
if dx < overlapW then
|
|
local ya = a.cy + a.stackTarget
|
|
local yb = b.cy + b.stackTarget
|
|
local gap = ya - yb
|
|
if gap < minSep then
|
|
b.stackTarget = b.stackTarget - (minSep - gap)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Apply stacking offsets with smooth convergence
|
|
for i = 1, stackN do
|
|
local e = stackPool[i]
|
|
local np = e.np
|
|
|
|
local curStack = np._stackY or 0
|
|
local tarStack = e.stackTarget
|
|
local d = tarStack - curStack
|
|
local newStack
|
|
if d > -0.5 and d < 0.5 then
|
|
newStack = tarStack
|
|
else
|
|
newStack = curStack + d * STACK_SMOOTH
|
|
end
|
|
if newStack > -0.3 and newStack < 0.3 then newStack = 0 end
|
|
np._stackY = newStack
|
|
|
|
local offY = (Settings.nameplateYOffset or 0) + newStack
|
|
|
|
local pY = np._appliedOffY or (Settings.nameplateYOffset or 0)
|
|
if (offY - pY) > 0.5 or (offY - pY) < -0.5 then
|
|
np._appliedOffY = offY
|
|
np.healthBG:ClearAllPoints()
|
|
np.healthBG:SetPoint("CENTER", np, "CENTER", 0, offY)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Main OnUpdate loop
|
|
local scanThrottle = 0
|
|
local SCAN_INTERVAL = 0.1
|
|
|
|
local function OnUpdate()
|
|
local now = GetTime()
|
|
|
|
-- Rebuild quest mob cache periodically
|
|
if now - questCacheTimer >= QUEST_CACHE_INTERVAL then
|
|
questCacheTimer = now
|
|
RebuildQuestMobCache()
|
|
end
|
|
|
|
-- Scan for new nameplates
|
|
if now - scanThrottle >= SCAN_INTERVAL then
|
|
scanThrottle = now
|
|
Scanner.ScanForNewNameplates(registry, HandleNamePlate)
|
|
end
|
|
|
|
-- Update all active nameplates
|
|
for frame, np in pairs(registry) do
|
|
if frame:IsShown() then
|
|
UpdateNamePlate(frame)
|
|
end
|
|
end
|
|
|
|
-- Resolve stacking overlaps and smooth position jitter
|
|
ResolveStacking()
|
|
|
|
-- Cleanup debuff timers
|
|
if NanamiPlates_Auras then
|
|
NanamiPlates_Auras:CleanupTimers()
|
|
end
|
|
end
|
|
|
|
-- Event handler
|
|
local function OnEvent()
|
|
local evnt = event
|
|
|
|
if evnt == "ADDON_LOADED" and arg1 == "Nanami-Plates" then
|
|
NP:LoadSettings()
|
|
NP.Print("Loaded.")
|
|
|
|
elseif evnt == "PLAYER_ENTERING_WORLD" then
|
|
Scanner.Reset()
|
|
if NanamiPlates_Castbar.ClearCastData then
|
|
NanamiPlates_Castbar.ClearCastData()
|
|
end
|
|
NP.playerClassCache = {}
|
|
|
|
if SpellDB and SpellDB.ScanSpellbook then
|
|
SpellDB:ScanSpellbook()
|
|
end
|
|
|
|
NP:DetectTankSpec()
|
|
|
|
if NanamiPlates_Threat then
|
|
NanamiPlates_Threat.BroadcastTankMode(true)
|
|
end
|
|
|
|
elseif evnt == "CHARACTER_POINTS_CHANGED" then
|
|
NP:DetectTankSpec()
|
|
|
|
elseif evnt == "UNIT_CASTEVENT" then
|
|
if superwow_active and arg1 then
|
|
NanamiPlates_Castbar.HandleUnitCastEvent(arg1, arg2, arg3, arg4, arg5)
|
|
end
|
|
|
|
elseif evnt == "SPELLCAST_STOP" then
|
|
if SpellDB then SpellDB:PersistPending() end
|
|
|
|
elseif evnt == "CHAT_MSG_SPELL_FAILED_LOCALPLAYER" then
|
|
if SpellDB then SpellDB:RemovePending() end
|
|
|
|
elseif evnt == "QUEST_LOG_UPDATE" then
|
|
questCacheTimer = 0
|
|
|
|
elseif evnt == "PLAYER_TARGET_CHANGED" then
|
|
-- Force debuff update on all plates
|
|
for frame, np in pairs(registry) do
|
|
if frame:IsShown() then
|
|
np.lastDebuffUpdate = 0
|
|
end
|
|
end
|
|
|
|
elseif evnt == "PLAYER_REGEN_DISABLED" then
|
|
playerInCombat = true
|
|
|
|
elseif evnt == "PLAYER_REGEN_ENABLED" then
|
|
playerInCombat = false
|
|
NP.playerClassCache = {}
|
|
if SpellDB then
|
|
for k in pairs(SpellDB.objects) do
|
|
SpellDB.objects[k] = nil
|
|
end
|
|
end
|
|
|
|
elseif evnt == "PARTY_MEMBERS_CHANGED" or evnt == "RAID_ROSTER_UPDATE" then
|
|
NP.playerClassCache = {}
|
|
if NanamiPlates_Threat then
|
|
NanamiPlates_Threat.BroadcastTankMode(true)
|
|
end
|
|
|
|
elseif NP.SPELL_EVENTS[evnt] then
|
|
if NanamiPlates_CombatLog then
|
|
NanamiPlates_CombatLog.HandleSpellEvent(evnt, arg1)
|
|
end
|
|
|
|
elseif NP.COMBAT_EVENTS[evnt] then
|
|
if NanamiPlates_CombatLog then
|
|
NanamiPlates_CombatLog.HandleCombatEvent(evnt, arg1)
|
|
end
|
|
end
|
|
end
|
|
|
|
NP.EventFrame:SetScript("OnEvent", OnEvent)
|
|
NP.EventFrame:SetScript("OnUpdate", OnUpdate)
|