diff --git a/init/libs.xml b/init/libs.xml index 47b82787..6c57da01 100644 --- a/init/libs.xml +++ b/init/libs.xml @@ -1,7 +1,6 @@ - diff --git a/libs/libcast.lua b/libs/libcast.lua deleted file mode 100644 index c34e05d6..00000000 --- a/libs/libcast.lua +++ /dev/null @@ -1,571 +0,0 @@ --- load pfUI environment -setfenv(1, pfUI:GetEnvironment()) - ---[[ libcast ]]-- --- A pfUI library that detects and saves all ongoing castbars of players, NPCs and enemies. --- The library also includes spells that usually don't have a castbar like Multi-Shot and Aimed Shot. --- This is exclusivly used for vanilla in order to provide pfGetChannelInfo and pfGetCastInfo functions. --- --- External functions: --- pfGetChannelInfo(unit) --- Returns information on the spell currently cast by the specified unit. --- Returns nil if no spell is being cast. --- --- cast[String] - The name of the spell, or nil if no spell is being cast. --- nameSubtext[String] - (DUMMY) The string describing the rank of the spell, e.g. "Rank 1". --- text[String] - The name to be displayed. --- texture[String] - The texture path associated with the spell. --- startTime[Number] - Specifies when casting has begun, in milliseconds. --- endTime[Number] - Specifies when casting will end, in milliseconds. --- isTradeSkill[Boolean] - (DUMMY) Specifies if the cast is a tradeskill --- --- pfGetCastInfo(unit) --- Returns information on the spell currently channeled by the specified unit. --- Returns nil if no spell is being channeled. --- --- cast[String] - The name of the spell, or nil if no spell is being cast. --- nameSubtext[String] - (DUMMY) The string describing the rank of the spell, e.g. "Rank 1". --- text[String] - The name to be displayed. --- texture[String] - The texture path associated with the spell. --- startTime[Number] - Specifies when casting has begun, in milliseconds. --- endTime[Number] - Specifies when casting will end, in milliseconds. --- isTradeSkill[Boolean] - (DUMMY) Specifies if the cast is a tradeskill --- --- Internal functions: --- libcast:AddAction(mob, spell, channel) --- Adds a spell to the database by using pfUI's spell database --- to obtain durations and icons --- --- libcast:RemoveAction(mob, spell) --- Removes the castbar of a given mob, if `spell` is an interrupt. --- spell can be set to "INTERRUPT" to force remove an action. --- - --- return instantly when another libcast is already active -if pfUI.api.libcast then return end - -local lastcasttex, lastrank, _ - -local libcast = CreateFrame("Frame", "pfEnemyCast") -local player = UnitName("player") - -pfGetChannelInfo = function(unit) - -- convert to name if unitstring was given - local unitName = pfValidUnits[unit] and UnitName(unit) or unit - - -- Get GUID if Nampower is available - local guid = nil - - -- Check if unit itself is a GUID (starts with "0x") - if type(unit) == "string" and string.sub(unit, 1, 2) == "0x" then - guid = unit -- unit IS the GUID - elseif pfValidUnits[unit] and UnitExists then - -- unit is a token like "target" - get GUID from it - local unitGuid = UnitGUID(unit) - guid = unitGuid - end - - -- For player: ALWAYS use libcast.db because it handles channel updates correctly - local isPlayer = unit == "player" or unitName == player - - if isPlayer then - local cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill - local db = libcast.db[player] - - if db and db.cast and db.start + db.casttime / 1000 > GetTime() then - if not db.channel then return end - cast = db.cast - nameSubtext = db.rank - text = "" - texture = db.icon - startTime = db.start * 1000 - endTime = startTime + db.casttime - isTradeSkill = nil - elseif db then - db.cast = nil - db.rank = nil - db.start = nil - db.casttime = nil - db.icon = nil - db.channel = nil - end - - return cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill - end - - -- For non-player units: use libdebuff GUID-based tracking or libcast.db - -- Try GUID-based lookup first (from libdebuff's SPELL_START tracking) - local db = nil - if guid and pfUI.libdebuff_casts and pfUI.libdebuff_casts[guid] then - -- Use libdebuff's cast tracking (from SPELL_START_OTHER events) - local castData = pfUI.libdebuff_casts[guid] - if castData.event == "START" and castData.endTime and castData.endTime > GetTime() then - -- Convert libdebuff format to libcast format - db = { - cast = castData.spellName, - rank = nil, - start = castData.startTime, - casttime = castData.duration * 1000, -- Convert back to ms - icon = castData.icon, - channel = nil -- TODO: libdebuff should distinguish channel vs cast - } - end - end - - -- Fallback to name-based lookup (CHAT_MSG castbars) - -- Skip when GUID is available to avoid same-name mob bleed - if not db and not guid and libcast.db[unitName] then - db = libcast.db[unitName] - end - - -- Fallback to libcast.db for non-player units - local cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill - - if db and db.cast and db.start + db.casttime / 1000 > GetTime() then - if not db.channel then return end - cast = db.cast - nameSubtext = db.rank - text = "" - texture = db.icon - startTime = db.start * 1000 - endTime = startTime + db.casttime - isTradeSkill = nil - elseif db then - db.cast = nil - db.rank = nil - db.start = nil - db.casttime = nil - db.icon = nil - db.channel = nil - end - - return cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill -end - -pfGetCastInfo = function(unit) - -- convert to name if unitstring was given - local unitName = pfValidUnits[unit] and UnitName(unit) or unit - - -- Get GUID if Nampower is available - local guid = nil - - -- Check if unit itself is a GUID (starts with "0x") - if type(unit) == "string" and string.sub(unit, 1, 2) == "0x" then - guid = unit -- unit IS the GUID - elseif pfValidUnits[unit] and UnitExists then - -- unit is a token like "target" - get GUID from it - local unitGuid = UnitGUID(unit) - guid = unitGuid - end - - -- For player: ALWAYS use libcast.db because it handles pushback correctly - local isPlayer = unit == "player" or unitName == player - - if isPlayer then - local cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill - local db = libcast.db[player] - - if db and db.cast and db.start + db.casttime / 1000 > GetTime() then - if db.channel then return end - cast = db.cast - nameSubtext = db.rank or "" - text = "" - texture = db.icon - startTime = db.start * 1000 - endTime = startTime + db.casttime - isTradeSkill = nil - elseif db then - db.cast = nil - db.rank = nil - db.start = nil - db.casttime = nil - db.icon = nil - db.channel = nil - end - - return cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill - end - - -- For non-player units: use libdebuff GUID-based tracking or libcast.db - -- Try GUID-based lookup first (from libdebuff's SPELL_START tracking) - local db = nil - if guid and pfUI.libdebuff_casts and pfUI.libdebuff_casts[guid] then - -- Use libdebuff's cast tracking (from SPELL_START_OTHER events) - local castData = pfUI.libdebuff_casts[guid] - if castData.event == "START" and castData.endTime and castData.endTime > GetTime() then - -- Convert libdebuff format to libcast format - db = { - cast = castData.spellName, - rank = nil, - start = castData.startTime, - casttime = castData.duration * 1000, -- Convert back to ms - icon = castData.icon, - channel = nil -- TODO: libdebuff should distinguish channel vs cast - } - end - end - - -- Fallback to name-based lookup (CHAT_MSG castbars) - -- Skip when GUID is available to avoid same-name mob bleed - if not db and not guid and libcast.db[unitName] then - db = libcast.db[unitName] - end - - -- Fallback to libcast.db for non-player units - local cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill - - if db and db.cast and db.start + db.casttime / 1000 > GetTime() then - if db.channel then return end - cast = db.cast - nameSubtext = db.rank or "" - text = "" - texture = db.icon - startTime = db.start * 1000 - endTime = startTime + db.casttime - isTradeSkill = nil - elseif db then - db.cast = nil - db.rank = nil - db.start = nil - db.casttime = nil - db.icon = nil - db.channel = nil - end - - return cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill -end - -function libcast:AddAction(mob, spell, channel) - if not mob or not spell then return nil end - - if L["spells"][spell] ~= nil then - local casttime = L["spells"][spell].t - local icon = L["spells"][spell].icon and string.format("%s%s", "Interface\\Icons\\", L["spells"][spell].icon) or nil - - -- add cast action to the database - if not self.db[mob] then self.db[mob] = {} end - self.db[mob].cast = spell - self.db[mob].rank = nil - self.db[mob].start = GetTime() - self.db[mob].casttime = casttime - self.db[mob].icon = icon - self.db[mob].channel = channel - - return true - end - - return nil -end - -function libcast:RemoveAction(mob, spell) - if self.db[mob] and ( L["interrupts"][spell] ~= nil or spell == "INTERRUPT" ) then - - -- remove cast action to the database - self.db[mob].cast = nil - self.db[mob].rank = nil - self.db[mob].start = nil - self.db[mob].casttime = nil - self.db[mob].icon = nil - self.db[mob].channel = nil - end -end - --- main data -libcast.db = { [player] = {} } - --- environmental casts -libcast:RegisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE") -libcast:RegisterEvent("CHAT_MSG_SPELL_HOSTILEPLAYER_DAMAGE") -libcast:RegisterEvent("CHAT_MSG_SPELL_HOSTILEPLAYER_BUFF") -libcast:RegisterEvent("CHAT_MSG_SPELL_FRIENDLYPLAYER_DAMAGE") -libcast:RegisterEvent("CHAT_MSG_SPELL_FRIENDLYPLAYER_BUFF") -libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_BUFFS") -libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_BUFFS") -libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE") -libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_DAMAGE") -libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_SELF_DAMAGE") -libcast:RegisterEvent("CHAT_MSG_SPELL_PARTY_DAMAGE") -libcast:RegisterEvent("CHAT_MSG_SPELL_PARTY_BUFF") -libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_PARTY_DAMAGE") -libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_PARTY_BUFFS") -libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE") -libcast:RegisterEvent("CHAT_MSG_SPELL_PERIODIC_CREATURE_BUFFS") -libcast:RegisterEvent("CHAT_MSG_SPELL_CREATURE_VS_CREATURE_DAMAGE") -libcast:RegisterEvent("CHAT_MSG_SPELL_CREATURE_VS_CREATURE_BUFF") - --- player spells -libcast:RegisterEvent("SPELLCAST_START") -libcast:RegisterEvent("SPELLCAST_STOP") -libcast:RegisterEvent("SPELLCAST_FAILED") -libcast:RegisterEvent("SPELLCAST_INTERRUPTED") -libcast:RegisterEvent("SPELLCAST_DELAYED") -libcast:RegisterEvent("SPELLCAST_CHANNEL_START") -libcast:RegisterEvent("SPELLCAST_CHANNEL_STOP") -libcast:RegisterEvent("SPELLCAST_CHANNEL_UPDATE") - -local mob, spell, icon, _ -local lastSpellId = nil -- spellId cached from SPELL_START_SELF (Nampower) - -libcast:SetScript("OnEvent", function() - -- Fill database with player casts - if event == "SPELLCAST_START" then - -- Get icon via spellId cached from SPELL_START_SELF - icon = lastSpellId and C_Spell.GetSpellTexture(lastSpellId) or nil - -- fallback to L["spells"] / lastcasttex if no icon resolved - if not icon then - icon = L["spells"][arg1] and L["spells"][arg1].icon and string.format("%s%s", "Interface\\Icons\\", L["spells"][arg1].icon) or lastcasttex - end - lastSpellId = nil - - this.db[player].cast = arg1 - this.db[player].rank = lastrank - this.db[player].start = GetTime() - this.db[player].casttime = arg2 - this.db[player].icon = icon - this.db[player].channel = nil - - if not L["spells"][arg1] or not L["spells"][arg1].icon or not L["spells"][arg1].t then - L["spells"][arg1] = L["spells"][arg1] or { } - L["spells"][arg1].icon = L["spells"][arg1].icon or icon - L["spells"][arg1].t = L["spells"][arg1].t or arg2 - end - lastcasttex, lastrank = nil, nil - elseif event == "SPELLCAST_STOP" or event == "SPELLCAST_FAILED" or event == "SPELLCAST_INTERRUPTED" then - lastSpellId = nil - if this.db[player] and not this.db[player].channel then - -- remove cast action to the database - this.db[player].cast = nil - this.db[player].rank = nil - this.db[player].rank = nil - this.db[player].start = nil - this.db[player].casttime = nil - this.db[player].icon = nil - this.db[player].channel = nil - else - lastcasttex, lastrank = nil, nil - end - elseif event == "SPELLCAST_DELAYED" then - if this.db[player].cast then - -- Pushback: increase casttime instead of shifting start - -- arg1 is the delay amount in milliseconds - this.db[player].casttime = this.db[player].casttime + arg1 - end - elseif event == "SPELLCAST_CHANNEL_START" then - -- add cast action to the database - this.db[player].cast = arg2 - this.db[player].rank = lastrank - this.db[player].start = GetTime() - this.db[player].casttime = arg1 - this.db[player].icon = L["spells"][arg2] and L["spells"][arg2].icon and string.format("%s%s", "Interface\\Icons\\", L["spells"][arg2].icon) or lastcasttex - this.db[player].channel = true - - lastcasttex, lastrank = nil, nil - elseif event == "SPELLCAST_CHANNEL_STOP" then - if this.db[player] and this.db[player].channel then - -- remove cast action to the database - this.db[player].cast = nil - this.db[player].rank = nil - this.db[player].start = nil - this.db[player].casttime = nil - this.db[player].icon = nil - this.db[player].channel = nil - end - elseif event == "SPELLCAST_CHANNEL_UPDATE" then - if this.db[player].cast then - this.db[player].start = -this.db[player].casttime/1000 + GetTime() + arg1/1000 - end - -- Fill database with environmental casts - elseif arg1 then - -- (.+) begins to cast (.+). - mob, spell = cmatch(arg1, SPELLCASTOTHERSTART) - if libcast:AddAction(mob, spell) then return end - - -- (.+) begins to perform (.+). - mob, spell = cmatch(arg1, SPELLPERFORMOTHERSTART) - if libcast:AddAction(mob, spell) then return end - - -- (.+) gains (.+). - mob, spell = cmatch(arg1, AURAADDEDOTHERHELPFUL) - if libcast:RemoveAction(mob, spell) then return end - - -- (.+) is afflicted by (.+). - mob, spell = cmatch(arg1, AURAADDEDOTHERHARMFUL) - if libcast:RemoveAction(mob, spell) then return end - - -- Your (.+) hits (.+) for (%d+). - spell, mob = cmatch(arg1, SPELLLOGSELFOTHER) - if libcast:RemoveAction(mob, spell) then return end - - -- Your (.+) crits (.+) for (%d+). - spell, mob = cmatch(arg1, SPELLLOGCRITSELFOTHER) - if libcast:RemoveAction(mob, spell) then return end - - -- (.+)'s (.+) %a hits (.+) for (%d+). - _, spell, mob = cmatch(arg1, SPELLLOGOTHEROTHER) - if libcast:RemoveAction(mob, spell) then return end - - -- (.+)'s (.+) %a crits (.+) for (%d+). - _, spell, mob = cmatch(arg1, SPELLLOGCRITOTHEROTHER) - if libcast:RemoveAction(mob, spell) then return end - - -- You interrupt (.+)'s (.+). - mob, _ = cmatch(arg1, SPELLINTERRUPTSELFOTHER) - if libcast:RemoveAction(mob, "INTERRUPT") then return end - - -- (.+) interrupts (.+)'s (.+). - _, mob, _ = cmatch(arg1, SPELLINTERRUPTOTHEROTHER) - if libcast:RemoveAction(mob, "INTERRUPT") then return end - end -end) - ---[[ Custom Casts - Enable Castbars for spells that don't have a castbar by default - (e.g Multi-Shot and Aimed Shot) -]]-- -local aimedshot = L["customcast"]["AIMEDSHOT"] -local multishot = L["customcast"]["MULTISHOT"] - --- Shot-timer haste lookup. Keyed by icon path because some of these buffs have --- spell-ID variants across server flavors but stable icons. Berserking is the --- variable Troll-racial calculation (more haste at lower HP); other entries are --- flat duration multipliers. -local SHOT_HASTE = { - ["Interface\\Icons\\Racial_Troll_Berserk"] = "berserking", - ["Interface\\Icons\\Ability_Hunter_RunningShot"] = 1.4, - ["Interface\\Icons\\Ability_Warrior_InnerRage"] = 1.3, - ["Interface\\Icons\\Inv_Trinket_Naxxramas04"] = 1.2, -} - -function libcast.ApplyShotHaste(duration) - for _, a in ipairs(C_UnitAuras.GetUnitAuras("player", "HELPFUL")) do - local mult = SHOT_HASTE[a.icon] - if mult == "berserking" then - local hp = UnitHealth("player") / UnitHealthMax("player") - local berserk = hp >= 0.40 and (1.30 - hp) / 3 or 0.3 - duration = duration / (1 + berserk) - elseif mult then - duration = duration / mult - end - end - return duration -end - -libcast.customcast = {} -libcast.customcast[strlower(aimedshot)] = function(begin, duration) - if begin then - local duration = libcast.ApplyShotHaste(duration or 3000) - - local _,_, lag = GetNetStats() - local start = GetTime() + lag/1000 - - -- add cast action to the database - libcast.db[player].cast = aimedshot - libcast.db[player].rank = lastrank - libcast.db[player].start = start - libcast.db[player].casttime = duration - libcast.db[player].icon = "Interface\\Icons\\Inv_spear_07" - libcast.db[player].channel = nil - else - -- remove cast action to the database - libcast.db[player].cast = nil - libcast.db[player].rank = nil - libcast.db[player].start = nil - libcast.db[player].casttime = nil - libcast.db[player].icon = nil - libcast.db[player].channel = nil - end -end - -libcast.customcast[strlower(multishot)] = function(begin, duration) - if begin then - local duration = libcast.ApplyShotHaste(duration or 500) - - local _,_, lag = GetNetStats() - local start = GetTime() + lag/1000 - - -- add cast action to the database - libcast.db[player].cast = multishot - libcast.db[player].rank = lastrank - libcast.db[player].start = start - libcast.db[player].casttime = duration - libcast.db[player].icon = "Interface\\Icons\\Ability_upgrademoonglaive" - libcast.db[player].channel = nil - else - -- remove cast action to the database - libcast.db[player].cast = nil - libcast.db[player].rank = nil - libcast.db[player].start = nil - libcast.db[player].casttime = nil - libcast.db[player].icon = nil - libcast.db[player].channel = nil - end -end - -local function CastCustom(id, bookType, rawSpellName, rank, texture, castingTime) - if not id or not rawSpellName then return end -- ignore if the spell is not found - if not castingTime or castingTime == 0 then - -- instant-cast: clear lastcasttex so next cast doesn't inherit this icon - lastcasttex, lastrank = nil, nil - return - end - - lastrank = rank - lastcasttex = texture - - local func = libcast.customcast[strlower(rawSpellName)] - if not func then return end - - if GetSpellCooldown(id, bookType) == 0 or pfGetCastInfo(player) then return end -- detect casting - - func(true) -end - -hooksecurefunc("UseContainerItem", function(id, index) - lastcasttex = GetContainerItemInfo(id, index) -end) - -hooksecurefunc("CastSpell", function(id, bookType) - local cachedRawSpellName, cachedRank, cachedTexture, cachedCastingTime, _, _, cachedSpellId, cachedBookType = libspell.GetSpellInfo(id, bookType) - - CastCustom(cachedSpellId, cachedBookType, cachedRawSpellName, cachedRank, cachedTexture, cachedCastingTime) -end) - -hooksecurefunc("CastSpellByName", function(spellCasted, target) - local cachedRawSpellName, cachedRank, cachedTexture, cachedCastingTime, _, _, cachedSpellId, cachedBookType = libspell.GetSpellInfo(spellCasted) - - CastCustom(cachedSpellId, cachedBookType, cachedRawSpellName, cachedRank, cachedTexture, cachedCastingTime) -end) - -hooksecurefunc("UseAction", function(slot, target, button) - if not IsCurrentAction(slot) then return end - - -- Resolve action slot → spellID (handles both spell and macro actions), - -- then resolve to spellbook slot for libspell. GetMacroSpell returns the - -- highest rank the player actually knows, so FindSpellBookSlotByID's - -- "spell must be in spellbook" requirement is satisfied. - local kind, id = GetActionInfo(slot) - local spellID - if kind == "spell" then - spellID = id - elseif kind == "macro" then - local _, _, sid = GetMacroSpell(id) - spellID = sid - end - if not spellID then return end - - local sbSlot = FindSpellBookSlotByID(spellID) - if not sbSlot then return end - - local cachedRawSpellName, cachedRank, cachedTexture, cachedCastingTime, _, _, cachedSpellId, cachedBookType = libspell.GetSpellInfo(sbSlot, BOOKTYPE_SPELL) - - CastCustom(cachedSpellId, cachedBookType, cachedRawSpellName, cachedRank, cachedTexture, cachedCastingTime) -end) - --- Cache spellId from SPELL_START_SELF so SPELLCAST_START can use it for icon lookup -pfUI.libdebuff_spell_start_self_hooks = pfUI.libdebuff_spell_start_self_hooks or {} -pfUI.libdebuff_spell_start_self_hooks["libcast_icon"] = function(spellId) - lastSpellId = spellId -end - --- add libcast to pfUI API -pfUI.api.libcast = libcast \ No newline at end of file diff --git a/libs/libdebuff.lua b/libs/libdebuff.lua index ba79d932..f4e51009 100644 --- a/libs/libdebuff.lua +++ b/libs/libdebuff.lua @@ -15,7 +15,8 @@ setfenv(1, pfUI:GetEnvironment()) -- of ClassicAPI's C_UnitAuras (which now provides sourceUnit/sourceGUID and -- non-player expirationTime). What remains in libdebuff is the cast-event -- bookkeeping consumed by GetBestAuraCast / GetEnhancedDebuffs and the --- libdebuff_casts / libdebuff_*_hooks broadcast surface. +-- libdebuff_*_hooks broadcast surface (subscribers in actionbar / swingtimer +-- / libtotem react to SPELL_GO and SPELL_FAILED). -- return instantly when another libdebuff is already active if pfUI.api.libdebuff then return end @@ -144,8 +145,6 @@ local iconCache = pfUI.libdebuff_icon_cache -- Cast Tracking: [casterGuid] = {spellID, spellName, icon, startTime, duration, endTime} -- Shared with nameplates for cast-bar display -pfUI.libdebuff_casts = pfUI.libdebuff_casts or {} -pfUI.libdebuff_item_icons = pfUI.libdebuff_item_icons or {} -- [casterGuid] = icon (persists across SPELL_GO) -- Cleveroids API: [targetGUID][spellID] = {start, duration, caster, stacks} pfUI.libdebuff_objects_guid = pfUI.libdebuff_objects_guid or {} @@ -965,23 +964,19 @@ if hasNampower then return elseif event == "SPELLCAST_CHANNEL_STOP" then - -- Channel interrupted by player - clear ownDebuffs for the channeled spell immediately. - -- DEBUFF_REMOVED fires later (0.5-1s server lag), causing phantom debuff display. - -- We look up the active channel cast and pre-clear ownDebuffs for its target. - local myGuid = GetPlayerGUID() - local castData = myGuid and pfUI.libdebuff_casts[myGuid] - if castData and castData.event == "CHANNEL" and castData.spellName then - local spellName = castData.spellName + -- Channel interrupted by player - clear ownDebuffs for the channeled spell + -- via C_Spell.ChannelInfo. DEBUFF_REMOVED fires later (0.5-1s server lag), + -- causing phantom debuff display without this pre-clear. + local spellName = select(1, C_Spell.ChannelInfo()) + if spellName then local targetGuid = UnitGUID and UnitGUID("target") if targetGuid and ownDebuffs[targetGuid] and ownDebuffs[targetGuid][spellName] then local data = ownDebuffs[targetGuid][spellName] - -- Only clear if the timer is still active (not already expired naturally) local remaining = (data.startTime + data.duration) - GetTime() if remaining > 0 then ownDebuffs[targetGuid][spellName] = nil end end - pfUI.libdebuff_casts[myGuid] = nil end elseif event == "PLAYER_ENTERING_WORLD" then @@ -1003,48 +998,15 @@ if hasNampower then end elseif event == "SPELL_START_SELF" or event == "SPELL_START_OTHER" then - local itemId = arg1 local spellId = arg2 local casterGuid = arg3 - local spellType = arg8 or 0 -- 0=Normal, 1=Channel, 2=Autorepeating - -- arg6=castTime, arg7=channel duration - -- prefer arg6 if present — some spells (e.g. Volley post-rework) still send - -- arg8=1 but now have a real cast time in arg6, so we only fall back to arg7 - -- when arg6 is nil (true channels like Blizzard) - -- arg6=castTime (ms), arg7=channel duration (ms), arg8=spellType - -- For channels: arg6=0 (no cast time), arg7=duration, spellType=1 - -- For normal casts: arg6=castTime, arg7=0, spellType=0 - -- Note: "not arg6" is wrong in Lua since 0 is truthy - use arg6 == 0 or nil + -- arg6=castTime (ms), arg7=channel duration (ms). Prefer arg6 when set — + -- some spells (e.g. post-rework Volley) flag as channel via arg8 but ship a + -- real cast time in arg6, so we only fall back to arg7 for true channels + -- like Blizzard where arg6 is 0/nil. local castTime = (arg6 and arg6 > 0) and arg6 or arg7 - local isChannel = spellType == 1 and (not arg6 or arg6 == 0) - if not casterGuid or not spellId then return end - local spellName = C_Spell.GetSpellName(spellId) - local icon = libdebuff:GetSpellIcon(spellId) - - -- Use item icon for item-triggered casts - if itemId and itemId > 0 then - icon = C_Item.GetItemIconByID(itemId) or icon - pfUI.libdebuff_item_icons[casterGuid] = { - icon = icon, - name = GetItemInfo(itemId), - } - else - pfUI.libdebuff_item_icons[casterGuid] = nil - end - - pfUI.libdebuff_casts[casterGuid] = { - spellID = spellId, - itemID = itemId and itemId > 0 and itemId or nil, - spellName = spellName, - icon = icon, - startTime = GetTime(), - duration = castTime and castTime / 1000 or 0, - endTime = castTime and (GetTime() + castTime / 1000) or nil, - event = isChannel and "CHANNEL" or "START" - } - if event == "SPELL_START_SELF" and pfUI.libdebuff_spell_start_self_hooks then for _, fn in pairs(pfUI.libdebuff_spell_start_self_hooks) do fn(spellId, casterGuid, arg4, castTime) @@ -1063,15 +1025,6 @@ if hasNampower then local numHit = arg6 or 0 local numMissed = arg7 or 0 - -- Clear cast bar only if SPELL_GO matches the active cast - -- (Reactive procs like Frost Armor trigger SPELL_GO but shouldn't clear the castbar) - -- Don't clear channels on SPELL_GO - channels persist until duration expires or SPELL_FAILED - if casterGuid and pfUI.libdebuff_casts[casterGuid] then - if pfUI.libdebuff_casts[casterGuid].spellID == spellId and pfUI.libdebuff_casts[casterGuid].event ~= "CHANNEL" then - pfUI.libdebuff_casts[casterGuid] = nil - end - end - -- Fire registered SPELL_GO_SELF hooks BEFORE miss guard -- (Swingtimer needs to see ALL casts, even misses, for swing reset) if event == "SPELL_GO_SELF" and pfUI.libdebuff_spell_go_hooks then @@ -1195,14 +1148,6 @@ if hasNampower then elseif event == "SPELL_FAILED_OTHER" then local casterGuid = arg1 - local spellId = arg2 - - if casterGuid and pfUI.libdebuff_casts[casterGuid] then - -- Only clear if spellID matches to avoid clearing a cast that already moved on - if pfUI.libdebuff_casts[casterGuid].spellID == spellId then - pfUI.libdebuff_casts[casterGuid] = nil - end - end if pfUI.libdebuff_spell_failed_other_hooks then for _, fn in pairs(pfUI.libdebuff_spell_failed_other_hooks) do fn(casterGuid, arg2) diff --git a/libs/libpredict.lua b/libs/libpredict.lua index 1b081a21..40ecc904 100644 --- a/libs/libpredict.lua +++ b/libs/libpredict.lua @@ -546,30 +546,47 @@ function libpredict:ParseComm(sender, msg) rank = tonumber(rankStr) end end - elseif select and pfGetCastInfo then + elseif select then -- latest healcomm msgtype = tonumber(string.sub(msg, 1, 3)) if not msgtype then return end + -- Resolve sender's name to a group unit token so C_Spell can query + -- the cast. Group rosters are tiny (44 slots max) so the walk is cheap + -- relative to the cost of receiving a HealComm message. + local function senderUnit() + if UnitName("player") == sender then return "player" end + for i = 1, GetNumPartyMembers() do + if UnitName("party"..i) == sender then return "party"..i end + end + for i = 1, GetNumRaidMembers() do + if UnitName("raid"..i) == sender then return "raid"..i end + end + end + if msgtype == 0 then msgtype = "Heal" heal = tonumber(string.sub(msg, 4, 8)) target = string.sub(msg,9, -1) - local starttime = select(5, pfGetCastInfo(sender)) - local endtime = select(6, pfGetCastInfo(sender)) - if not starttime or not endtime then return end - time = endtime - starttime + local unit = senderUnit() + if not unit then return end + local startMs = select(4, C_Spell.UnitCastingInfo(unit)) + local endMs = select(5, C_Spell.UnitCastingInfo(unit)) + if not startMs or not endMs then return end + time = (endMs - startMs) / 1000 elseif msgtype == 1 then msgtype = "Stop" elseif msgtype == 2 then msgtype = "Heal" heal = tonumber(string.sub(msg,4, 8)) target = {strsplit(":", string.sub(msg,9, -1))} - local starttime = select(5, pfGetCastInfo(sender)) - local endtime = select(6, pfGetCastInfo(sender)) - if not starttime or not endtime then return end - time = endtime - starttime + local unit = senderUnit() + if not unit then return end + local startMs = select(4, C_Spell.UnitCastingInfo(unit)) + local endMs = select(5, C_Spell.UnitCastingInfo(unit)) + if not startMs or not endMs then return end + time = (endMs - startMs) / 1000 end end end diff --git a/modules/afkcam.lua b/modules/afkcam.lua index df911f9c..069a9220 100644 --- a/modules/afkcam.lua +++ b/modules/afkcam.lua @@ -151,9 +151,7 @@ pfUI:RegisterModule("afkcam", function () delay:SetScript("OnUpdate", function() if ( this.tick or 0) > GetTime() then return else this.tick = GetTime() + 1 end - local name = UnitName("player") - local cast = pfGetCastInfo(name) - if not cast then cast = pfGetChannelInfo(name) end + local cast = C_Spell.UnitCastingInfo("player") or C_Spell.UnitChannelInfo("player") if not this.delay then this.delay = 0 end if cast then diff --git a/modules/castbar.lua b/modules/castbar.lua index c01509bb..6c1cd010 100644 --- a/modules/castbar.lua +++ b/modules/castbar.lua @@ -103,60 +103,37 @@ pfUI:RegisterModule("castbar", function () local query = this.unitstr ~= "" and this.unitstr or this.unitname if not query then return end - -- Check if we have a GUID-based focus (Turtle WoW native GUID) - local focusGuid = nil - if this.unitstr and string.find(this.unitstr, "^0x") then - focusGuid = this.unitstr - elseif this.unitstr and this.unitstr == "player" and UnitGUID then - focusGuid = UnitGUID("player") - elseif this.unitstr and this.unitstr ~= "player" then - local guid = UnitGUID(this.unitstr) - if guid then focusGuid = guid end + -- C_Spell takes a unit token; ClassicAPI's resolver also accepts GUID + -- strings, so this.unitstr can be "player" / "target" / "focus" or a + -- raw "0x..." GUID transparently. + local cast, nameSubtext, texture, startTime, endTime + local name, _, tex, startMs, endMs, _, _, _, spellID = C_Spell.UnitCastingInfo(query) + local isChan + if not name then + name, _, tex, startMs, endMs, _, _, spellID = C_Spell.UnitChannelInfo(query) + isChan = true end - this.focusGuid = focusGuid - - -- Try libdebuff_casts first for GUID-based units (works with Turtle GUID + Nampower events) - local cast, nameSubtext, text, texture, startTime, endTime - local castBlocked = false - if focusGuid and pfUI.libdebuff_casts and pfUI.libdebuff_casts[focusGuid] then - local castData = pfUI.libdebuff_casts[focusGuid] - if castData.event == "CAST" or castData.event == "FAIL" then - castBlocked = true - pfUI.libdebuff_casts[focusGuid] = nil - elseif (castData.event == "START" or castData.event == "CHANNEL") and castData.endTime and castData.endTime > GetTime() then - cast = castData.spellName - texture = castData.icon - startTime = castData.startTime * 1000 - endTime = castData.endTime * 1000 - -- Try to get rank from spell DB via spellID (nameSubtext is not stored in libdebuff_casts) - if castData.spellID and GetSpellRecField then - nameSubtext = GetSpellRecField(castData.spellID, "rank") or "" - else - nameSubtext = "" - end - if castData.event == "CHANNEL" then - channel = cast - end + -- Synthetic fallback for abilities the engine treats as instant-cast but + -- that have a meaningful wait window (e.g. Turtle WoW Steady Shot on the + -- ranged-swing queue). Per-unit table populated by module-side hooks. + if not name and pfUI.synthetic_casts and pfUI.synthetic_casts[query] then + local s = pfUI.synthetic_casts[query] + if s.endMs > GetTime() * 1000 then + name, tex, startMs, endMs, spellID = s.name, s.icon, s.startMs, s.endMs, s.spellID + isChan = nil end end - - local useLibcastForPlayer = this.unitstr == "player" - - -- For player: use player name to query libcast.db directly - if not cast and useLibcastForPlayer then - query = UnitName("player") - end - - -- Fallback: pfGetCastInfo only when no focusGuid (Nampower not available for this unit). - -- If we have a focusGuid, libdebuff is authoritative - don't fall back to libcast - -- even if no cast is active (prevents false positives e.g. spellbook clicks on CD spells). - if not cast and not castBlocked and not focusGuid and pfGetCastInfo then - cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill = pfGetCastInfo(query) - end - - if not cast and not castBlocked and not focusGuid and pfGetChannelInfo then - channel, nameSubtext, text, texture, startTime, endTime, isTradeSkill = pfGetChannelInfo(this.unitstr or this.unitname) - cast = channel + if name and startMs and endMs then + cast = name + texture = tex + startTime = startMs + endTime = endMs + if spellID and GetSpellRecField then + nameSubtext = GetSpellRecField(spellID, "rank") or "" + else + nameSubtext = "" + end + if isChan then channel = cast end end if cast then @@ -181,35 +158,8 @@ pfUI:RegisterModule("castbar", function () this.icon:Show() this.icon:SetHeight(size) this.icon:SetWidth(size) - - -- Override with item icon from libdebuff_casts or persistent item icon cache - local useTexture = texture - local useItemName = nil - if pfUI.libdebuff_casts or pfUI.libdebuff_item_icons then - local castGuid = nil - if this.unitstr and UnitExists then - local guid = UnitGUID(this.unitstr) - castGuid = guid - end - if castGuid then - -- First check active cast data - if pfUI.libdebuff_casts and pfUI.libdebuff_casts[castGuid] and pfUI.libdebuff_casts[castGuid].itemID then - useTexture = pfUI.libdebuff_casts[castGuid].icon or texture - -- Fallback to persistent item icon cache - elseif pfUI.libdebuff_item_icons and pfUI.libdebuff_item_icons[castGuid] then - useTexture = pfUI.libdebuff_item_icons[castGuid].icon or texture - useItemName = pfUI.libdebuff_item_icons[castGuid].name - end - end - end - - this.icon.texture:SetTexture(useTexture) + this.icon.texture:SetTexture(texture) this.bar:SetPoint("TOPLEFT", this.icon, "TOPRIGHT", this.spacing, 0) - - -- Override spell name with item name for item-triggered casts - if useItemName and this.showname then - this.bar.left:SetText(useItemName .. " " .. rank) - end else this.bar:SetPoint("TOPLEFT", this, 0, 0) this.icon:Hide() @@ -255,7 +205,6 @@ pfUI:RegisterModule("castbar", function () this.lastMax = nil this.fadeout = 1 this.delay = 0 - this.itemIconApplied = nil end end) @@ -267,10 +216,6 @@ pfUI:RegisterModule("castbar", function () if not delayMs or delayMs <= 0 or not this.endTime then return end this.delay = (this.delay or 0) + delayMs / 1000 this.endTime = this.endTime + delayMs - local focusGuid = this.focusGuid - if focusGuid and pfUI.libdebuff_casts and pfUI.libdebuff_casts[focusGuid] then - pfUI.libdebuff_casts[focusGuid].endTime = this.endTime / 1000 - end end cb:RegisterEvent("SPELL_DELAYED_SELF") @@ -299,10 +244,6 @@ pfUI:RegisterModule("castbar", function () if diff > 50 then this.delay = (this.delay or 0) + diff / 1000 this.endTime = newEndTime - local focusGuid = this.focusGuid - if focusGuid and pfUI.libdebuff_casts and pfUI.libdebuff_casts[focusGuid] then - pfUI.libdebuff_casts[focusGuid].endTime = newEndTime / 1000 - end end elseif event == CASTBAR_EVENT_CAST_START or event == CASTBAR_EVENT_CHANNEL_START then diff --git a/modules/nameplates.lua b/modules/nameplates.lua index 5579e1c2..a6b76ca2 100644 --- a/modules/nameplates.lua +++ b/modules/nameplates.lua @@ -3,8 +3,6 @@ pfUI:RegisterModule("nameplates", function () pcall(SetCVar, "ShowVKeyCastbar", 0) -- Local function references for performance - local pfGetCastInfo = pfGetCastInfo -- provided by libcast for vanilla - local pfGetChannelInfo = pfGetChannelInfo -- provided by libcast for vanilla local GetTime = GetTime local UnitExists = UnitExists local UnitName = UnitName @@ -65,9 +63,30 @@ pfUI:RegisterModule("nameplates", function () local raidGuidCache = {} -- guid -> name (rebuilt on RAID_ROSTER_UPDATE/PARTY_MEMBERS_CHANGED) - -- Helper function to safely access libdebuff cast data + -- Resolve a plate GUID to its cast/channel info via C_Spell. Returns a + -- compact struct (spellName / icon / startTime / endTime / duration / + -- isChannel) or nil when the unit isn't casting / the GUID can't map to a + -- live token. local function GetCastInfo(guid) - return pfUI.libdebuff_casts and pfUI.libdebuff_casts[guid] + if not guid then return nil end + local unit = UnitTokenFromGUID(guid) + if not unit then return nil end + local name, _, texture, startMs, endMs, _, _, _, spellID = C_Spell.UnitCastingInfo(unit) + local isChannel + if not name then + name, _, texture, startMs, endMs, _, _, spellID = C_Spell.UnitChannelInfo(unit) + isChannel = true + end + if not name or not startMs or not endMs then return nil end + return { + spellName = name, + spellID = spellID, + icon = texture, + startTime = startMs / 1000, + endTime = endMs / 1000, + duration = (endMs - startMs) / 1000, + isChannel = isChannel, + } end local guidTargetTokenCache = {} -- guid -> "target" interned string @@ -245,15 +264,6 @@ pfUI:RegisterModule("nameplates", function () return end - local function wipe(table) - if type(table) ~= "table" then - return - end - for k in pairs(table) do - table[k] = nil - end - end - local function DisableObject(object) if not object then return end if not object.GetObjectType then return end @@ -559,10 +569,6 @@ end if threatMemory[guid] then threatMemory[guid] = nil end if guidTargetTokenCache[guid] then guidTargetTokenCache[guid] = nil end if combatColorCache[guid] then combatColorCache[guid] = nil end - local castInfo = GetCastInfo(guid) - if castInfo and castInfo.endTime and castInfo.endTime < GetTime() then - if pfUI.libdebuff_casts then pfUI.libdebuff_casts[guid] = nil end - end local plate = C_NamePlate.GetNamePlateForUnit(arg1) if plate and plate.nameplate and plate.nameplate.cachedGuid == guid then plate.nameplate.cachedGuid = nil @@ -1305,9 +1311,7 @@ end local isCastingNonTarget = not target and nameplate.castbar and nameplate.castbar:IsShown() if not isCastingNonTarget and not target and cfg.showcastbar and nameplate.cachedGuid then local castInfo = GetCastInfo(nameplate.cachedGuid) - if castInfo and castInfo.spellID and castInfo.endTime and castInfo.endTime > now then - isCastingNonTarget = true - elseif pfGetCastInfo and nameplate.castUpdate then + if castInfo and castInfo.endTime > now then isCastingNonTarget = true end end @@ -1595,94 +1599,40 @@ end -- Shared castbar update logic (used by both dedicated frame and central loop) nameplates.UpdateCastbar = function(nameplate, now) if not nameplate or not nameplate.castbar then return end - local unitstr = nameplate.cachedGuid - local castInfo = unitstr and GetCastInfo(unitstr) + local castInfo = GetCastInfo(nameplate.cachedGuid) + if not castInfo or castInfo.endTime < now then + nameplate.castbar.isShown = nil + nameplate.castbar.lastEndTime = nil + nameplate.castbar:Hide() + return + end - if castInfo and castInfo.spellID then - if castInfo.startTime + castInfo.duration < now then - wipe(castInfo) - nameplate.castbar:Hide() - elseif castInfo.event == "CAST" or castInfo.event == "FAIL" then - wipe(castInfo) - nameplate.castbar:Hide() - else - -- Only update min/max, color and icon once per cast (when endTime changes) - local isChannel = castInfo.event == "CHANNEL" - local duration = castInfo.endTime - castInfo.startTime - if nameplate.castbar.lastEndTime ~= castInfo.endTime then - nameplate.castbar.lastEndTime = castInfo.endTime - nameplate.castbar.lastTextTick = nil - -- Use relative 0..duration range (same as castbar.lua) to avoid - -- floating-point precision loss with large absolute timestamps - nameplate.castbar:SetMinMaxValues(0, duration) - nameplate.castbar:SetStatusBarColor(strsplit(",", C.appearance.castbar[(isChannel and "channelcolor" or "castbarcolor")])) - if castInfo.icon then - nameplate.castbar.icon.tex:SetTexture(castInfo.icon) - nameplate.castbar.icon.tex:SetTexCoord(.1,.9,.1,.9) - end - if cfg.spellname then - nameplate.castbar.spell:SetText(castInfo.spellName) - else - nameplate.castbar.spell:SetText("") - end - end - local barValue - if isChannel then - barValue = castInfo.endTime - now - else - barValue = now - castInfo.startTime - end - barValue = barValue < 0 and 0 or barValue - barValue = barValue > duration and duration or barValue - nameplate.castbar:SetValue(barValue) - SetCastbarText(nameplate.castbar, castInfo.endTime - now) - if not nameplate.castbar.isShown then nameplate.castbar.isShown = true; nameplate.castbar:Show() end + local isChannel = castInfo.isChannel + local duration = castInfo.duration + if nameplate.castbar.lastEndTime ~= castInfo.endTime then + nameplate.castbar.lastEndTime = castInfo.endTime + nameplate.castbar.lastTextTick = nil + -- Relative 0..duration range to avoid float precision loss with large + -- absolute timestamps. + nameplate.castbar:SetMinMaxValues(0, duration) + nameplate.castbar:SetStatusBarColor(strsplit(",", C.appearance.castbar[(isChannel and "channelcolor" or "castbarcolor")])) + if castInfo.icon then + nameplate.castbar.icon.tex:SetTexture(castInfo.icon) + nameplate.castbar.icon.tex:SetTexCoord(.1,.9,.1,.9) end - else - -- libcast fallback (vanilla without Nampower) - if unitstr and pfGetCastInfo then - local cast, _, _, texture, startTime, endTime = pfGetCastInfo(unitstr) - local channel - if not cast then - channel, _, _, texture, startTime, endTime = pfGetChannelInfo(unitstr) - end - if cast or channel then - local effect = cast or channel - local duration = endTime - startTime - local max = duration / 1000 - local cur = now - startTime / 1000 - if channel then cur = max + startTime / 1000 - now end - if cur < 0 then cur = 0 end - if cur > max then cur = max end - if nameplate.castbar.lastEndTime ~= endTime then - nameplate.castbar.lastEndTime = endTime - nameplate.castbar.lastTextTick = nil - nameplate.castbar:SetMinMaxValues(0, max) - nameplate.castbar:SetStatusBarColor(strsplit(",", C.appearance.castbar[(channel and "channelcolor" or "castbarcolor")])) - if texture then - nameplate.castbar.icon.tex:SetTexture(texture) - nameplate.castbar.icon.tex:SetTexCoord(.1, .9, .1, .9) - end - if cfg.spellname then - nameplate.castbar.spell:SetText(effect) - else - nameplate.castbar.spell:SetText("") - end - end - nameplate.castbar:SetValue(cur) - SetCastbarText(nameplate.castbar, channel and cur or (max - cur)) - if not nameplate.castbar.isShown then nameplate.castbar.isShown = true; nameplate.castbar:Show() end - else - nameplate.castbar.isShown = nil - nameplate.castbar.lastEndTime = nil - nameplate.castbar:Hide() - end + if cfg.spellname then + nameplate.castbar.spell:SetText(castInfo.spellName) else - nameplate.castbar.isShown = nil - nameplate.castbar.lastEndTime = nil - nameplate.castbar:Hide() + nameplate.castbar.spell:SetText("") end end + + local barValue = isChannel and (castInfo.endTime - now) or (now - castInfo.startTime) + if barValue < 0 then barValue = 0 end + if barValue > duration then barValue = duration end + nameplate.castbar:SetValue(barValue) + SetCastbarText(nameplate.castbar, castInfo.endTime - now) + if not nameplate.castbar.isShown then nameplate.castbar.isShown = true; nameplate.castbar:Show() end end -- Dedicated frame that updates ONLY the target plate castbar. diff --git a/modules/superwow.lua b/modules/superwow.lua index 9783e283..9c8996a5 100644 --- a/modules/superwow.lua +++ b/modules/superwow.lua @@ -174,102 +174,4 @@ pfUI:RegisterModule("superwow", function () end end - -- Enhance libcast with SuperWoW data for NPCs and other players - -- Player casts use SPELLCAST_* events for proper pushback handling - local supercast = CreateFrame("Frame") - local playerGuid = nil - - supercast:RegisterEvent("PLAYER_ENTERING_WORLD") - supercast:RegisterEvent("UNIT_CASTEVENT") - supercast:RegisterEvent("PLAYER_LOGOUT") - supercast:SetScript("OnEvent", function() - -- Handle shutdown to prevent crash 132 - if event == "PLAYER_LOGOUT" then - this:UnregisterAllEvents() - this:SetScript("OnEvent", nil) - return - end - - if event == "PLAYER_ENTERING_WORLD" then - -- Cache player GUID - if UnitExists then - local guid = UnitGUID("player") - playerGuid = guid - end - return - end - - local guid = arg1 - local isPlayer = guid == playerGuid - - -- For non-player units: disable combat parsing events (one-time init) - if not isPlayer and not supercast.init then - -- disable combat parsing events in superwow mode (for non-player units) - libcast:UnregisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE") - libcast:UnregisterEvent("CHAT_MSG_SPELL_HOSTILEPLAYER_DAMAGE") - libcast:UnregisterEvent("CHAT_MSG_SPELL_HOSTILEPLAYER_BUFF") - libcast:UnregisterEvent("CHAT_MSG_SPELL_FRIENDLYPLAYER_DAMAGE") - libcast:UnregisterEvent("CHAT_MSG_SPELL_FRIENDLYPLAYER_BUFF") - libcast:UnregisterEvent("CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_BUFFS") - libcast:UnregisterEvent("CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_BUFFS") - libcast:UnregisterEvent("CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE") - libcast:UnregisterEvent("CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_DAMAGE") - libcast:UnregisterEvent("CHAT_MSG_SPELL_PERIODIC_SELF_DAMAGE") - libcast:UnregisterEvent("CHAT_MSG_SPELL_PARTY_DAMAGE") - libcast:UnregisterEvent("CHAT_MSG_SPELL_PARTY_BUFF") - libcast:UnregisterEvent("CHAT_MSG_SPELL_PERIODIC_PARTY_DAMAGE") - libcast:UnregisterEvent("CHAT_MSG_SPELL_PERIODIC_PARTY_BUFFS") - libcast:UnregisterEvent("CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE") - libcast:UnregisterEvent("CHAT_MSG_SPELL_PERIODIC_CREATURE_BUFFS") - libcast:UnregisterEvent("CHAT_MSG_SPELL_CREATURE_VS_CREATURE_DAMAGE") - libcast:UnregisterEvent("CHAT_MSG_SPELL_CREATURE_VS_CREATURE_BUFF") - supercast.init = true - end - - if arg3 == "START" or arg3 == "CAST" or arg3 == "CHANNEL" then - local target = arg2 - local event_type = arg3 - local spell_id = arg4 - local timer = arg5 - - local spell = C_Spell.GetSpellName(spell_id) or UNKNOWN - local icon = C_Spell.GetSpellTexture(spell_id) or "Interface\\Icons\\INV_Misc_QuestionMark" - - -- skip on buff procs during cast - if event_type == "CAST" then - if not libcast.db[guid] or libcast.db[guid].cast ~= spell then - -- ignore casts without 'START' event, while there is already another cast. - -- those events can be for example a frost shield proc while casting frostbolt. - -- we want to keep the cast itself, so we simply skip those. - return - end - end - - -- For player: store in libcast.db[playerName] so pushback tracking works - -- For others: store by GUID - local dbKey = isPlayer and UnitName("player") or guid - - -- add cast action to the database - if not libcast.db[dbKey] then libcast.db[dbKey] = {} end - libcast.db[dbKey].cast = spell - libcast.db[dbKey].rank = nil - libcast.db[dbKey].start = GetTime() - libcast.db[dbKey].casttime = timer or 0 - libcast.db[dbKey].icon = icon - libcast.db[dbKey].channel = event_type == "CHANNEL" or false - elseif arg3 == "FAIL" then - -- For player: use playerName, for others: use GUID - local dbKey = isPlayer and UnitName("player") or guid - - -- delete all cast entries - if libcast.db[dbKey] then - libcast.db[dbKey].cast = nil - libcast.db[dbKey].rank = nil - libcast.db[dbKey].start = nil - libcast.db[dbKey].casttime = nil - libcast.db[dbKey].icon = nil - libcast.db[dbKey].channel = nil - end - end - end) end) \ No newline at end of file diff --git a/modules/turtle-wow.lua b/modules/turtle-wow.lua index ba6dde73..78004c04 100644 --- a/modules/turtle-wow.lua +++ b/modules/turtle-wow.lua @@ -312,42 +312,41 @@ pfUI:RegisterModule("turtle-wow", function () end end) - -- add Trueshot recognition to to custom castbars. - if not libcast.customcast["steadyshot"] then - -- add trueshot to pfUI's custom casts - local player = UnitName("player") + -- Synthetic Steady Shot cast bar. The engine treats Steady Shot as + -- instant, but the arrow doesn't actually fire until the next ranged + -- swing (~1.4s baseline). Drive a synthetic entry off Nampower's + -- SPELL_QUEUE_EVENT so hunters see a tickdown matching the swing wait. + -- castbar.lua reads pfUI.synthetic_casts[unit] as a fallback when + -- C_Spell.UnitCastingInfo returns nil. + pfUI_locale["enUS"]["customcast"]["TRUESHOT"] = "Steady Shot" + pfUI_locale["zhCN"]["customcast"]["TRUESHOT"] = "稳固射击" + pfUI.synthetic_casts = pfUI.synthetic_casts or {} - -- add locales - pfUI_locale["enUS"]["customcast"]["TRUESHOT"] = "Steady Shot" - pfUI_locale["zhCN"]["customcast"]["TRUESHOT"] = "稳固射击" - local trueshot = L["customcast"]["TRUESHOT"] or "" + local steadyShotName = L["customcast"]["TRUESHOT"] + local STEADY_SHOT_ICON = "Interface\\Icons\\Ability_hunter_steadyshot" + local STEADY_SHOT_DURATION_MS = 1400 -- not haste-scaled; close enough for the visual cue + local ON_SWING_QUEUED, ON_SWING_QUEUE_POPPED = 0, 1 - libcast.customcast[strlower(trueshot)] = function(begin, duration) - if begin then - -- cast time is 1sec, however it takes 1.4sec to fire in average - local duration = libcast.ApplyShotHaste(duration or 1400) - - local _,_, lag = GetNetStats() - local start = GetTime() + lag/1000 - - -- add cast action to the database - libcast.db[player].cast = trueshot - libcast.db[player].rank = lastrank - libcast.db[player].start = start - libcast.db[player].casttime = duration - libcast.db[player].icon = "Interface\\Icons\\Ability_hunter_steadyshot" - libcast.db[player].channel = nil - else - -- remove cast action to the database - libcast.db[player].cast = nil - libcast.db[player].rank = nil - libcast.db[player].start = nil - libcast.db[player].casttime = nil - libcast.db[player].icon = nil - libcast.db[player].channel = nil - end + local steadyShot = CreateFrame("Frame") + steadyShot:RegisterEvent("SPELL_QUEUE_EVENT") + steadyShot:SetScript("OnEvent", function() + if event ~= "SPELL_QUEUE_EVENT" then return end + local eventCode, spellId = arg1, arg2 + if eventCode ~= ON_SWING_QUEUED and eventCode ~= ON_SWING_QUEUE_POPPED then return end + if C_Spell.GetSpellName(spellId) ~= steadyShotName then return end + if eventCode == ON_SWING_QUEUED then + local now = GetTime() * 1000 + pfUI.synthetic_casts["player"] = { + name = steadyShotName, + icon = STEADY_SHOT_ICON, + startMs = now, + endMs = now + STEADY_SHOT_DURATION_MS, + spellID = spellId, + } + else + pfUI.synthetic_casts["player"] = nil end - end + end) -- add skin to twow's talent inspect frame if pfUI.skin["Inspect"] and pfUI_config["disabled"]["skin_Inspect"] ~= "1" then