第一次发版v0.1.0

This commit is contained in:
rucky
2026-03-20 10:20:05 +08:00
commit 017e37a365
17 changed files with 6166 additions and 0 deletions

554
Auras.lua Normal file
View File

@@ -0,0 +1,554 @@
NanamiPlates_Auras = {}
local NP = NanamiPlates
local Settings = NP.Settings
local SpellDB = NanamiPlates_SpellDB
local pairs = pairs
local ipairs = ipairs
local string_find = string.find
local string_format = string.format
local string_gsub = string.gsub
local math_floor = math.floor
local GetTime = GetTime
local UnitDebuff = UnitDebuff
local UnitGUID = UnitGUID
local UnitLevel = UnitLevel
local UnitName = UnitName
local UnitExists = UnitExists
local CreateFrame = CreateFrame
local _, playerClass = UnitClass("player")
playerClass = playerClass or ""
local MAX_DEBUFFS = 16
local JUDGEMENT_EFFECTS = {
"Judgement of Wisdom", "Judgement of Light", "Judgement of the Crusader",
"Judgement of Justice", "Judgement"
}
NanamiPlates_Auras.timers = {}
local debuffTimers = NanamiPlates_Auras.timers
local function FormatTime(remaining)
if not remaining or remaining < 0 then return "", 1, 1, 1, 1 end
if remaining > 3600 then
return math_floor(remaining / 3600 + 0.5) .. "h", 0.5, 0.5, 0.5, 1
elseif remaining > 60 then
return math_floor(remaining / 60 + 0.5) .. "m", 0.5, 0.5, 0.5, 1
elseif remaining > 10 then
return math_floor(remaining + 0.5) .. "", 0.7, 0.7, 0.7, 1
elseif remaining > 5 then
return math_floor(remaining + 0.5) .. "", 1, 1, 0, 1
elseif remaining > 0 then
return string_format("%.1f", remaining), 1, 0, 0, 1
end
return "", 1, 1, 1, 1
end
NanamiPlates_Auras.FormatTime = FormatTime
local function GetSpellData(unit, name, effect, level)
if not SpellDB or not SpellDB.objects then return nil end
local dataUnit = unit and SpellDB:FindEffectData(unit, level or 0, effect)
local dataName = name and SpellDB:FindEffectData(name, level or 0, effect)
if dataUnit and dataName then
return (dataUnit.start or 0) >= (dataName.start or 0) and dataUnit or dataName
end
return dataUnit or dataName
end
local function DebuffOnUpdate()
local now = GetTime()
if (this.tick or 0) > now then return else this.tick = now + 0.1 end
if not this:IsShown() then return end
if not this.expirationTime or this.expirationTime <= 0 then
if this.cd then this.cd:SetText("") end
return
end
local timeLeft = this.expirationTime - now
if timeLeft > 0 then
local text, r, g, b, a = FormatTime(timeLeft)
if this.cd then
this.cd:SetText(text)
if r then this.cd:SetTextColor(r, g, b, a or 1) end
this.cd:SetAlpha(1)
end
else
if this.cd then
this.cd:SetText("")
this.cd:SetAlpha(0)
end
this.expirationTime = 0
end
end
function NanamiPlates_Auras:CreateDebuffFrames(nameplate)
nameplate.debuffs = {}
local plateName = nameplate:GetName() or "UnknownPlate"
local size = Settings.debuffIconSize or 20
for i = 1, MAX_DEBUFFS do
local debuff = CreateFrame("Frame", plateName .. "Debuff" .. i, nameplate)
debuff:SetWidth(size)
debuff:SetHeight(size)
debuff:SetFrameLevel(nameplate.health:GetFrameLevel() + 5)
debuff:EnableMouse(false)
debuff.icon = debuff:CreateTexture(nil, "ARTWORK")
debuff.icon:SetAllPoints()
debuff.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
debuff.icon:SetDrawLayer("ARTWORK")
debuff.border = debuff:CreateTexture(nil, "BACKGROUND")
debuff.border:SetTexture(0, 0, 0, 1)
debuff.border:SetPoint("TOPLEFT", debuff, "TOPLEFT", -1, 1)
debuff.border:SetPoint("BOTTOMRIGHT", debuff, "BOTTOMRIGHT", 1, -1)
debuff.border:SetDrawLayer("BACKGROUND")
debuff.cdframe = CreateFrame("Frame", nil, debuff)
debuff.cdframe:SetAllPoints(debuff)
debuff.cdframe:SetFrameLevel(debuff:GetFrameLevel() + 2)
debuff.cdframe:EnableMouse(false)
debuff.cd = debuff.cdframe:CreateFontString(nil, "OVERLAY")
debuff.cd:SetFont(NP.GetFont(), 10, NP.GetFontOutline())
debuff.cd:SetPoint("BOTTOM", debuff, "BOTTOM", 0, -1)
debuff.cd:SetTextColor(1, 1, 0, 1)
debuff.cd:SetText("")
debuff.cd:SetDrawLayer("OVERLAY", 7)
debuff.count = debuff.cdframe:CreateFontString(nil, "OVERLAY")
debuff.count:SetFont(NP.GetFont(), 9, NP.GetFontOutline())
debuff.count:SetPoint("TOPRIGHT", debuff, "TOPRIGHT", 2, 2)
debuff.count:SetTextColor(1, 1, 1, 1)
debuff.count:SetText("")
debuff.count:SetDrawLayer("OVERLAY", 7)
debuff:SetScript("OnUpdate", DebuffOnUpdate)
debuff:Hide()
nameplate.debuffs[i] = debuff
end
end
function NanamiPlates_Auras:UpdateDebuffs(nameplate, unitstr, plateName, isTarget, hasValidGUID, superwow_active)
local size = Settings.debuffIconSize or 20
for i = 1, MAX_DEBUFFS do
local debuff = nameplate.debuffs[i]
debuff:SetWidth(size)
debuff:SetHeight(size)
local cdFontSize = math_floor(size * 0.5 + 0.5)
local countFontSize = math_floor(size * 0.4 + 0.5)
if cdFontSize < 7 then cdFontSize = 7 end
if countFontSize < 6 then countFontSize = 6 end
debuff.cd:SetFont(NP.GetFont(), cdFontSize, NP.GetFontOutline())
debuff.count:SetFont(NP.GetFont(), countFontSize, NP.GetFontOutline())
debuff:Hide()
debuff.count:SetText("")
debuff.expirationTime = 0
end
local now = GetTime()
local claimedMyDebuffs = {}
local effectiveUnit = (isTarget) and "target" or (superwow_active and hasValidGUID and unitstr) or nil
if not effectiveUnit and not plateName then return 0 end
local scanUnit = effectiveUnit
if not scanUnit and plateName and UnitExists("target") and UnitName("target") == plateName then
scanUnit = "target"
end
if not scanUnit then return 0 end
-- Collect debuffs
local collectedDebuffs = {}
local ownerBoundCounts = {}
local ownerBoundFirst = {}
for i = 1, 40 do
local texture, stacks = UnitDebuff(scanUnit, i)
if not texture then break end
local effect = SpellDB and SpellDB:ScanDebuff(scanUnit, i)
if (not effect or effect == "") and SpellDB and SpellDB.textureToSpell then
effect = SpellDB.textureToSpell[texture]
end
-- If effect is non-empty but not in DEBUFFS (e.g. Chinese name), try fallbacks
if SpellDB and effect and effect ~= "" and SpellDB.DEBUFFS and not SpellDB.DEBUFFS[effect] then
-- Try texture-based lookup (covers spellbook-scanned entries)
local texEffect = SpellDB.textureToSpell and SpellDB.textureToSpell[texture]
if texEffect and SpellDB.DEBUFFS[texEffect] then
-- Learn this locale mapping for future
if SpellDB.LearnLocale then
SpellDB:LearnLocale(effect, texEffect)
end
effect = texEffect
elseif SpellDB.WARLOCK_DOT_TEXTURES and SpellDB.WARLOCK_DOT_TEXTURES[texture] then
effect = SpellDB.WARLOCK_DOT_TEXTURES[texture]
elseif SpellDB.WARLOCK_CURSE_TEXTURES and SpellDB.WARLOCK_CURSE_TEXTURES[texture] then
effect = SpellDB.WARLOCK_CURSE_TEXTURES[texture]
end
end
local isOwnerBound = effect and SpellDB and SpellDB.OWNER_BOUND_DEBUFFS and SpellDB.OWNER_BOUND_DEBUFFS[effect]
if isOwnerBound then
ownerBoundCounts[effect] = (ownerBoundCounts[effect] or 0) + 1
if not ownerBoundFirst[effect] then
ownerBoundFirst[effect] = { index = i, texture = texture, stacks = stacks }
end
end
table.insert(collectedDebuffs, {
index = i, texture = texture, stacks = stacks,
effect = effect, isOwnerBound = isOwnerBound
})
end
-- Display debuffs
local debuffIndex = 1
local displayedOwnerBound = {}
local unitlevel = (scanUnit == "target") and UnitLevel("target") or (unitstr and UnitLevel(unitstr)) or 0
for _, debuffData in ipairs(collectedDebuffs) do
if debuffIndex > MAX_DEBUFFS then break end
local effect = debuffData.effect
local texture = debuffData.texture
local stacks = debuffData.stacks
local isOwnerBound = debuffData.isOwnerBound
local isRoguePoison = false
if playerClass == "ROGUE" then
if effect and SpellDB.ROGUE_POISONS and SpellDB.ROGUE_POISONS[effect] then
isRoguePoison = true
elseif texture and SpellDB.ROGUE_POISON_TEXTURES and SpellDB.ROGUE_POISON_TEXTURES[texture] then
isRoguePoison = true
if not effect or effect == "" then effect = SpellDB.ROGUE_POISON_TEXTURES[texture] end
end
end
local isHunterTrap = false
if playerClass == "HUNTER" then
if effect and SpellDB.HUNTER_TRAPS and SpellDB.HUNTER_TRAPS[effect] then
isHunterTrap = true
elseif texture and SpellDB.HUNTER_TRAP_TEXTURES and SpellDB.HUNTER_TRAP_TEXTURES[texture] then
isHunterTrap = true
effect = SpellDB.HUNTER_TRAP_TEXTURES[texture]
end
end
local isHunterSting = false
if playerClass == "HUNTER" then
if effect and SpellDB.HUNTER_STINGS and SpellDB.HUNTER_STINGS[effect] then
isHunterSting = true
elseif texture and SpellDB.HUNTER_STING_TEXTURES and SpellDB.HUNTER_STING_TEXTURES[texture] then
isHunterSting = true
effect = SpellDB.HUNTER_STING_TEXTURES[texture]
end
end
local isWarlockCurse = false
if playerClass == "WARLOCK" then
if effect and SpellDB.WARLOCK_CURSES and SpellDB.WARLOCK_CURSES[effect] then
isWarlockCurse = true
elseif texture and SpellDB.WARLOCK_CURSE_TEXTURES and SpellDB.WARLOCK_CURSE_TEXTURES[texture] then
isWarlockCurse = true
effect = SpellDB.WARLOCK_CURSE_TEXTURES[texture]
end
end
local isWarlockDot = false
if playerClass == "WARLOCK" and not isWarlockCurse then
if texture and SpellDB.WARLOCK_DOT_TEXTURES and SpellDB.WARLOCK_DOT_TEXTURES[texture] then
local dotName = SpellDB.WARLOCK_DOT_TEXTURES[texture]
if not effect or effect == "" or not SpellDB.DEBUFFS[effect] then
effect = dotName
end
isWarlockDot = true
elseif effect and (effect == "Corruption" or effect == "Siphon Life" or effect == "Immolate" or effect == "Dark Harvest") then
isWarlockDot = true
end
end
if isWarlockDot or isWarlockCurse then
isOwnerBound = effect and SpellDB.OWNER_BOUND_DEBUFFS and SpellDB.OWNER_BOUND_DEBUFFS[effect]
end
local isMyDebuff = false
local duration, timeleft = nil, nil
if isRoguePoison or isHunterTrap or isHunterSting or isWarlockCurse or isWarlockDot then
isMyDebuff = true
end
if effect and effect ~= "" then
local data = GetSpellData(unitstr, plateName, effect, unitlevel)
if data and data.start and data.duration then
if data.start + data.duration > now then
duration = data.duration
timeleft = data.duration + data.start - now
if data.isOwn == true and not claimedMyDebuffs[effect] then
isMyDebuff = true
claimedMyDebuffs[effect] = true
end
end
end
if playerClass == "PALADIN" and (string_find(effect, "Judgement of ") or string_find(effect, "Seal of ") or effect == "Crusader Strike" or effect == "Hammer of Justice" or effect == "Repentance") then
isMyDebuff = true
claimedMyDebuffs[effect] = true
end
if not timeleft then
local dbDuration = SpellDB:GetDuration(effect, 0)
if dbDuration > 0 then
SpellDB:AddEffect(plateName, unitlevel, effect, dbDuration, isMyDebuff)
if unitstr and unitstr ~= plateName then
SpellDB:AddEffect(unitstr, unitlevel, effect, dbDuration, isMyDebuff)
end
end
end
end
if effect and effect ~= "" and not duration then
duration = SpellDB:GetDuration(effect, 0)
end
-- Display logic
if isOwnerBound then
if not displayedOwnerBound[effect] then
local ownerCheckUnit = unitstr or plateName
local isMyOwnerBound = isMyDebuff
if not isMyOwnerBound and SpellDB.IsOwnerBoundDebuffMine then
isMyOwnerBound = SpellDB:IsOwnerBoundDebuffMine(ownerCheckUnit, effect)
if not isMyOwnerBound and plateName and plateName ~= ownerCheckUnit then
isMyOwnerBound = SpellDB:IsOwnerBoundDebuffMine(plateName, effect)
end
end
local shouldShowOwnerBound = isMyOwnerBound or not Settings.showOnlyMyDebuffs
if shouldShowOwnerBound then
displayedOwnerBound[effect] = true
local debuff = nameplate.debuffs[debuffIndex]
debuff.icon:SetTexture(texture)
local instanceCount = ownerBoundCounts[effect] or 1
if instanceCount > 1 then
debuff.count:SetText(instanceCount)
debuff.count:SetTextColor(0.3, 0.7, 1, 1)
elseif stacks and stacks > 1 then
debuff.count:SetText(stacks)
else
debuff.count:SetText("")
end
local debuffKey = (unitstr or plateName) .. "_" .. effect
local displayTimeLeft = nil
if timeleft and timeleft > 0 then
displayTimeLeft = timeleft
debuffTimers[debuffKey] = { startTime = now - (duration - timeleft), duration = duration, lastSeen = now }
else
local fallbackDuration = duration or SpellDB:GetDuration(effect, 0)
if fallbackDuration <= 0 then fallbackDuration = 30 end
if not debuffTimers[debuffKey] then
debuffTimers[debuffKey] = { startTime = now, duration = fallbackDuration, lastSeen = now }
end
local cached = debuffTimers[debuffKey]
cached.lastSeen = now
if (now - cached.startTime) > cached.duration then
cached.startTime = now
cached.duration = fallbackDuration
end
displayTimeLeft = cached.duration - (now - cached.startTime)
end
if Settings.showDebuffTimers and displayTimeLeft and displayTimeLeft > 0 then
debuff.expirationTime = now + displayTimeLeft
local text, r, g, b, a = FormatTime(displayTimeLeft)
debuff.cd:SetText(text)
if r then debuff.cd:SetTextColor(r, g, b, a) end
debuff.cdframe:Show()
else
debuff.expirationTime = 0
debuff.cd:SetText("")
end
debuff:Show()
debuffIndex = debuffIndex + 1
end
end
else
local uniqueClass = effect and SpellDB and SpellDB.SHARED_DEBUFFS and SpellDB.SHARED_DEBUFFS[effect]
local isUnique = uniqueClass and (uniqueClass == true or uniqueClass == playerClass)
local shouldDisplay = true
if Settings.showOnlyMyDebuffs and not isMyDebuff and not isUnique and not isOwnerBound and not isHunterTrap then
shouldDisplay = false
end
if shouldDisplay then
local debuff = nameplate.debuffs[debuffIndex]
debuff.icon:SetTexture(texture)
debuff.count:SetText((stacks and stacks > 1) and stacks or "")
local debuffKey = (unitstr or plateName) .. "_" .. (effect or texture)
local displayTimeLeft = nil
if timeleft and timeleft > 0 then
displayTimeLeft = timeleft
debuffTimers[debuffKey] = { startTime = now - (duration - timeleft), duration = duration, lastSeen = now }
else
local dbDur = (effect and effect ~= "") and SpellDB:GetDuration(effect, 0) or 0
local fallbackDuration = (duration and duration > 0 and duration) or (dbDur > 0 and dbDur) or 12
if not debuffTimers[debuffKey] then
debuffTimers[debuffKey] = { startTime = now, duration = fallbackDuration, lastStacks = stacks or 0 }
end
local cached = debuffTimers[debuffKey]
cached.lastSeen = now
local stacksChanged = stacks and cached.lastStacks and stacks ~= cached.lastStacks
if fallbackDuration > 1 and (cached.duration ~= fallbackDuration or (now - cached.startTime) > cached.duration or stacksChanged) then
cached.duration = fallbackDuration
cached.startTime = now
end
cached.lastStacks = stacks or 0
displayTimeLeft = cached.duration - (now - cached.startTime)
end
if Settings.showDebuffTimers and displayTimeLeft and displayTimeLeft > 0 then
debuff.expirationTime = now + displayTimeLeft
local text, r, g, b, a = FormatTime(displayTimeLeft)
debuff.cd:SetText(text)
if r then debuff.cd:SetTextColor(r, g, b, a) end
debuff.cdframe:Show()
else
debuff.expirationTime = 0
debuff.cd:SetText("")
end
debuff:Show()
debuffIndex = debuffIndex + 1
end
end
end
return debuffIndex - 1
end
function NanamiPlates_Auras:UpdateDebuffPositions(nameplate, numDebuffs)
if numDebuffs <= 0 then return end
local anchor = nameplate.debuffAnchor or nameplate.healthBG
for i = 1, numDebuffs do
local debuff = nameplate.debuffs[i]
if debuff then
debuff:ClearAllPoints()
if i == 1 then
debuff:SetPoint("TOPLEFT", anchor, "BOTTOMLEFT", 0, -2)
else
debuff:SetPoint("LEFT", nameplate.debuffs[i - 1], "RIGHT", 1, 0)
end
end
end
end
local lastDebuffCleanup = 0
local lastOwnerBoundCleanup = 0
function NanamiPlates_Auras:CleanupTimers()
local now = GetTime()
if now - lastDebuffCleanup > 3 then
lastDebuffCleanup = now
for key, data in pairs(self.timers) do
local expired = false
if data.lastSeen and (now - data.lastSeen > 5) then
expired = true
end
if data.startTime and data.duration and (now - data.startTime > data.duration + 10) then
expired = true
end
if expired then
self.timers[key] = nil
end
end
end
if now - lastOwnerBoundCleanup > 10 then
lastOwnerBoundCleanup = now
if SpellDB and SpellDB.CleanupOwnerBoundCache then
SpellDB:CleanupOwnerBoundCache()
end
end
end
-- Paladin judgement refresh
function NanamiPlates_Auras:RefreshJudgementsOnTarget()
if playerClass ~= "PALADIN" then return end
if not UnitExists("target") then return end
if not SpellDB or not SpellDB.objects then return end
local name = UnitName("target")
local level = UnitLevel("target") or 0
local guid = UnitGUID and UnitGUID("target")
local hasNameData = name and SpellDB.objects[name]
local hasGuidData = guid and SpellDB.objects[guid]
if not hasNameData and not hasGuidData then return end
for _, effect in ipairs(JUDGEMENT_EFFECTS) do
local found = false
local dur = SpellDB:GetDuration(effect, 0) or 10
if hasNameData then
for lvl, effects in pairs(SpellDB.objects[name]) do
if effects[effect] then
effects[effect].start = GetTime()
effects[effect].duration = dur
found = true
break
end
end
end
if hasGuidData and not found then
for lvl, effects in pairs(SpellDB.objects[guid]) do
if effects[effect] then
effects[effect].start = GetTime()
effects[effect].duration = dur
found = true
break
end
end
end
if found then
debuffTimers[name .. "_" .. effect] = nil
if guid then debuffTimers[guid .. "_" .. effect] = nil end
end
end
end
function NanamiPlates_Auras:SealHandler(attacker, victim)
if playerClass ~= "PALADIN" then return end
local isOwn = (attacker == "You" or attacker == UnitName("player"))
if not isOwn then return end
self:RefreshJudgementsOnTarget()
end
function NanamiPlates_Auras:HolyStrikeHandler(msg)
if not msg or playerClass ~= "PALADIN" then return end
local holyStrike = string_find(string.sub(msg, 6, 17), "Holy Strike")
if not holyStrike then return end
if not string_find(msg, "%d+") then return end
self:RefreshJudgementsOnTarget()
end
NanamiPlates.Auras = NanamiPlates_Auras

