第一次发版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