mirror of
https://github.com/brues-code/pfUI.git
synced 2026-09-21 23:26:56 +00:00
casts: route everything through C_Spell, drop libcast + libdebuff_casts
ClassicAPI's recent C_Spell additions cover remote-unit casts natively
(SMSG_SPELL_START co-hook caching per caster GUID), so the two parallel
cast trackers pfUI was running — libcast.lua and the libdebuff_casts
table inside libdebuff.lua — can both retire.
Migrations:
- modules/castbar.lua: focus/player cast-info gathering reads
C_Spell.UnitCastingInfo / UnitChannelInfo directly. Fallback ladder
(libdebuff_casts → pfGetCastInfo → pfGetChannelInfo) collapses into a
single call. Pushback handlers stop writing back into a non-existent
cache; the local this.endTime is the source of truth.
- modules/nameplates.lua: GetCastInfo(guid) now resolves to a unit token
via UnitTokenFromGUID and queries C_Spell, returning the same compact
struct shape downstream code expected. UpdateCastbar collapses from a
three-branch hierarchy (dead IterDebuffs / libdebuff_casts / libcast)
to one C_Spell read.
- modules/afkcam.lua: pfGetCastInfo+pfGetChannelInfo round-trip becomes
a single C_Spell.UnitCastingInfo("player") or UnitChannelInfo fallback.
- libs/libpredict.lua: HealComm timing uses C_Spell on the sender's unit
token after a small group-roster walk to resolve the sender's name.
Deletions:
- libs/libcast.lua entirely (-571 lines) plus its init/libs.xml entry.
- libdebuff_casts / libdebuff_item_icons tables and their write sites
in libs/libdebuff.lua (the SPELL_START_*, SPELL_GO_*, SPELL_FAILED_*
event handlers stop maintaining them but keep firing the
libdebuff_*_hooks broadcast surface for actionbar / swingtimer /
libtotem). SPELLCAST_CHANNEL_STOP now reads the active channel from
C_Spell.ChannelInfo.
- modules/superwow.lua's supercast block — UNIT_CASTEVENT writes into
libcast.db are redundant now that C_Spell co-hooks the same packet.
- The cast-bar item-icon override that swapped in a potion/trinket
icon for item-triggered casts. Spell icon stays; the item-icon
metadata path (libdebuff_item_icons) went with libdebuff_casts.
Steady Shot synthetic cast bar — Turtle WoW-specific:
- castbar.lua gains a pfUI.synthetic_casts[unit] fallback that fires
only when C_Spell returns nil, so abilities the engine treats as
instant but which have a meaningful wait window can still render a
cast bar.
- modules/turtle-wow.lua replaces the old libcast.customcast block with
a Nampower SPELL_QUEUE_EVENT subscriber. ON_SWING_QUEUED matching
the localized "Steady Shot" name writes a 1.4s synthetic entry;
ON_SWING_QUEUE_POPPED clears it; castbar's endMs guard self-expires
the entry as a safety net. Note: haste scaling (libcast.ApplyShotHaste)
is gone — bar may finish slightly early under +ranged haste buffs.
Net: 152 insertions, 972 deletions.
This commit is contained in:
@@ -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
|
||||
+11
-66
@@ -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)
|
||||
|
||||
+26
-9
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user