111
Castbar.lua Normal file
View File

@@ -0,0 +1,111 @@
NanamiPlates_Castbar = {}
local GetTime = GetTime
local UnitGUID = UnitGUID
local UnitName = UnitName
local superwow_active = (SpellInfo ~= nil) or (UnitGUID ~= nil) or (SUPERWOW_VERSION ~= nil)
local castDB
local function InitReferences()
castDB = NanamiPlates.castDB
end
local function HandleUnitCastEvent(guid, target, eventType, spellId, timer)
if not castDB then
castDB = NanamiPlates.castDB
end
if not castDB then return false end
local SpellDB = NanamiPlates_SpellDB
if eventType == "START" or eventType == "CAST" or eventType == "CHANNEL" then
local spell, icon
if SpellInfo and spellId then
spell, _, icon = SpellInfo(spellId)
end
spell = spell or "Casting"
icon = icon or "Interface\\Icons\\INV_Misc_QuestionMark"
if SpellDB and eventType == "CAST" then
local effectTarget = target
local isOwn = (guid == (UnitGUID and UnitGUID("player")))
if (not effectTarget or effectTarget == "") and isOwn then
if UnitExists("target") then
effectTarget = UnitGUID and UnitGUID("target") or UnitName("target")
end
end
if effectTarget and effectTarget ~= "" then
local duration = SpellDB:GetDuration(spell, 0)
if duration and duration > 0 then
SpellDB:RefreshEffect(effectTarget, 0, spell, duration, isOwn)
if NanamiPlates_Auras and NanamiPlates_Auras.timers then
NanamiPlates_Auras.timers[effectTarget .. "_" .. spell] = nil
end
if isOwn then
local targetName = UnitExists("target") and UnitName("target")
if targetName and targetName ~= effectTarget then
SpellDB:RefreshEffect(targetName, 0, spell, duration, true)
NanamiPlates_Auras.timers[targetName .. "_" .. spell] = nil
end
if SpellDB.OWNER_BOUND_DEBUFFS and SpellDB.OWNER_BOUND_DEBUFFS[spell] then
SpellDB:TrackOwnerBoundDebuff(effectTarget, spell, duration)
if targetName and targetName ~= effectTarget then
SpellDB:TrackOwnerBoundDebuff(targetName, spell, duration)
end
end
end
end
end
end
if eventType == "CAST" then
if castDB[guid] and castDB[guid].spell ~= spell then
return true
end
end
castDB[guid] = {
spell = spell,
startTime = GetTime(),
duration = timer or 2000,
icon = icon,
channel = (eventType == "CHANNEL")
}
elseif eventType == "FAIL" then
if castDB[guid] then
castDB[guid] = nil
end
end
return false
end
local function ClearCastData()
if not castDB then castDB = NanamiPlates.castDB end
if castDB then
for k in pairs(castDB) do castDB[k] = nil end
end
end
local function GetCast(guid)
if not castDB then castDB = NanamiPlates.castDB end
return castDB and castDB[guid]
end
local function RemoveCast(guid)
if not castDB then castDB = NanamiPlates.castDB end
if castDB and castDB[guid] then castDB[guid] = nil end
end
NanamiPlates_Castbar = {
InitReferences = InitReferences,
HandleUnitCastEvent = HandleUnitCastEvent,
ClearCastData = ClearCastData,
GetCast = GetCast,
RemoveCast = RemoveCast,
}
NanamiPlates.Castbar = NanamiPlates_Castbar

