跟随版本 0.8.19

This commit is contained in:
rucky
2026-03-24 15:56:28 +08:00
parent 40d37dc8c4
commit c0f1ecc713
19 changed files with 2227 additions and 259 deletions

View File

@@ -1,6 +1,177 @@
SFrames.Target = {}
local _A = SFrames.ActiveTheme
local targetCLCast = nil
local spellIconCache = {}
local SPELL_ICONS = {
-- Mage
["Fireball"] = "Spell_Fire_FlameBolt",
["火球术"] = "Spell_Fire_FlameBolt",
["Frostbolt"] = "Spell_Frost_FrostBolt02",
["寒冰箭"] = "Spell_Frost_FrostBolt02",
["Polymorph"] = "Spell_Nature_Polymorph",
["变形术"] = "Spell_Nature_Polymorph",
["Arcane Missiles"] = "Spell_Nature_StarFall",
["奥术飞弹"] = "Spell_Nature_StarFall",
["Pyroblast"] = "Spell_Fire_Fireball02",
["炎爆术"] = "Spell_Fire_Fireball02",
["Scorch"] = "Spell_Fire_SoulBurn",
["灼烧"] = "Spell_Fire_SoulBurn",
["Flamestrike"] = "Spell_Fire_SelfDestruct",
["烈焰风暴"] = "Spell_Fire_SelfDestruct",
["Blizzard"] = "Spell_Frost_IceStorm",
["暴风雪"] = "Spell_Frost_IceStorm",
-- Warlock
["Shadow Bolt"] = "Spell_Shadow_ShadowBolt",
["暗影箭"] = "Spell_Shadow_ShadowBolt",
["Fear"] = "Spell_Shadow_Possession",
["恐惧术"] = "Spell_Shadow_Possession",
["Immolate"] = "Spell_Fire_Immolation",
["献祭"] = "Spell_Fire_Immolation",
["Soul Fire"] = "Spell_Fire_Fireball02",
["灵魂之火"] = "Spell_Fire_Fireball02",
["Drain Life"] = "Spell_Shadow_LifeDrain02",
["吸取生命"] = "Spell_Shadow_LifeDrain02",
["Drain Mana"] = "Spell_Shadow_SiphonMana",
["吸取法力"] = "Spell_Shadow_SiphonMana",
["Rain of Fire"] = "Spell_Shadow_RainOfFire",
["火焰之雨"] = "Spell_Shadow_RainOfFire",
["Hellfire"] = "Spell_Fire_Incinerate",
["地狱烈焰"] = "Spell_Fire_Incinerate",
-- Priest
["Greater Heal"] = "Spell_Holy_GreaterHeal",
["强效治疗术"] = "Spell_Holy_GreaterHeal",
["Flash Heal"] = "Spell_Holy_FlashHeal",
["快速治疗"] = "Spell_Holy_FlashHeal",
["Heal"] = "Spell_Holy_Heal",
["治疗术"] = "Spell_Holy_Heal",
["Smite"] = "Spell_Holy_HolySmite",
["惩击"] = "Spell_Holy_HolySmite",
["Mind Blast"] = "Spell_Shadow_UnholyFrenzy",
["心灵震爆"] = "Spell_Shadow_UnholyFrenzy",
["Mind Flay"] = "Spell_Shadow_SiphonMana",
["精神鞭笞"] = "Spell_Shadow_SiphonMana",
["Mind Control"] = "Spell_Shadow_ShadowWordDominate",
["精神控制"] = "Spell_Shadow_ShadowWordDominate",
["Holy Fire"] = "Spell_Holy_SearingLight",
["神圣之火"] = "Spell_Holy_SearingLight",
["Resurrection"] = "Spell_Holy_Resurrection",
["复活术"] = "Spell_Holy_Resurrection",
-- Shaman
["Lightning Bolt"] = "Spell_Nature_Lightning",
["闪电箭"] = "Spell_Nature_Lightning",
["Chain Lightning"] = "Spell_Nature_ChainLightning",
["闪电链"] = "Spell_Nature_ChainLightning",
["Healing Wave"] = "Spell_Nature_MagicImmunity",
["治疗波"] = "Spell_Nature_MagicImmunity",
["Lesser Healing Wave"] = "Spell_Nature_HealingWaveLesser",
["次级治疗波"] = "Spell_Nature_HealingWaveLesser",
["Chain Heal"] = "Spell_Nature_HealingWaveGreater",
["治疗链"] = "Spell_Nature_HealingWaveGreater",
["Ancestral Spirit"] = "Spell_Nature_Regenerate",
["先祖之魂"] = "Spell_Nature_Regenerate",
-- Druid
["Wrath"] = "Spell_Nature_AbolishMagic",
["愤怒"] = "Spell_Nature_AbolishMagic",
["Starfire"] = "Spell_Arcane_StarFire",
["星火术"] = "Spell_Arcane_StarFire",
["Regrowth"] = "Spell_Nature_ResistNature",
["愈合"] = "Spell_Nature_ResistNature",
["Healing Touch"] = "Spell_Nature_HealingTouch",
["治疗之触"] = "Spell_Nature_HealingTouch",
["Entangling Roots"] = "Spell_Nature_StrangleVines",
["纠缠根须"] = "Spell_Nature_StrangleVines",
["Hibernate"] = "Spell_Nature_Sleep",
["休眠"] = "Spell_Nature_Sleep",
["Rebirth"] = "Spell_Nature_Reincarnation",
["复生"] = "Spell_Nature_Reincarnation",
["Tranquility"] = "Spell_Nature_Tranquility",
["宁静"] = "Spell_Nature_Tranquility",
["Moonfire"] = "Spell_Nature_StarFall",
["月火术"] = "Spell_Nature_StarFall",
-- Paladin
["Holy Light"] = "Spell_Holy_HolyBolt",
["圣光术"] = "Spell_Holy_HolyBolt",
["Flash of Light"] = "Spell_Holy_FlashHeal",
["圣光闪现"] = "Spell_Holy_FlashHeal",
["Hammer of Wrath"] = "Spell_Holy_SealOfMight",
["愤怒之锤"] = "Spell_Holy_SealOfMight",
["Exorcism"] = "Spell_Holy_Excorcism_02",
["驱邪术"] = "Spell_Holy_Excorcism_02",
["Redemption"] = "Spell_Holy_Resurrection",
["救赎"] = "Spell_Holy_Resurrection",
-- Hunter
["Aimed Shot"] = "INV_Spear_07",
["瞄准射击"] = "INV_Spear_07",
["Multi-Shot"] = "Ability_UpgradeMoonGlaive",
["多重射击"] = "Ability_UpgradeMoonGlaive",
["Volley"] = "Ability_Marksmanship",
["乱射"] = "Ability_Marksmanship",
["Revive Pet"] = "Ability_Hunter_BeastSoothe",
["复活宠物"] = "Ability_Hunter_BeastSoothe",
-- Common NPC / generic
["Shoot"] = "Ability_Marksmanship",
["射击"] = "Ability_Marksmanship",
["Mend"] = "Spell_Holy_Heal",
["修补"] = "Spell_Holy_Heal",
["Rejuvenation"] = "Spell_Nature_Rejuvenation",
["回春术"] = "Spell_Nature_Rejuvenation",
}
for k, v in pairs(SPELL_ICONS) do
SPELL_ICONS[k] = "Interface\\Icons\\" .. v
end
local function BuildSpellIconCache()
if not GetSpellName or not GetSpellTexture then return end
local i = 1
while true do
local name = GetSpellName(i, "spell")
if not name then break end
local tex = GetSpellTexture(i, "spell")
if tex then spellIconCache[name] = tex end
i = i + 1
end
end
local function GetSpellIcon(spellName)
if not spellName then return nil end
local tex = spellIconCache[spellName]
or SPELL_ICONS[spellName]
or (NanamiPlates_CombatLog and NanamiPlates_CombatLog.castIcons
and NanamiPlates_CombatLog.castIcons[spellName])
if tex then return tex end
if GetSpellName and GetSpellTexture then
local i = 1
while true do
local name = GetSpellName(i, "spell")
if not name then break end
if name == spellName then
tex = GetSpellTexture(i, "spell")
if tex then
spellIconCache[spellName] = tex
return tex
end
end
i = i + 1
end
end
return nil
end
SFrames.GetSpellIcon = GetSpellIcon
SFrames.BuildSpellIconCache = BuildSpellIconCache
local function CLMatch(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 function Clamp(value, minValue, maxValue)
if value < minValue then
return minValue
@@ -467,7 +638,11 @@ function SFrames.Target:Initialize()
self:CreateAuras()
self:CreateCastbar()
self:InitializeDistanceFrame()
self:InitCastDetection()
BuildSpellIconCache()
SFrames:RegisterEvent("SPELLS_CHANGED", BuildSpellIconCache)
f.unit = "target"
f:SetScript("OnEnter", function()
GameTooltip_SetDefaultAnchor(GameTooltip, this)
@@ -492,11 +667,181 @@ function SFrames.Target:Initialize()
end
end
function SFrames.Target:InitCastDetection()
local castFrame = CreateFrame("Frame", nil, UIParent)
castFrame:RegisterEvent("UNIT_CASTEVENT")
castFrame:RegisterEvent("SPELLCAST_START")
castFrame:RegisterEvent("SPELLCAST_STOP")
castFrame:RegisterEvent("SPELLCAST_FAILED")
castFrame:RegisterEvent("SPELLCAST_INTERRUPTED")
castFrame:RegisterEvent("SPELLCAST_CHANNEL_START")
castFrame:RegisterEvent("SPELLCAST_CHANNEL_STOP")
local CL_EVENTS = {
"CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE",
"CHAT_MSG_SPELL_CREATURE_VS_PARTY_DAMAGE",
"CHAT_MSG_SPELL_CREATURE_VS_CREATURE_DAMAGE",
"CHAT_MSG_SPELL_CREATURE_VS_CREATURE_BUFF",
"CHAT_MSG_SPELL_CREATURE_VS_PARTY_BUFF",
"CHAT_MSG_SPELL_CREATURE_VS_SELF_BUFF",
"CHAT_MSG_SPELL_HOSTILEPLAYER_DAMAGE",
"CHAT_MSG_SPELL_HOSTILEPLAYER_BUFF",
"CHAT_MSG_SPELL_FRIENDLYPLAYER_DAMAGE",
"CHAT_MSG_SPELL_FRIENDLYPLAYER_BUFF",
"CHAT_MSG_SPELL_PARTY_DAMAGE",
"CHAT_MSG_SPELL_PARTY_BUFF",
"CHAT_MSG_SPELL_SELF_DAMAGE",
"CHAT_MSG_SPELL_SELF_BUFF",
}
for _, ev in ipairs(CL_EVENTS) do
castFrame:RegisterEvent(ev)
end
local function ResolveSelfIcon(spellName)
local texture
local _UCI = UnitCastingInfo or (ShaguTweaks and ShaguTweaks.UnitCastingInfo)
if _UCI then
local _, _, _, tex = _UCI("player")
texture = tex
end
if not texture then
local _UCH = UnitChannelInfo or (ShaguTweaks and ShaguTweaks.UnitChannelInfo)
if _UCH then
local _, _, _, tex = _UCH("player")
texture = tex
end
end
if not texture and SFrames.castdb and UnitGUID then
local guid = UnitGUID("player")
if guid and SFrames.castdb[guid] and SFrames.castdb[guid].icon then
texture = SFrames.castdb[guid].icon
end
end
if not texture or texture == "Interface\\Icons\\INV_Misc_QuestionMark" then
texture = GetSpellIcon(spellName) or texture
end
return texture or "Interface\\Icons\\INV_Misc_QuestionMark"
end
castFrame:SetScript("OnEvent", function()
-- Player's own cast events (for self-target and friendly-target-is-self)
if event == "SPELLCAST_START" then
if UnitExists("target") and UnitIsUnit("target", "player") then
local spellName = arg1
local duration = arg2
targetCLCast = {
spell = spellName,
startTime = GetTime(),
duration = (duration or 2000) / 1000,
icon = ResolveSelfIcon(spellName),
channel = false,
}
end
return
end
if event == "SPELLCAST_CHANNEL_START" then
if UnitExists("target") and UnitIsUnit("target", "player") then
local duration = arg1
local spellName = arg2
targetCLCast = {
spell = spellName,
startTime = GetTime(),
duration = (duration or 2000) / 1000,
icon = ResolveSelfIcon(spellName),
channel = true,
}
end
return
end
if event == "SPELLCAST_STOP" or event == "SPELLCAST_FAILED"
or event == "SPELLCAST_INTERRUPTED" or event == "SPELLCAST_CHANNEL_STOP" then
if UnitExists("target") and UnitIsUnit("target", "player") then
targetCLCast = nil
end
return
end
-- UNIT_CASTEVENT (SuperWoW / TurtleWoW): works for all units
if event == "UNIT_CASTEVENT" then
if not UnitGUID or not UnitExists("target") then return end
local targetGUID = UnitGUID("target")
if not targetGUID or arg1 ~= targetGUID then return end
if arg3 == "START" or arg3 == "CAST" or arg3 == "CHANNEL" then
local spell, icon
if SpellInfo and arg4 then
spell, _, icon = SpellInfo(arg4)
end
spell = spell or "Casting"
if not icon or icon == "" or icon == "Interface\\Icons\\INV_Misc_QuestionMark" then
icon = GetSpellIcon(spell) or icon
end
icon = icon or "Interface\\Icons\\INV_Misc_QuestionMark"
targetCLCast = {
spell = spell,
startTime = GetTime(),
duration = (arg5 or 2000) / 1000,
icon = icon,
channel = (arg3 == "CHANNEL"),
}
elseif arg3 == "FAIL" then
targetCLCast = nil
end
return
end
-- Combat log parsing: "X begins to cast Y" (third-person, all other units)
if not arg1 or not UnitExists("target") then return end
local targetName = UnitName("target")
if not targetName then return end
local msg = arg1
local caster, spell
local castStart = SPELLCASTOTHERSTART or "%s begins to cast %s."
caster, spell = CLMatch(msg, castStart)
if not caster then
local perfStart = SPELLPERFORMOTHERSTART or "%s begins to perform %s."
caster, spell = CLMatch(msg, perfStart)
end
if caster and caster == targetName and spell then
local icon = GetSpellIcon(spell) or "Interface\\Icons\\INV_Misc_QuestionMark"
targetCLCast = {
spell = spell,
startTime = GetTime(),
duration = 2.0,
icon = icon,
channel = false,
}
return
end
if targetCLCast then
local isFail = false
for u in string.gfind(msg, "(.+)'s .+ is interrupted%.") do
if u == targetName then isFail = true end
end
if not isFail then
for u in string.gfind(msg, "(.+)'s .+ fails%.") do
if u == targetName then isFail = true end
end
end
if not isFail and SPELLINTERRUPTOTHEROTHER then
local a = CLMatch(msg, SPELLINTERRUPTOTHEROTHER)
if a == targetName then isFail = true end
end
if isFail then targetCLCast = nil end
end
end)
end
function SFrames.Target:OnTargetChanged()
targetCLCast = nil
if UnitExists("target") then
self.frame:Show()
self:UpdateAll()
-- Force distance update immediately
if SFrames.Target.distanceFrame then
local dist = self:GetDistance("target")
SFrames.Target.distanceFrame.text:SetText(dist or "---")
@@ -1141,49 +1486,149 @@ function SFrames.Target:CastbarOnUpdate()
cb.ibg:Hide()
return
end
-- Try to read cast from Vanilla extensions (SuperWoW or TurtleWoW modern API, or ShaguTweaks)
local cast, nameSubtext, text, texture, startTime, endTime
local cast, texture, startTime, endTime, channel
-- 1) UnitCastingInfo / UnitChannelInfo (TurtleWoW / ShaguTweaks)
local _UnitCastingInfo = UnitCastingInfo or (ShaguTweaks and ShaguTweaks.UnitCastingInfo)
if _UnitCastingInfo then
cast, nameSubtext, text, texture, startTime, endTime = _UnitCastingInfo("target")
local c, _, _, tex, st, et = _UnitCastingInfo("target")
if c then
cast, texture, startTime, endTime = c, tex, st, et
end
end
local channel
local _UnitChannelInfo = UnitChannelInfo or (ShaguTweaks and ShaguTweaks.UnitChannelInfo)
if not cast and _UnitChannelInfo then
channel, nameSubtext, text, texture, startTime, endTime = _UnitChannelInfo("target")
cast = channel
if not cast then
local _UnitChannelInfo = UnitChannelInfo or (ShaguTweaks and ShaguTweaks.UnitChannelInfo)
if _UnitChannelInfo then
local c, _, _, tex, st, et = _UnitChannelInfo("target")
if c then
cast, texture, startTime, endTime = c, tex, st, et
channel = true
end
end
end
-- 2) SFrames.castdb (UNIT_CASTEVENT via Tweaks.lua, has SpellInfo icon)
if SFrames.castdb and UnitGUID then
local guid = UnitGUID("target")
if guid then
local entry = SFrames.castdb[guid]
if entry and entry.cast and entry.start and entry.casttime then
local elapsed = GetTime() - entry.start
local duration = entry.casttime / 1000
if elapsed < duration + 0.5 then
if not cast then
cast = entry.cast
startTime = entry.start * 1000
endTime = (entry.start + duration) * 1000
channel = entry.channel
end
if not texture or texture == "Interface\\Icons\\INV_Misc_QuestionMark" then
texture = entry.icon
end
end
end
end
end
-- 3) NanamiPlates castDB (GUID-based, UNIT_CASTEVENT with SpellInfo icon)
if NanamiPlates and NanamiPlates.castDB and UnitGUID then
local guid = UnitGUID("target")
if guid then
local entry = NanamiPlates.castDB[guid]
if entry and entry.spell and entry.startTime and entry.duration then
local elapsed = GetTime() - entry.startTime
local duration = entry.duration / 1000
if elapsed < duration + 0.5 then
if not cast then
cast = entry.spell
startTime = entry.startTime * 1000
endTime = (entry.startTime + duration) * 1000
channel = entry.channel
end
if not texture or texture == "Interface\\Icons\\INV_Misc_QuestionMark" then
texture = entry.icon
end
end
end
end
end
-- 4) NanamiPlates castTracker (name-based, combat log)
if not cast and NanamiPlates and NanamiPlates.castTracker then
local targetName = UnitName("target")
if targetName and NanamiPlates.castTracker[targetName] then
local entries = NanamiPlates.castTracker[targetName]
if entries and entries[1] then
local entry = entries[1]
if entry.spell and entry.startTime then
local duration = (entry.duration or 2000) / 1000
local elapsed = GetTime() - entry.startTime
if elapsed < duration + 0.5 then
cast = entry.spell
texture = entry.icon
startTime = entry.startTime * 1000
endTime = (entry.startTime + duration) * 1000
channel = false
end
end
end
end
end
-- 5) Local UNIT_CASTEVENT / SPELLCAST_* tracker
if targetCLCast then
local elapsed = GetTime() - targetCLCast.startTime
if elapsed < targetCLCast.duration + 0.5 then
if not cast then
cast = targetCLCast.spell
startTime = targetCLCast.startTime * 1000
endTime = (targetCLCast.startTime + targetCLCast.duration) * 1000
channel = targetCLCast.channel
end
if not texture or texture == "Interface\\Icons\\INV_Misc_QuestionMark" then
texture = targetCLCast.icon
end
else
targetCLCast = nil
end
end
if cast and startTime and endTime then
local duration = (endTime - startTime) / 1000
local cur = GetTime() - (startTime / 1000)
if channel then
cur = duration + (startTime / 1000) - GetTime()
end
if cur > duration then cur = duration end
if cur < 0 then cur = 0 end
cb:SetMinMaxValues(0, duration)
cb:SetValue(cur)
cb.text:SetText(cast)
if texture then
cb.icon:SetTexture(texture)
if not texture or texture == "Interface\\Icons\\INV_Misc_QuestionMark" then
texture = GetSpellIcon(cast) or texture
end
cb:SetAlpha(1)
cb.cbbg:SetAlpha(1)
cb.icon:SetAlpha(1)
cb.ibg:SetAlpha(1)
cb:Show()
cb.cbbg:Show()
cb.icon:Show()
cb.ibg:Show()
if texture then
cb.icon:SetTexture(texture)
cb.icon:SetAlpha(1)
cb.ibg:SetAlpha(1)
cb.icon:Show()
cb.ibg:Show()
else
cb.icon:Hide()
cb.ibg:Hide()
end
else
cb:Hide()
cb.cbbg:Hide()