-- 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 if we're not on a vanilla client if pfUI.client > 11200 then return end -- return instantly when another libcast is already active if pfUI.api.libcast then return end local lastcasttex, lastrank, _ local scanner = libtipscan:GetScanner("libcast") 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 = GetUnitGUID(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 = GetUnitGUID(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 from Nampower using spellId cached from SPELL_START_SELF icon = nil if lastSpellId and GetSpellRecField and GetSpellIconTexture then local iconId = GetSpellRecField(lastSpellId, "spellIconID") if iconId then icon = GetSpellIconTexture(iconId) if icon and not string.find(icon, "\\") then icon = "Interface\\Icons\\" .. icon end end end -- fallback to L["spells"] / lastcasttex if Nampower didn't provide icon 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"] libcast.customcast = {} libcast.customcast[strlower(aimedshot)] = function(begin, duration) if begin then local duration = duration or 3000 for i=1,32 do if UnitBuff("player", i) == "Interface\\Icons\\Racial_Troll_Berserk" then local berserk = 0.3 if((UnitHealth("player")/UnitHealthMax("player")) >= 0.40) then berserk = (1.30 - (UnitHealth("player") / UnitHealthMax("player"))) / 3 end duration = duration / (1 + berserk) elseif UnitBuff("player", i) == "Interface\\Icons\\Ability_Hunter_RunningShot" then duration = duration / 1.4 elseif UnitBuff("player", i) == "Interface\\Icons\\Ability_Warrior_InnerRage" then duration = duration / 1.3 elseif UnitBuff("player", i) == "Interface\\Icons\\Inv_Trinket_Naxxramas04" then duration = duration / 1.2 end end 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 = duration or 500 for i=1,32 do if UnitBuff("player", i) == "Interface\\Icons\\Racial_Troll_Berserk" then local berserk = 0.3 if((UnitHealth("player")/UnitHealthMax("player")) >= 0.40) then berserk = (1.30 - (UnitHealth("player") / UnitHealthMax("player"))) / 3 end duration = duration / (1 + berserk) elseif UnitBuff("player", i) == "Interface\\Icons\\Ability_Hunter_RunningShot" then duration = duration / 1.4 elseif UnitBuff("player", i) == "Interface\\Icons\\Ability_Warrior_InnerRage" then duration = duration / 1.3 elseif UnitBuff("player", i) == "Interface\\Icons\\Inv_Trinket_Naxxramas04" then duration = duration / 1.2 end end 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 GetActionText(slot) or not IsCurrentAction(slot) then return end scanner:SetAction(slot) local rawSpellName, rank = scanner:Line(1) if not rawSpellName then return end -- ignore if the spell is not found local cachedRawSpellName, cachedRank, cachedTexture, cachedCastingTime, _, _, cachedSpellId, cachedBookType = libspell.GetSpellInfo(rawSpellName .. (rank and ("(" .. rank .. ")") or "")) 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