293
CombatLog.lua Normal file
View File

@@ -0,0 +1,293 @@
NanamiPlates_CombatLog = {}
local NP = NanamiPlates
local SpellDB = NanamiPlates_SpellDB
local Auras = NanamiPlates_Auras
local string_gsub = string.gsub
local string_gfind = string.gfind
local string_find = string.find
local string_sub = string.sub
local GetTime = GetTime
local UnitExists = UnitExists
local UnitName = UnitName
local UnitGUID = UnitGUID
local superwow_active = NP.superwow_active
local castTracker
local recentMeleeHits
local function InitReferences()
castTracker = NP.castTracker
recentMeleeHits = NP.recentMeleeHits
end
local function cmatch(str, pattern)
if not str or not pattern then return nil end
local pat = string_gsub(pattern, "%%%d?%$?s", "(.+)")
pat = string_gsub(pat, "%%%d?%$?d", "(%d+)")
for a, b, c, d in string_gfind(str, pat) do
return a, b, c, d
end
return nil
end
local castIcons = {
["Fireball"] = "Interface\\Icons\\Spell_Fire_FlameBolt",
["Frostbolt"] = "Interface\\Icons\\Spell_Frost_FrostBolt02",
["Shadow Bolt"] = "Interface\\Icons\\Spell_Shadow_ShadowBolt",
["Greater Heal"] = "Interface\\Icons\\Spell_Holy_GreaterHeal",
["Flash Heal"] = "Interface\\Icons\\Spell_Holy_FlashHeal",
["Lightning Bolt"] = "Interface\\Icons\\Spell_Nature_Lightning",
["Chain Lightning"] = "Interface\\Icons\\Spell_Nature_ChainLightning",
["Healing Wave"] = "Interface\\Icons\\Spell_Nature_MagicImmunity",
["Fear"] = "Interface\\Icons\\Spell_Shadow_Possession",
["Polymorph"] = "Interface\\Icons\\Spell_Nature_Polymorph",
["Smite"] = "Interface\\Icons\\Spell_Holy_HolySmite",
["Mind Blast"] = "Interface\\Icons\\Spell_Shadow_UnholyFrenzy",
["Holy Light"] = "Interface\\Icons\\Spell_Holy_HolyLight",
["Starfire"] = "Interface\\Icons\\Spell_Arcane_StarFire",
["Wrath"] = "Interface\\Icons\\Spell_Nature_AbolishMagic",
["Entangling Roots"] = "Interface\\Icons\\Spell_Nature_StrangleVines",
["Moonfire"] = "Interface\\Icons\\Spell_Nature_StarFall",
["Regrowth"] = "Interface\\Icons\\Spell_Nature_ResistNature",
["Rejuvenation"] = "Interface\\Icons\\Spell_Nature_Rejuvenation",
}
local function ParseCastStart(msg)
if not msg then return end
if not castTracker then castTracker = NP.castTracker end
if not castTracker then return end
local unit, spell = nil, nil
for u, s in string_gfind(msg, "(.+) begins to cast (.+)%.") do
unit, spell = u, s
end
if not unit then
for u, s in string_gfind(msg, "(.+) begins to perform (.+)%.") do
unit, spell = u, s
end
end
if unit and spell then
if not castTracker[unit] then castTracker[unit] = {} end
table.insert(castTracker[unit], {
spell = spell,
startTime = GetTime(),
duration = 2000,
icon = castIcons[spell],
})
end
local interruptedUnit = nil
for u in string_gfind(msg, "(.+)'s .+ is interrupted%.") do interruptedUnit = u end
if not interruptedUnit then
for u in string_gfind(msg, "(.+)'s .+ fails%.") do interruptedUnit = u end
end
if interruptedUnit and castTracker[interruptedUnit] then
table.remove(castTracker[interruptedUnit], 1)
end
end
local function ParseAttackHit(msg)
if not msg then return end
local attacker, victim = nil, nil
if string_sub(msg, 1, 8) == "You hit " then
local forPos = string_find(msg, " for ")
if forPos then
victim = string_sub(msg, 9, forPos - 1)
attacker = "You"
end
elseif string_sub(msg, 1, 9) == "You crit " then
local forPos = string_find(msg, " for ")
if forPos then
victim = string_sub(msg, 10, forPos - 1)
attacker = "You"
end
end
if attacker == "You" and victim and Auras then
Auras:SealHandler(attacker, victim)
end
if not recentMeleeHits then recentMeleeHits = NP.recentMeleeHits end
if not recentMeleeHits then return end
if attacker == "You" and victim then
recentMeleeHits[victim] = GetTime()
if superwow_active and UnitExists("target") and UnitName("target") == victim then
local guid = UnitGUID and UnitGUID("target")
if guid then recentMeleeHits[guid] = GetTime() end
end
end
if not victim then
for a, v in string_gfind(msg, "(.+) hits (.-) for %d+%.") do
attacker, victim = a, v
break
end
end
if not victim then
for a, v in string_gfind(msg, "(.+) crits (.-) for %d+%.") do
attacker, victim = a, v
break
end
end
if attacker == "You" and victim and recentMeleeHits then
recentMeleeHits[victim] = GetTime()
if superwow_active and UnitExists("target") and UnitName("target") == victim then
local guid = UnitGUID and UnitGUID("target")
if guid then recentMeleeHits[guid] = GetTime() end
end
end
if attacker and victim and Auras then
Auras:SealHandler(attacker, victim)
end
end
-- Handle spell-related combat log events
function NanamiPlates_CombatLog.HandleSpellEvent(evnt, msg)
if not msg then return end
InitReferences()
if evnt == "CHAT_MSG_SPELL_AURA_GONE_OTHER" or evnt == "CHAT_MSG_SPELL_AURA_GONE_SELF" then
local target, effect
for t, e in string_gfind(msg, "(.+) is no longer afflicted by (.+)%.") do
target, effect = t, e
end
if not target then
for e in string_gfind(msg, "(.+) fades from .+%.") do effect = e end
end
if effect and SpellDB and SpellDB.objects then
for unit, levels in pairs(SpellDB.objects) do
for lvl, effects in pairs(levels) do
if effects[effect] then effects[effect] = nil end
end
end
end
return
end
if evnt == "CHAT_MSG_SPELL_FAILED_LOCALPLAYER" then
if SpellDB then SpellDB:RemovePending() end
return
end
-- Check for pending spell resolution
if SpellDB and SpellDB.pending and SpellDB.pending[3] then
local effect = SpellDB.pending[3]
-- Check if spell was resisted/missed/etc
local removePatterns = NP.REMOVE_PENDING_PATTERNS
if removePatterns then
for _, pattern in ipairs(removePatterns) do
if cmatch(msg, pattern) then
SpellDB:RemovePending()
return
end
end
end
-- Check for spell hit/application
if string_find(msg, effect) then
local affTarget = nil
for t in string_gfind(msg, "(.+) is afflicted by " .. effect) do
affTarget = t
end
if affTarget or string_find(msg, "Your " .. effect) then
SpellDB:PersistPending(effect)
if SpellDB.OWNER_BOUND_DEBUFFS and SpellDB.OWNER_BOUND_DEBUFFS[effect] then
local targetName = affTarget or (UnitExists("target") and UnitName("target"))
if targetName then
SpellDB:TrackOwnerBoundDebuff(targetName, effect)
if superwow_active and UnitExists("target") and UnitName("target") == targetName then
local guid = UnitGUID and UnitGUID("target")
if guid then SpellDB:TrackOwnerBoundDebuff(guid, effect) end
end
end
end
end
end
end
-- Fallback: track debuffs via "afflicted by" messages using recentCasts.
-- Handles instant-cast DOTs (e.g. Corruption) whose pending was overwritten
-- by a subsequent cast before the combat log confirmed application.
do
local affTarget, affEffect
for t, e in string_gfind(msg, "(.+) is afflicted by (.+)%.") do
affTarget, affEffect = t, e
end
if affEffect and SpellDB and SpellDB.recentCasts then
if SpellDB.WARLOCK_CURSES and SpellDB.WARLOCK_CURSES[affEffect] then
local hasMalediction = SpellDB.HasMalediction and SpellDB:HasMalediction()
for otherCurse, _ in pairs(SpellDB.WARLOCK_CURSES) do
if otherCurse ~= affEffect then
local otherRecent = SpellDB.recentCasts[otherCurse]
local thisRecent = SpellDB.recentCasts[affEffect]
if otherRecent and thisRecent and otherRecent.time > thisRecent.time then
if hasMalediction and SpellDB:CanCursesCoexist(affEffect, otherCurse) then
-- skip: these two curses can coexist
else
return
end
end
end
end
end
local recent = SpellDB.recentCasts[affEffect]
if recent and (GetTime() - recent.time) < 4 then
SpellDB:RefreshEffect(affTarget, 0, affEffect, recent.duration, true)
if superwow_active and UnitExists("target") and UnitName("target") == affTarget then
local guid = UnitGUID and UnitGUID("target")
if guid then
SpellDB:RefreshEffect(guid, 0, affEffect, recent.duration, true)
end
end
if SpellDB.OWNER_BOUND_DEBUFFS and SpellDB.OWNER_BOUND_DEBUFFS[affEffect] then
SpellDB:TrackOwnerBoundDebuff(affTarget, affEffect, recent.duration)
if superwow_active and UnitExists("target") and UnitName("target") == affTarget then
local guid = UnitGUID and UnitGUID("target")
if guid then SpellDB:TrackOwnerBoundDebuff(guid, affEffect, recent.duration) end
end
end
if Auras and Auras.timers then
Auras.timers[affTarget .. "_" .. affEffect] = nil
end
end
end
end
-- Parse cast starts for non-SuperWoW fallback
if NP.SPELL_DAMAGE_EVENTS and NP.SPELL_DAMAGE_EVENTS[evnt] then
ParseCastStart(msg)
end
-- Holy Strike handler for Paladin
if evnt == "CHAT_MSG_SPELL_SELF_DAMAGE" and Auras then
Auras:HolyStrikeHandler(msg)
end
end
function NanamiPlates_CombatLog.HandleCombatEvent(evnt, msg)
if not msg then return end
InitReferences()
ParseAttackHit(msg)
end
NanamiPlates_CombatLog.cmatch = cmatch
NanamiPlates_CombatLog.castIcons = castIcons
NanamiPlates_CombatLog.ParseCastStart = ParseCastStart
NanamiPlates_CombatLog.ParseAttackHit = ParseAttackHit
NanamiPlates_CombatLog.InitReferences = InitReferences
NanamiPlates.CombatLog = NanamiPlates_CombatLog

193
ComboPoints.lua Normal file
View File

@@ -0,0 +1,193 @@
NanamiPlates_ComboPoints = {}
local NP = NanamiPlates
local Settings = NP.Settings
local GetComboPoints = GetComboPoints
local UnitExists = UnitExists
local CreateFrame = CreateFrame
local MAX_COMBO_POINTS = 5
local CP_SIZE = 10
local CP_SPACING = 3
local COMBO_COLORS = {
{0.3, 1.0, 0.3, 1},
{0.6, 1.0, 0.0, 1},
{1.0, 0.85, 0.0, 1},
{1.0, 0.45, 0.0, 1},
{1.0, 0.1, 0.1, 1},
}
local _, playerClass = UnitClass("player")
playerClass = playerClass or ""
local canUseComboPoints = (playerClass == "ROGUE" or playerClass == "DRUID")
function NanamiPlates_ComboPoints:CanUseComboPoints()
return canUseComboPoints
end
function NanamiPlates_ComboPoints:CreateComboPointFrames(nameplate)
if not canUseComboPoints then return end
nameplate.comboPoints = {}
local plateName = nameplate:GetName() or "UnknownPlate"
local size = Settings.comboPointsSize or CP_SIZE
for i = 1, MAX_COMBO_POINTS do
local cp = CreateFrame("Frame", plateName .. "CP" .. i, nameplate)
cp:SetWidth(size)
cp:SetHeight(size)
cp:SetFrameLevel(nameplate.health:GetFrameLevel() + 6)
cp:EnableMouse(false)
-- Outer glow/shadow
cp.glow = cp:CreateTexture(nil, "BACKGROUND")
cp.glow:SetTexture("Interface\\Buttons\\WHITE8X8")
cp.glow:SetPoint("CENTER", cp, "CENTER", 0, 0)
cp.glow:SetWidth(size + 4)
cp.glow:SetHeight(size + 4)
cp.glow:SetVertexColor(0, 0, 0, 0.6)
cp.glow:SetDrawLayer("BACKGROUND")
-- Border (themed)
cp.border = cp:CreateTexture(nil, "BORDER")
cp.border:SetTexture("Interface\\Buttons\\WHITE8X8")
cp.border:SetPoint("CENTER", cp, "CENTER", 0, 0)
cp.border:SetWidth(size + 2)
cp.border:SetHeight(size + 2)
local brR, brG, brB = NP.GetThemeColor("panelBorder", 0.55, 0.30, 0.42, 1)
cp.border:SetVertexColor(brR, brG, brB, 1)
cp.border:SetDrawLayer("BORDER")
-- Main fill
cp.icon = cp:CreateTexture(nil, "ARTWORK")
cp.icon:SetTexture("Interface\\Buttons\\WHITE8X8")
cp.icon:SetPoint("CENTER", cp, "CENTER", 0, 0)
cp.icon:SetWidth(size)
cp.icon:SetHeight(size)
cp.icon:SetVertexColor(0.12, 0.08, 0.12, 0.6)
cp.icon:SetDrawLayer("ARTWORK")
-- Number text
cp.text = cp:CreateFontString(nil, "OVERLAY")
cp.text:SetFont(NP.GetFont(), size - 2, NP.GetFontOutline())
cp.text:SetPoint("CENTER", cp, "CENTER", 0, 0)
cp.text:SetText("")
cp.text:SetDrawLayer("OVERLAY")
cp.active = false
cp:Hide()
nameplate.comboPoints[i] = cp
end
end
function NanamiPlates_ComboPoints:UpdateComboPoints(nameplate, isTarget)
if not canUseComboPoints then return 0 end
if not nameplate.comboPoints then return 0 end
if not Settings.showComboPoints then
for i = 1, MAX_COMBO_POINTS do
if nameplate.comboPoints[i] then nameplate.comboPoints[i]:Hide() end
end
return 0
end
local numPoints = 0
if isTarget and UnitExists("target") then
numPoints = GetComboPoints("player", "target") or 0
end
local size = Settings.comboPointsSize or CP_SIZE
for i = 1, MAX_COMBO_POINTS do
local cp = nameplate.comboPoints[i]
if cp then
cp:SetWidth(size)
cp:SetHeight(size)
cp.glow:SetWidth(size + 4)
cp.glow:SetHeight(size + 4)
cp.border:SetWidth(size + 2)
cp.border:SetHeight(size + 2)
cp.icon:SetWidth(size)
cp.icon:SetHeight(size)
cp.text:SetFont(NP.GetFont(), size - 2 > 6 and size - 2 or 6, NP.GetFontOutline())
if i <= numPoints then
local color = COMBO_COLORS[i] or COMBO_COLORS[MAX_COMBO_POINTS]
cp.icon:SetVertexColor(color[1], color[2], color[3], 1)
cp.glow:SetVertexColor(color[1] * 0.4, color[2] * 0.4, color[3] * 0.4, 0.7)
cp.text:SetText(i)
cp.text:SetTextColor(1, 1, 1, 0.9)
cp.active = true
cp:Show()
elseif numPoints > 0 then
cp.icon:SetVertexColor(0.12, 0.08, 0.12, 0.5)
cp.glow:SetVertexColor(0, 0, 0, 0.4)
cp.text:SetText("")
cp.active = false
cp:Show()
else
cp:Hide()
end
end
end
return numPoints
end
function NanamiPlates_ComboPoints:UpdateComboPointPositions(nameplate, numDebuffs)
if not canUseComboPoints then return end
if not nameplate.comboPoints then return end
if not Settings.showComboPoints then return end
if not nameplate.healthBG then return end
local numVisible = 0
for i = 1, MAX_COMBO_POINTS do
if nameplate.comboPoints[i] and nameplate.comboPoints[i]:IsShown() then
numVisible = MAX_COMBO_POINTS
break
end
end
if numVisible == 0 then
if nameplate.name and nameplate._cpNameOffset then
nameplate.name:ClearAllPoints()
nameplate.name:SetPoint("BOTTOM", nameplate.healthBG, "TOP", 0, 2)
nameplate._cpNameOffset = nil
end
return
end
local size = Settings.comboPointsSize or CP_SIZE
local spacing = CP_SPACING
local totalWidth = (size * MAX_COMBO_POINTS) + (spacing * (MAX_COMBO_POINTS - 1))
local startOffset = -totalWidth / 2 + size / 2
for i = 1, MAX_COMBO_POINTS do
local cp = nameplate.comboPoints[i]
if cp then
cp:ClearAllPoints()
local xOffset = startOffset + (i - 1) * (size + spacing)
cp:SetPoint("BOTTOM", nameplate.healthBG, "TOP", xOffset, 2)
end
end
if nameplate.name then
nameplate.name:ClearAllPoints()
nameplate.name:SetPoint("BOTTOM", nameplate.healthBG, "TOP", 0, 2 + size + 2)
nameplate._cpNameOffset = true
end
end
function NanamiPlates_ComboPoints:HideComboPoints(nameplate)
if not nameplate.comboPoints then return end
for i = 1, MAX_COMBO_POINTS do
if nameplate.comboPoints[i] then nameplate.comboPoints[i]:Hide() end
end
if nameplate.name and nameplate._cpNameOffset then
nameplate.name:ClearAllPoints()
nameplate.name:SetPoint("BOTTOM", nameplate.healthBG, "TOP", 0, 2)
nameplate._cpNameOffset = nil
end
end
NanamiPlates.ComboPoints = NanamiPlates_ComboPoints

228
Config.lua Normal file
View File

@@ -0,0 +1,228 @@
local NP = NanamiPlates
local function GetThemeColor(key, fallbackR, fallbackG, fallbackB, fallbackA)
if SFrames and SFrames.ActiveTheme and SFrames.ActiveTheme[key] then
local c = SFrames.ActiveTheme[key]
return c[1] or fallbackR, c[2] or fallbackG, c[3] or fallbackB, c[4] or fallbackA or 1
end
return fallbackR, fallbackG, fallbackB, fallbackA or 1
end
NP.GetThemeColor = GetThemeColor
local function GetFont()
if SFrames and SFrames.GetFont then
return SFrames:GetFont()
end
return "Fonts\\ARIALN.TTF"
end
NP.GetFont = GetFont
local function GetFontOutline()
if SFrames and SFrames.Media and SFrames.Media.fontOutline then
return SFrames.Media.fontOutline
end
return "OUTLINE"
end
NP.GetFontOutline = GetFontOutline
local function GetTexture()
if SFrames and SFrames.GetTexture then
return SFrames:GetTexture()
end
return "Interface\\TargetingFrame\\UI-StatusBar"
end
NP.GetTexture = GetTexture
NP.Settings = {
healthbarHeight = 12,
healthbarWidth = 120,
healthFontSize = 9,
healthTextFormat = 4,
friendHealthbarHeight = 4,
friendHealthbarWidth = 85,
friendHealthFontSize = 8,
friendHealthTextFormat = 1,
castbarHeight = 10,
castbarWidth = 120,
showCastbarIcon = true,
friendCastbarHeight = 6,
friendCastbarWidth = 85,
friendShowCastbarIcon = true,
castbarColor = {1, 0.8, 0, 1},
levelFontSize = 9,
nameFontSize = 9,
friendLevelFontSize = 7,
friendNameFontSize = 8,
raidIconPosition = "LEFT",
namePosition = "BOTTOM",
showOnlyMyDebuffs = false,
showDebuffTimers = true,
debuffIconSize = 20,
showComboPoints = true,
comboPointsSize = 12,
showTargetGlow = true,
targetArrowStyle = 1,
targetArrowSize = 24,
targetArrowOffset = 0,
targetArrowTint = 0,
nonTargetAlpha = 0.35,
showCritterNameplates = false,
showManaBar = true,
manabarHeight = 3,
nameplateYOffset = 15,
pvpEnemyAsFriendly = false,
pvpEnemyNoClassColors = false,
showQuestIcon = true,
}
NP.Colors = {
hostile = {0.85, 0.2, 0.2, 1},
neutral = {0.9, 0.7, 0.0, 1},
friendly = {0.2, 0.8, 0.2, 1},
tapped = {0.5, 0.5, 0.5, 1},
class = {
WARRIOR = {0.78, 0.61, 0.43},
MAGE = {0.41, 0.80, 0.94},
ROGUE = {1.0, 0.96, 0.41},
DRUID = {1.0, 0.49, 0.04},
HUNTER = {0.67, 0.83, 0.45},
SHAMAN = {0.14, 0.35, 1.0},
PRIEST = {1.0, 1.0, 1.0},
WARLOCK = {0.58, 0.51, 0.79},
PALADIN = {0.96, 0.55, 0.73},
},
power = {
[0] = {0.0, 0.0, 1.0},
[1] = {1.0, 0.0, 0.0},
[2] = {1.0, 0.5, 0.0},
[3] = {1.0, 1.0, 0.0},
[4] = {0.0, 1.0, 1.0},
},
}
NP.THREAT_COLORS = {
DPS = {
AGGRO = {1.0, 0.2, 0.2, 1},
HIGH_THREAT = {1.0, 0.6, 0.0, 1},
NO_AGGRO = {0.85, 0.2, 0.2, 1},
},
TANK = {
AGGRO = {0.2, 0.8, 0.2, 1},
LOSING_AGGRO = {1.0, 0.6, 0.0, 1},
NO_AGGRO = {1.0, 0.1, 0.1, 1},
OTHER_TANK = {0.6, 0.8, 1.0, 1},
},
TAPPED = {0.5, 0.5, 0.5, 1},
STUN = {0.376, 0.027, 0.431, 1},
}
NP.Critters = {
["adder"] = true, ["beetle"] = true, ["belfry bat"] = true,
["biletoad"] = true, ["black rat"] = true, ["brown prairie dog"] = true,
["caged rabbit"] = true, ["caged sheep"] = true, ["caged squirrel"] = true,
["caged toad"] = true, ["cat"] = true, ["chicken"] = true,
["cleo"] = true, ["core rat"] = true, ["cow"] = true,
["cured deer"] = true, ["cured gazelle"] = true, ["deeprun rat"] = true,
["deer"] = true, ["dog"] = true, ["effsee"] = true,
["enthralled deeprun rat"] = true, ["fang"] = true, ["fawn"] = true,
["fire beetle"] = true, ["fluffy"] = true, ["frog"] = true,
["gazelle"] = true, ["hare"] = true, ["horse"] = true,
["huge toad"] = true, ["infected deer"] = true, ["infected squirrel"] = true,
["jungle toad"] = true, ["krakle's thermometer"] = true, ["lady"] = true,
["larva"] = true, ["lava crab"] = true, ["maggot"] = true,
["moccasin"] = true, ["mouse"] = true, ["mr. bigglesworth"] = true,
["nibbles"] = true, ["noarm"] = true, ["old blanchy"] = true,
["parrot"] = true, ["pig"] = true, ["pirate treasure trigger mob"] = true,
["plagued insect"] = true, ["plagued maggot"] = true, ["plagued rat"] = true,
["plagueland termite"] = true, ["polymorphed chicken"] = true,
["polymorphed rat"] = true, ["prairie dog"] = true, ["rabbit"] = true,
["ram"] = true, ["rat"] = true, ["riding ram"] = true,
["roach"] = true, ["salome"] = true, ["school of fish"] = true,
["scorpion"] = true, ["sheep"] = true, ["shen'dralar wisp"] = true,
["sickly deer"] = true, ["sickly gazelle"] = true, ["snake"] = true,
["spider"] = true, ["spike"] = true, ["squirrel"] = true,
["swine"] = true, ["tainted cockroach"] = true, ["tainted rat"] = true,
["toad"] = true, ["transporter malfunction"] = true, ["turtle"] = true,
["underfoot"] = true, ["voice of elune"] = true,
["waypoint (only gm can see it)"] = true, ["wisp"] = true,
}
function NP:DetectTankSpec()
local _, playerClass = UnitClass("player")
if not playerClass then return end
if playerClass ~= "WARRIOR" and playerClass ~= "PALADIN" and playerClass ~= "DRUID" then
return
end
if not GetTalentTabInfo then return end
local _, _, p1 = GetTalentTabInfo(1)
local _, _, p2 = GetTalentTabInfo(2)
local _, _, p3 = GetTalentTabInfo(3)
p1 = p1 or 0
p2 = p2 or 0
p3 = p3 or 0
local isTank = false
if playerClass == "WARRIOR" then
isTank = (p3 >= p1 and p3 >= p2 and p3 >= 11)
elseif playerClass == "PALADIN" then
isTank = (p2 >= p1 and p2 >= p3 and p2 >= 11)
elseif playerClass == "DRUID" then
isTank = (p2 >= p1 and p2 >= p3 and p2 >= 11)
end
local newRole = isTank and "TANK" or "DPS"
if newRole ~= self.playerRole then
self.playerRole = newRole
self:SaveSettings()
self.Print("Auto role: " .. newRole)
if NanamiPlates_Threat then
NanamiPlates_Threat.BroadcastTankMode(true)
end
end
end
function NP:LoadSettings()
if NanamiPlatesDB then
for k, v in pairs(NanamiPlatesDB) do
if self.Settings[k] ~= nil then
self.Settings[k] = v
end
end
if NanamiPlatesDB.playerRole then
self.playerRole = NanamiPlatesDB.playerRole
end
end
end
function NP:SaveSettings()
NanamiPlatesDB = {}
for k, v in pairs(self.Settings) do
NanamiPlatesDB[k] = v
end
NanamiPlatesDB.playerRole = self.playerRole
end
if SFrames and SFrames.Config and SFrames.Config.colors and SFrames.Config.colors.class then
for k, v in pairs(SFrames.Config.colors.class) do
NP.Colors.class[k] = {v.r, v.g, v.b}
end
end

215
Core.lua Normal file
View File

@@ -0,0 +1,215 @@
NanamiPlates = {}
NanamiPlates.modules = {}
NanamiPlates.registry = {}
local pairs = pairs
local tostring = tostring
local CreateFrame = CreateFrame
local superwow_active = (SpellInfo ~= nil) or (UnitGUID ~= nil) or (SUPERWOW_VERSION ~= nil)
NanamiPlates.superwow_active = superwow_active
local _, playerClass = UnitClass("player")
playerClass = playerClass or ""
NanamiPlates.playerClass = playerClass
NanamiPlates.castDB = {}
NanamiPlates.castTracker = {}
NanamiPlates.debuffTracker = {}
NanamiPlates.recentMeleeCrits = {}
NanamiPlates.recentMeleeHits = {}
NanamiPlates.playerClassCache = {}
NanamiPlates.playerRole = "DPS"
local function Print(msg)
if DEFAULT_CHAT_FRAME then
DEFAULT_CHAT_FRAME:AddMessage("|cffff88cc[Nanami-Plates]|r " .. tostring(msg))
end
end
NanamiPlates.Print = Print
local function HookScript(frame, script, func)
local prev = frame:GetScript(script)
frame:SetScript(script, function(a1, a2, a3, a4, a5, a6, a7, a8, a9)
if prev then prev(a1, a2, a3, a4, a5, a6, a7, a8, a9) end
func(a1, a2, a3, a4, a5, a6, a7, a8, a9)
end)
end
NanamiPlates.HookScript = HookScript
local function DisableShaguTweaksNameplates()
if ShaguTweaks and ShaguTweaks.libnameplate then
ShaguTweaks.libnameplate:SetScript("OnUpdate", nil)
ShaguTweaks.libnameplate.OnInit = {}
ShaguTweaks.libnameplate.OnShow = {}
ShaguTweaks.libnameplate.OnUpdate = {}
ShaguTweaks.libnameplate.disabled_by_nanamiplates = true
return true
end
return false
end
local function DisablePfUINameplates()
if pfUI then
if pfUI.modules then pfUI.modules["nameplates"] = nil end
if pfNameplates then
pfNameplates:Hide()
pfNameplates:UnregisterAllEvents()
end
if pfUI.nameplates then pfUI.nameplates = nil end
return true
end
return false
end
DisableShaguTweaksNameplates()
DisablePfUINameplates()
local function GetPlayerClassByName(name)
if not name then return nil end
local cache = NanamiPlates.playerClassCache
if cache[name] then return cache[name] end
local playerName = UnitName("player")
if name == playerName then
cache[name] = playerClass
return playerClass
end
local numRaid = GetNumRaidMembers()
if numRaid > 0 then
for i = 1, numRaid do
local raidName, _, _, _, _, raidClass = GetRaidRosterInfo(i)
if raidName == name then
cache[name] = raidClass
return raidClass
end
end
else
local numParty = GetNumPartyMembers()
for i = 1, numParty do
local partyUnit = "party" .. i
if UnitExists(partyUnit) then
local partyName = UnitName(partyUnit)
if partyName == name then
local _, partyClass = UnitClass(partyUnit)
cache[name] = partyClass
return partyClass
end
end
end
end
return nil
end
NanamiPlates.GetPlayerClassByName = GetPlayerClassByName
local NP_EventFrame = CreateFrame("Frame", "NanamiPlatesFrame", UIParent)
NP_EventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
NP_EventFrame:RegisterEvent("ADDON_LOADED")
NP_EventFrame:RegisterEvent("PLAYER_TARGET_CHANGED")
NP_EventFrame:RegisterEvent("UNIT_AURA")
NP_EventFrame:RegisterEvent("PARTY_MEMBERS_CHANGED")
NP_EventFrame:RegisterEvent("RAID_ROSTER_UPDATE")
NP_EventFrame:RegisterEvent("PLAYER_LEVEL_UP")
NP_EventFrame:RegisterEvent("PLAYER_REGEN_DISABLED")
NP_EventFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
NP_EventFrame:RegisterEvent("UNIT_CASTEVENT")
NP_EventFrame:RegisterEvent("SPELLCAST_STOP")
NP_EventFrame:RegisterEvent("CHAT_MSG_SPELL_FAILED_LOCALPLAYER")
NP_EventFrame:RegisterEvent("CHARACTER_POINTS_CHANGED")
NP_EventFrame:RegisterEvent("QUEST_LOG_UPDATE")
NP_EventFrame:RegisterEvent("CHAT_MSG_SPELL_HOSTILEPLAYER_DAMAGE")
NP_EventFrame:RegisterEvent("CHAT_MSG_SPELL_CREATURE_VS_CREATURE_DAMAGE")
NP_EventFrame:RegisterEvent("CHAT_MSG_SPELL_CREATURE_VS_PARTY_DAMAGE")
NP_EventFrame:RegisterEvent("CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE")
NP_EventFrame:RegisterEvent("CHAT_MSG_SPELL_HOSTILEPLAYER_BUFF")
NP_EventFrame:RegisterEvent("CHAT_MSG_SPELL_CREATURE_VS_CREATURE_BUFF")
NP_EventFrame:RegisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE")
NP_EventFrame:RegisterEvent("CHAT_MSG_SPELL_TRADESKILLS")
NP_EventFrame:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE")
NP_EventFrame:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE")
NP_EventFrame:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_SELF_DAMAGE")
NP_EventFrame:RegisterEvent("CHAT_MSG_SPELL_AURA_GONE_OTHER")
NP_EventFrame:RegisterEvent("CHAT_MSG_SPELL_AURA_GONE_SELF")
NP_EventFrame:RegisterEvent("CHAT_MSG_SPELL_CREATURE_VS_PARTY_BUFF")
NP_EventFrame:RegisterEvent("CHAT_MSG_SPELL_CREATURE_VS_SELF_BUFF")
NP_EventFrame:RegisterEvent("CHAT_MSG_COMBAT_SELF_HITS")
NP_EventFrame:RegisterEvent("CHAT_MSG_COMBAT_PARTY_HITS")
NP_EventFrame:RegisterEvent("CHAT_MSG_COMBAT_FRIENDLYPLAYER_HITS")
NP_EventFrame:RegisterEvent("CHAT_MSG_COMBAT_CREATURE_VS_CREATURE_HITS")
NP_EventFrame:RegisterEvent("CHAT_MSG_COMBAT_SELF_RANGED_HITS")
NP_EventFrame:RegisterEvent("CHAT_MSG_COMBAT_PARTY_RANGED_HITS")
NanamiPlates.EventFrame = NP_EventFrame
NanamiPlates.SPELL_EVENTS = {
["CHAT_MSG_SPELL_HOSTILEPLAYER_DAMAGE"] = true,
["CHAT_MSG_SPELL_CREATURE_VS_CREATURE_DAMAGE"] = true,
["CHAT_MSG_SPELL_CREATURE_VS_PARTY_DAMAGE"] = true,
["CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE"] = true,
["CHAT_MSG_SPELL_HOSTILEPLAYER_BUFF"] = true,
["CHAT_MSG_SPELL_CREATURE_VS_CREATURE_BUFF"] = true,
["CHAT_MSG_SPELL_SELF_DAMAGE"] = true,
["CHAT_MSG_SPELL_TRADESKILLS"] = true,
["CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE"] = true,
["CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE"] = true,
["CHAT_MSG_SPELL_PERIODIC_SELF_DAMAGE"] = true,
["CHAT_MSG_SPELL_AURA_GONE_OTHER"] = true,
["CHAT_MSG_SPELL_AURA_GONE_SELF"] = true,
["CHAT_MSG_SPELL_CREATURE_VS_PARTY_BUFF"] = true,
["CHAT_MSG_SPELL_CREATURE_VS_SELF_BUFF"] = true,
["CHAT_MSG_SPELL_FAILED_LOCALPLAYER"] = true,
}
NanamiPlates.SPELL_DAMAGE_EVENTS = {
["CHAT_MSG_SPELL_HOSTILEPLAYER_DAMAGE"] = true,
["CHAT_MSG_SPELL_CREATURE_VS_CREATURE_DAMAGE"] = true,
["CHAT_MSG_SPELL_CREATURE_VS_PARTY_DAMAGE"] = true,
["CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE"] = true,
["CHAT_MSG_SPELL_SELF_DAMAGE"] = true,
["CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE"] = true,
["CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE"] = true,
["CHAT_MSG_SPELL_PERIODIC_SELF_DAMAGE"] = true,
}
NanamiPlates.COMBAT_EVENTS = {
["CHAT_MSG_COMBAT_SELF_HITS"] = true,
["CHAT_MSG_COMBAT_PARTY_HITS"] = true,
["CHAT_MSG_COMBAT_FRIENDLYPLAYER_HITS"] = true,
["CHAT_MSG_COMBAT_CREATURE_VS_CREATURE_HITS"] = true,
["CHAT_MSG_COMBAT_SELF_RANGED_HITS"] = true,
["CHAT_MSG_COMBAT_PARTY_RANGED_HITS"] = true,
}
NanamiPlates.STUN_EFFECTS = {
"Cheap Shot", "Kidney Shot", "Bash", "Hammer of Justice",
"Charge Stun", "Intercept Stun", "Concussion Blow",
"Gouge", "Sap", "Pounce"
}
NanamiPlates.REMOVE_PENDING_PATTERNS = {
SPELLIMMUNESELFOTHER or "%s is immune to your %s.",
IMMUNEDAMAGECLASSSELFOTHER or "%s is immune to your %s damage.",
SPELLMISSSELFOTHER or "Your %s missed %s.",
SPELLRESISTSELFOTHER or "Your %s was resisted by %s.",
SPELLEVADEDSELFOTHER or "Your %s was evaded by %s.",
SPELLDODGEDSELFOTHER or "Your %s was dodged by %s.",
SPELLDEFLECTEDSELFOTHER or "Your %s was deflected by %s.",
SPELLREFLECTSELFOTHER or "Your %s was reflected back by %s.",
SPELLPARRIEDSELFOTHER or "Your %s was parried by %s.",
SPELLLOGABSORBSELFOTHER or "Your %s is absorbed by %s.",
}
NanamiPlates.TANK_CLASSES = {
["Warrior"] = true,
["Paladin"] = true,
["Druid"] = true,
["Shaman"] = true,
}
if DEFAULT_CHAT_FRAME then
DEFAULT_CHAT_FRAME:AddMessage("|cffff88cc[Nanami-Plates]|r Loading...")
end

153
Healthbar.lua Normal file
View File

@@ -0,0 +1,153 @@
NanamiPlates_Healthbar = {}
local NP = NanamiPlates
local NEUTRAL_COLOR = {0.9, 0.7, 0.0, 1}
local HOSTILE_THRESHOLD = {r_min = 0.9, g_max = 0.2, b_max = 0.2}
local NEUTRAL_THRESHOLD = {r_min = 0.9, g_min = 0.9, b_max = 0.2}
function NanamiPlates_Healthbar.ResetCache(nameplate)
if not nameplate then return end
nameplate.cachedIsFriendly = nil
nameplate.cachedIsHostile = nil
nameplate.cachedIsNeutral = nil
nameplate.wasNeutral = nil
nameplate.lastPlateName = nil
nameplate.lastColorR = nil
nameplate.lastColorG = nil
nameplate.lastColorB = nil
end
function NanamiPlates_Healthbar.DetectUnitType(nameplate, original)
if not nameplate or not original or not original.healthbar then
return false, false, true, 0, 1, 0
end
local r, g, b = original.healthbar:GetStatusBarColor()
-- Guard: if color is (0,0,0) the engine hasn't set it yet, use previous cache or default hostile
if r == 0 and g == 0 and b == 0 then
if nameplate.cachedIsHostile ~= nil then
return nameplate.cachedIsHostile, nameplate.cachedIsNeutral, nameplate.cachedIsFriendly, r, g, b
end
return true, false, false, 1, 0, 0
end
local isHostile, isNeutral, isFriendly
local lastR, lastG, lastB = nameplate.lastColorR, nameplate.lastColorG, nameplate.lastColorB
if r == lastR and g == lastG and b == lastB then
isHostile = nameplate.cachedIsHostile
isNeutral = nameplate.cachedIsNeutral
isFriendly = nameplate.cachedIsFriendly
else
isHostile = r > HOSTILE_THRESHOLD.r_min and g < HOSTILE_THRESHOLD.g_max and b < HOSTILE_THRESHOLD.b_max
isNeutral = r > NEUTRAL_THRESHOLD.r_min and g > NEUTRAL_THRESHOLD.g_min and b < NEUTRAL_THRESHOLD.b_max
isFriendly = not isHostile and not isNeutral
nameplate.lastColorR = r
nameplate.lastColorG = g
nameplate.lastColorB = b
nameplate.cachedIsHostile = isHostile
nameplate.cachedIsNeutral = isNeutral
nameplate.cachedIsFriendly = isFriendly
if nameplate.wasNeutral == nil then
nameplate.wasNeutral = isNeutral
end
end
return isHostile, isNeutral, isFriendly, r, g, b
end
function NanamiPlates_Healthbar.CheckUnitChange(nameplate, plateName, isNeutral)
if not nameplate then return end
if plateName and plateName ~= nameplate.lastPlateName then
nameplate.lastPlateName = plateName
nameplate.wasNeutral = isNeutral
end
end
function NanamiPlates_Healthbar.IsNeutral(nameplate)
if not nameplate then return false end
return nameplate.cachedIsNeutral or nameplate.wasNeutral or false
end
function NanamiPlates_Healthbar.WasNeutral(nameplate)
if not nameplate then return false end
return nameplate.wasNeutral or false
end
function NanamiPlates_Healthbar.IsFriendly(nameplate)
if not nameplate then return false end
return nameplate.cachedIsFriendly or false
end
function NanamiPlates_Healthbar.IsHostile(nameplate)
if not nameplate then return false end
return nameplate.cachedIsHostile or false
end
function NanamiPlates_Healthbar.GetNeutralColor()
return NEUTRAL_COLOR[1], NEUTRAL_COLOR[2], NEUTRAL_COLOR[3], NEUTRAL_COLOR[4]
end
function NanamiPlates_Healthbar.ApplyNeutralColor(nameplate)
if not nameplate or not nameplate.health then return end
nameplate.health:SetStatusBarColor(NEUTRAL_COLOR[1], NEUTRAL_COLOR[2], NEUTRAL_COLOR[3], NEUTRAL_COLOR[4])
end
function NanamiPlates_Healthbar.ShouldShowNeutral(nameplate, isNeutral, isAttackingPlayer)
if not nameplate then return false end
return (isNeutral or nameplate.wasNeutral) and not isAttackingPlayer
end
function NanamiPlates_Healthbar.IsCritter(frame, nameplate, original, unitstr)
if unitstr and UnitCreatureType then
local creatureType = UnitCreatureType(unitstr)
if creatureType == "Critter" then return true end
end
if NP and NP.Critters then
local plateName = nil
if original and original.name and original.name.GetText then
plateName = original.name:GetText()
end
if not plateName and nameplate and nameplate.name and nameplate.name.GetText then
plateName = nameplate.name:GetText()
end
if plateName and NP.Critters[string.lower(plateName)] then
return true
end
end
if not unitstr and original and original.healthbar then
local r, g, b = original.healthbar:GetStatusBarColor()
local isNeutralColor = r > 0.9 and g > 0.9 and b < 0.2
if isNeutralColor then
local levelText = nil
if original.level and original.level.GetText then
levelText = original.level:GetText()
end
local _, hpmax = original.healthbar:GetMinMaxValues()
if levelText == "1" and hpmax and hpmax > 0 and hpmax < 100 then
return true
end
end
end
return false
end
function NanamiPlates_Healthbar.ShouldSkipNameplate(frame, nameplate, original, Settings)
if not Settings or Settings.showCritterNameplates then
return false
end
local unitstr = nil
if NP and NP.superwow_active and frame and frame.GetName then
unitstr = frame:GetName(1)
end
return NanamiPlates_Healthbar.IsCritter(frame, nameplate, original, unitstr)
end
NanamiPlates.Healthbar = NanamiPlates_Healthbar

22
Nanami-Plates.toc Normal file
View File

@@ -0,0 +1,22 @@
## Interface: 11200
## Title: Nanami-Plates
## Notes: Nanami-UI style nameplates for Turtle WoW
## Author: Nanami
## Version: 1.0.0
## Dependencies: Nanami-UI
## OptionalDeps: TWThreat
## SavedVariables: NanamiPlatesDB
Core.lua
Config.lua
SpellDB.lua
Scanner.lua
Healthbar.lua
Castbar.lua
Auras.lua
ComboPoints.lua
Threat.lua
Target.lua
Plates.lua
CombatLog.lua
Options.lua

1072
Options.lua Normal file

File diff suppressed because it is too large Load Diff

1402
Plates.lua Normal file

File diff suppressed because it is too large Load Diff

107
README.md Normal file
View File

@@ -0,0 +1,107 @@
# Nanami-Plates
> Turtle WoW 高度可定制的姓名板替换插件,采用 Nanami-UI 风格,提供血条、施法条、减益追踪、仇恨着色、连击点、目标指示、任务怪标记等丰富功能,完整支持 SuperWoW 增强特性。
**Nanami-Plates** 完全替换游戏默认姓名板,为 Turtle WoW (1.12) 提供现代化、美观且功能强大的姓名板体验。作为 Nanami-UI 套件的一部分,它自动继承主题配色、字体和材质,与整体 UI 风格浑然一体。
## 核心功能
### 血条系统
- 敌方与友方姓名板独立设置宽度、高度、字号
- 6 种血量显示格式:隐藏 / 百分比 / 当前值 / 当前(百分比) / 当前/最大 / 当前/最大 %
- 等级文本根据与玩家的等级差自动着色(红/橙/黄/绿/灰)
- 精英(+)、稀有、世界Boss(??) 分类标识
- 玩家单位自动显示职业颜色
- 被其他玩家标记的怪物显示灰色Tapped 检测)
- 可选隐藏小动物(兔子、松鼠等)姓名板
### 施法条
- 显示法术名称、法术图标和施法倒计时
- 完整支持 SuperWoW 的 `UNIT_CASTEVENT` / `UnitCastingInfo` / `UnitChannelInfo`
- 无 SuperWoW 时回退至暴雪原生施法条数据
- 敌方与友方施法条独立配置尺寸
### 减益 (Debuff) 追踪
- 在姓名板下方显示减益图标,支持最多 16 个
- 图标上实时显示剩余时间倒计时(颜色随时间变化:白→黄→红)
- 内置完整的法术持续时间数据库SpellDB涵盖所有职业的 DoT、诅咒、毒药、陷阱等
- 可选「仅显示自己的减益」过滤模式
- 术士诅咒互斥逻辑完美处理(含 Malediction 天赋支持)
- 自动学习本地化法术名称映射,多语言服务器无缝兼容
### 连击点显示
- 盗贼和德鲁伊(猫形态)在目标姓名板上方显示连击点
- 每个连击点独立着色(绿→黄→橙→红),带数字标识
- 可自定义连击点大小
### 仇恨系统
- 集成 TWThreat 插件数据,实时仇恨着色
- 自动检测坦克天赋(战士防护 / 骑士防护 / 德鲁伊野性)自动切换坦克/输出模式
- **坦克模式:** 持有仇恨=绿色 / 即将丢失=橙色 / 未持有=红色 / 其他坦克持有=蓝色
- **输出模式:** OT=红色 / 高仇恨=橙色 / 安全=默认色
- 目标被控制(击晕/闷棍等)时显示紫色
- 通过插件频道在队伍/团队中广播坦克模式
### 目标指示
- 4 种箭头样式可选,显示在目标姓名板两侧 `>> [血条] <<`
- 箭头大小、偏移量、染色强度均可调节
- 目标血条高亮发光效果(自适应亮度的 Additive 混合)
- 目标血条边框高亮
- 目标姓名板自动提升 FrameStrata 保持在最前
- 非目标姓名板透明度可调(默认 35%
### 法力/能量/怒气条
- SuperWoW 环境下可在血条下方显示目标的法力条
- 根据能量类型自动着色(蓝=法力 / 红=怒气 / 橙=能量 / 黄=幸福值)
### 任务怪标记
- 自动扫描任务日志,在任务目标怪物旁显示 "!" 图标
- 显示击杀进度(如 "3/8"
- 兼容 pfQuest 的提示数据
### 团队标记
- 正确显示团队标记图标(骷髅、叉、月亮等)
## 设置面板
输入 `/np` 或点击小地图按钮即可打开设置界面,共 5 个标签页:
| 标签 | 内容 |
|------|------|
| 血条 | 敌方/友方血条尺寸、字号、血量格式、垂直偏移 |
| 施法条 | 敌方/友方施法条尺寸、法术图标开关 |
| 减益 | 减益计时器、仅显示自己的减益、图标大小、连击点设置 |
| 目标 | 箭头样式/大小/偏移/染色、非目标透明度、坦克/输出角色切换 |
| 其他 | 小动物姓名板、法力条、任务怪图标、重置设置 |
## 命令
| 命令 | 功能 |
|------|------|
| `/np` | 打开设置面板 |
| `/np tank` | 切换坦克/输出模式 |
| `/np reset` | 重置所有设置并重载 UI |
| `/np minimap` | 显示/隐藏小地图按钮 |
| `/np debug` | 输出当前目标的 Debuff 调试信息 |
## 依赖与兼容
- **必需:** Nanami-UI 主框架
- **可选:** TWThreat仇恨数据源
- **增强:** SuperWoW — 启用后解锁施法条详情、精确减益追踪、法力条显示等高级功能
- **自动禁用冲突:** 自动禁用 ShaguTweaks 和 pfUI 的姓名板模块,避免冲突
## 安装
1.`Nanami-Plates` 文件夹放入 `Interface\AddOns\`
2. 确保已安装 Nanami-UI
3. 进入游戏,按 `V` 键显示姓名板

63
Scanner.lua Normal file
View File

@@ -0,0 +1,63 @@
NanamiPlates_Scanner = {}
local initializedChildren = 0
local cachedWorldChildren = {}
local function CheckRegionForBorder(r)
if r and r.GetObjectType and r:GetObjectType() == "Texture" and r.GetTexture then
return r:GetTexture() == "Interface\\Tooltips\\Nameplate-Border"
end
return false
end
function NanamiPlates_Scanner.IsNamePlate(frame)
if not frame then return nil end
local objType = frame:GetObjectType()
if objType ~= "Frame" and objType ~= "Button" then return nil end
local r1, r2, r3, r4, r5, r6 = frame:GetRegions()
if CheckRegionForBorder(r1) then return true end
if CheckRegionForBorder(r2) then return true end
if CheckRegionForBorder(r3) then return true end
if CheckRegionForBorder(r4) then return true end
if CheckRegionForBorder(r5) then return true end
if CheckRegionForBorder(r6) then return true end
return nil
end
function NanamiPlates_Scanner.ScanForNewNameplates(registry, callback)
local parentcount = WorldFrame:GetNumChildren()
if initializedChildren >= parentcount then
return false
end
cachedWorldChildren = { WorldFrame:GetChildren() }
local foundNew = false
for i = initializedChildren + 1, parentcount do
local plate = cachedWorldChildren[i]
if plate and not registry[plate] then
if NanamiPlates_Scanner.IsNamePlate(plate) then
callback(plate)
foundNew = true
end
end
end
initializedChildren = parentcount
return foundNew
end
function NanamiPlates_Scanner.Reset()
initializedChildren = 0
for k in pairs(cachedWorldChildren) do
cachedWorldChildren[k] = nil
end
end
function NanamiPlates_Scanner.GetInitializedCount()
return initializedChildren
end
NanamiPlates.Scanner = NanamiPlates_Scanner

1484
SpellDB.lua Normal file

File diff suppressed because it is too large Load Diff

65
Target.lua Normal file
View File

@@ -0,0 +1,65 @@
NanamiPlates_Target = {}
local NP = NanamiPlates
local Settings = NP.Settings
local function ApplyArrowTint(ind)
if not ind or not ind.tex then return end
local tint = Settings.targetArrowTint or 0
if tint <= 0 then
ind.tex:SetVertexColor(1, 1, 1, 1)
else
local acR, acG, acB = NP.GetThemeColor("accent", 1.0, 0.5, 0.8, 1)
local r = 1 + (acR - 1) * tint
local g = 1 + (acG - 1) * tint
local b = 1 + (acB - 1) * tint
ind.tex:SetVertexColor(r, g, b, 1)
end
end
function NanamiPlates_Target.UpdateTarget(nameplate, isTarget)
if not nameplate then return end
local indL = nameplate.targetArrowL
local indR = nameplate.targetArrowR
if not indL or not indR then return end
if isTarget and Settings.showTargetGlow then
local acR, acG, acB = NP.GetThemeColor("accent", 1.0, 0.5, 0.8, 0.9)
ApplyArrowTint(indL)
ApplyArrowTint(indR)
indL:Show()
indR:Show()
-- Highlight healthBG border for target
if nameplate.healthBG then
nameplate.healthBG:SetBackdropBorderColor(acR, acG, acB, 1)
end
-- Show additive glow overlay (color/alpha synced in UpdateNamePlate)
if nameplate.targetGlow then
if not nameplate.targetGlow:IsShown() then
local barR, barG, barB = nameplate.health:GetStatusBarColor()
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
nameplate.targetGlow:SetStatusBarColor(barR, barG, barB, 1)
nameplate.targetGlow:SetAlpha(glowAlpha)
end
nameplate.targetGlow:Show()
end
else
indL:Hide()
indR:Hide()
-- Restore default border for non-target
if nameplate.healthBG then
local brR, brG, brB, brA = NP.GetThemeColor("panelBorder", 0.55, 0.30, 0.42, 0.9)
nameplate.healthBG:SetBackdropBorderColor(brR, brG, brB, brA)
end
if nameplate.targetGlow then
nameplate.targetGlow:Hide()
end
end
end
NanamiPlates.Target = NanamiPlates_Target

204
Threat.lua Normal file
View File

@@ -0,0 +1,204 @@
NanamiPlates_Threat = {}
local NP = NanamiPlates
local string_find = string.find
local string_sub = string.sub
local string_format = string.format
local string_gfind = string.gfind
local pairs = pairs
local ipairs = ipairs
local tonumber = tonumber
local tostring = tostring
local GetTime = GetTime
local UnitName = UnitName
local UnitExists = UnitExists
local UnitIsUnit = UnitIsUnit
local UnitInRaid = UnitInRaid
local UnitInParty = UnitInParty
local SendAddonMessage = SendAddonMessage
local GP_TankModeThreats = {}
local GP_Threats = {}
local GP_TankPlayers = {}
NanamiPlates_Threat.GP_TankModeThreats = GP_TankModeThreats
NanamiPlates_Threat.GP_Threats = GP_Threats
NanamiPlates_Threat.GP_TankPlayers = GP_TankPlayers
local function GP_Split(str, delimiter)
local result = {}
local pattern = "([^" .. delimiter .. "]+)"
for match in string_gfind(str, pattern) do
table.insert(result, match)
end
return result
end
local function GP_HandleTankModePacket(packet)
local startPos = string_find(packet, "TMTv1=")
if not startPos then return end
local dataStr = string_sub(packet, startPos + 6)
for k in pairs(GP_TankModeThreats) do GP_TankModeThreats[k] = nil end
local entries = GP_Split(dataStr, ";")
for _, entry in ipairs(entries) do
local parts = GP_Split(entry, ":")
if parts[1] and parts[2] and parts[3] and parts[4] then
GP_TankModeThreats[parts[2]] = {
creature = parts[1],
name = parts[3],
perc = tonumber(parts[4]) or 0
}
end
end
end
local function GP_HandleThreatPacket(packet)
local startPos = string_find(packet, "TWTv4=")
if not startPos then return end
local dataStr = string_sub(packet, startPos + 6)
for k in pairs(GP_Threats) do GP_Threats[k] = nil end
local entries = GP_Split(dataStr, ";")
for _, entry in ipairs(entries) do
local parts = GP_Split(entry, ":")
if parts[1] and parts[3] and parts[4] then
GP_Threats[parts[1]] = {
threat = tonumber(parts[3]) or 0,
perc = tonumber(parts[4]) or 0,
tank = (parts[6] == "1")
}
end
end
end
local NP_ADDON_PREFIX = "NanamiPlates"
local TANK_BROADCAST_DEBOUNCE = 5
local lastTankBroadcast = 0
function NanamiPlates_Threat.BroadcastTankMode(force)
local now = GetTime()
if not force and (now - lastTankBroadcast) < TANK_BROADCAST_DEBOUNCE then return end
local playerRole = NP and NP.playerRole
local isTank = (playerRole == "TANK")
local msg = isTank and "TM=1" or "TM=0"
local myName = UnitName("player")
if myName then
GP_TankPlayers[myName] = isTank or nil
end
if UnitInRaid("player") then
SendAddonMessage(NP_ADDON_PREFIX, msg, "RAID")
lastTankBroadcast = now
elseif UnitInParty() then
SendAddonMessage(NP_ADDON_PREFIX, msg, "PARTY")
lastTankBroadcast = now
end
end
function NanamiPlates_Threat.IsPlayerTank(playerName)
if not playerName then return false end
return GP_TankPlayers[playerName] == true
end
local function GP_HandleTankModeMessage(sender, msg)
if string_find(msg, "TM=") then
local isTank = string_sub(msg, 4, 4) == "1"
GP_TankPlayers[sender] = isTank or nil
end
end
function NanamiPlates_Threat.GetTWTankModeThreat(mobGUID, mobName)
local playerName = UnitName("player")
if mobGUID then
local data = GP_TankModeThreats[mobGUID]
if data then
local playerHasAggro = (data.name == playerName)
return true, playerHasAggro, data.name, data.perc or 0
end
end
if mobName then
for guid, data in pairs(GP_TankModeThreats) do
if data.creature == mobName then
local playerHasAggro = (data.name == playerName)
return true, playerHasAggro, data.name, data.perc or 0
end
end
end
return false, false, nil, 0
end
function NanamiPlates_Threat.GetGPThreatData()
local playerName = UnitName("player")
local playerPct = 0
local highestOtherPct = 0
local hasData = false
local threatHolderName = nil
for name, data in pairs(GP_Threats) do
hasData = true
local pct = data.perc or 0
if name == playerName then
playerPct = pct
else
if pct > highestOtherPct then highestOtherPct = pct end
end
if pct >= 100 then threatHolderName = name end
end
return hasData, (playerPct >= 100), playerPct, highestOtherPct, threatHolderName
end
function NanamiPlates_Threat.IsInPlayerGroup(unit)
if not unit or not UnitExists(unit) then return false end
if UnitIsUnit(unit, "player") then return true end
for i = 1, 4 do
if UnitIsUnit(unit, "party" .. i) then return true end
end
if UnitInRaid("player") then
for i = 1, 40 do
if UnitIsUnit(unit, "raid" .. i) then return true end
end
end
if UnitIsUnit(unit, "pet") then return true end
for i = 1, 4 do
if UnitIsUnit(unit, "partypet" .. i) then return true end
end
return false
end
local NP_ThreatFrame = CreateFrame("Frame")
NP_ThreatFrame:RegisterEvent("CHAT_MSG_ADDON")
NP_ThreatFrame:RegisterEvent("PARTY_MEMBERS_CHANGED")
NP_ThreatFrame:RegisterEvent("RAID_ROSTER_UPDATE")
NP_ThreatFrame:RegisterEvent("ZONE_CHANGED_NEW_AREA")
NP_ThreatFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
NP_ThreatFrame:SetScript("OnEvent", function()
if event == "CHAT_MSG_ADDON" then
local prefix = arg1
local msg = arg2 or ""
local sender = arg4
if string_find(msg, "TMTv1=") then GP_HandleTankModePacket(msg) end
if string_find(msg, "TWTv4=") then GP_HandleThreatPacket(msg) end
if prefix == NP_ADDON_PREFIX and msg and sender then
GP_HandleTankModeMessage(sender, msg)
end
elseif event == "PARTY_MEMBERS_CHANGED" or event == "RAID_ROSTER_UPDATE"
or event == "ZONE_CHANGED_NEW_AREA" or event == "PLAYER_ENTERING_WORLD" then
NanamiPlates_Threat.BroadcastTankMode(true)
end
end)
NanamiPlates.Threat = NanamiPlates_Threat

BIN
img/arrow.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

BIN
img/icon.tga Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB