mirror of
https://github.com/brues-code/pfUI.git
synced 2026-09-24 00:26:55 +00:00
00292ca3b4
commit70b1e66c2dAuthor: Brues <5278969+brues-code@users.noreply.github.com> Date: Sun Jul 26 18:30:18 2026 -0500 Use SetSize/SetShown and simplify minimap/map commit7b6bfe9975Author: Brues <5278969+brues-code@users.noreply.github.com> Date: Sat Jul 25 11:27:14 2026 -0500 Include raidpet frames in /pftest test mode The unit-frame test toggle (showall) only previews frames that exist and are positioned. Raidpet frames are created when raidpet.visible == "1", but LayoutPets' collapse mode only positions pets whose raidpet<N> unit actually exists, so solo/in test mode they stayed hidden. Add a showall branch to LayoutPets that mirror-lays every pet cell and shows it (bypassing collapse and the roster gate), still guarded by the existing showpets check so nothing happens when raidpet is disabled. Call LayoutPets from the /pftest handler so the grid updates immediately on toggle-on and restores to the normal layout on toggle-off. commit4c65c38647Author: Brues <5278969+brues-code@users.noreply.github.com> Date: Sat Jul 25 10:34:33 2026 -0500 Remove unused RangeCache local in UnitInRange commit6c96bbbe6cAuthor: Brues <5278969+brues-code@users.noreply.github.com> Date: Sat Jul 25 10:34:33 2026 -0500 Use Clamp() for the two-sided clamps in ui-widgets Replace the hand-rolled math.min/math.max and if/elseif clamp idioms in the status bar and scroll frame with Clamp(). Equivalent for every value seen (min <= max always holds), just clearer. commit830e0a0be1Author: Brues <5278969+brues-code@users.noreply.github.com> Date: Sat Jul 25 10:34:33 2026 -0500 Drop vanilla GetItemInfo shim for C_Item.GetItemInfo The compat/vanilla.lua GetItemInfo override reshaped vanilla's tuple into retail order by inserting nil at slot 4 (itemLevel), but truncated at 8 fields, dropping itemEquipLoc/itemTexture. ClassicAPI's global GetItemInfo keeps the vanilla shape (its hook only warms the item cache), so the shim stayed necessary but incomplete. Remove it and point the three callers at ClassicAPI's C_Item.GetItemInfo, which returns the full 18-field retail tuple. Positions 1-8 are identical to the old shim output, so bags/roll are unchanged; character.lua's itemSlot (equipLoc, field 9) was always nil under the truncating shim and now resolves correctly for ShaguScore. commitc0da63657dAuthor: Brues <5278969+brues-code@users.noreply.github.com> Date: Sat Jul 25 10:09:34 2026 -0500 Various cleanup Removes version compatibility checks for TBC/Vanilla, consolidating code to target a single WoW version. Refactors repetitive SetWidth/SetHeight calls to SetSize for cleaner code. Removes duplicate function definitions from unitxp module and simplifies conditional show/hide operations using SetShown. commit11a6302c2dAuthor: Brues <5278969+brues-code@users.noreply.github.com> Date: Sat Jul 25 09:53:37 2026 -0500 Delegate pfUI.api.strsplit to ClassicAPI's strsplit Replace the Lua pattern-based implementation with a thin wrapper around ClassicAPI's C-level strsplit. Keeps the pfUI.api.strsplit entry point for backwards compatibility with addons that call it, while dropping the redundant reimplementation. Behavioral note: the old version used ([^delim]+) which silently collapsed empty fields; delegating to real strsplit now preserves them ("a,,b" -> "a", "", "b"). All in-repo callers split numeric color tuples, version strings, or build name-keyed lookup tables, none of which are affected by empty-field preservation. commit67c126eac8Author: Brues <5278969+brues-code@users.noreply.github.com> Date: Sat Jul 25 01:01:02 2026 -0500 Bump ClassicAPI minimum version to 1.8.0 commita6cf157518Author: Brues <5278969+brues-code@users.noreply.github.com> Date: Sat Jul 25 00:49:50 2026 -0500 read spell rank via C_Spell.GetSpellSubtext The rank string comes from the spell subtext ("Rank N"), which ClassicAPI's C_Spell.GetSpellSubtext returns directly -- so drop nampower's GetSpellRecField(spellId, "rank") here. The existing "Rank " gsub parse is unchanged. commit4b1ba99b4fAuthor: Brues <5278969+brues-code@users.noreply.github.com> Date: Sat Jul 25 00:33:20 2026 -0500 Move player info overlay onto ClassicAPI (drop Nampower) The haste/spell-power overlay read both values through Nampower and disabled itself entirely without it. Move both to ClassicAPI so it works on any ClassicAPI client: - Spell power: GetSpellPower("net") (nampower) -> GetSpellBonusDamage(i) (ClassicAPI, same per-school field). Merged the build+scan into one loop; the default school seeds the tiebreak so equal schools still favor it. - Haste: GetUnitField("player", "modCastSpeed") -> UnitSpellHaste("player"), which returns the haste percentage directly (the (1/modCastSpeed-1)*100 conversion is now baked in, off the same UNIT_MOD_CAST_SPEED field, signed). - Dropped the "if not GetUnitField then return" gate -- UpdateInfoText no longer touches Nampower, so the overlay runs everywhere. commit7df4aa6d50Author: Brues <5278969+brues-code@users.noreply.github.com> Date: Sat Jul 25 00:32:44 2026 -0500 Hook the real global _G.UnitHealth for feign death Inside a RegisterModule body, `function UnitHealth(...)` defines UnitHealth on the pfUI environment, not the real global -- so the feign-death real-HP fix only reached callers that resolve UnitHealth through pfUI's env, and missed _G consumers (Blizzard frames, other addons). Hook _G.UnitHealth explicitly (and capture oldUnitHealth from _G) so the un-gate applies everywhere. commitb0bf2fd869Author: Brues <5278969+brues-code@users.noreply.github.com> Date: Fri Jul 24 21:10:58 2026 -0500 Refactor nampower module Simplify reactive spell storage by using spell IDs instead of texture/name tables. Update to use modern C_Spell APIs (GetSpellTexture, GetSpellName) instead of manual texture paths. Consolidate SetWidth/SetHeight calls to SetSize and use print() instead of DEFAULT_CHAT_FRAME:AddMessage(). commit0b06961333Author: Brues <5278969+brues-code@users.noreply.github.com> Date: Fri Jul 24 20:54:23 2026 -0500 Use GetNamePlateForUnit for target lookups; drop dead ScanGuid block - Replace the three GetNamePlateForGUID(UnitGUID("target")) round-trips with GetNamePlateForUnit("target"), which resolves the token to a GUID internally -- no UnitGUID string detour, and it returns nil for no-target/no-plate so the UnitExists guards collapse. GetNamePlateForGUID is left for raw-GUID cases. - Remove the dead libunitscan.ScanGuid nameplate block (ScanGuid was deleted from libunitscan long ago, so the guard was never true) -- it carried the last GetUnitField("npcFlags") read. commit3e6210086eAuthor: Brues <5278969+brues-code@users.noreply.github.com> Date: Fri Jul 24 20:54:23 2026 -0500 Drop Nampower stats system and polling from unit frames GetUnitStats now reads health/power straight from UnitHealth/UnitPower (the descriptor fields the server broadcasts) -- for a real unit token GetUnitField read nothing different, so the whole Nampower-vs-fallback apparatus was measuring a distinction that no longer exists. Removed: - The stats system: pfUI.uf.stats, pfUIStatsFrame + UpdateStatsDisplay, the lastUnitStats change-cache, the /pfuistats slash command, and every counter increment (event/heartbeat/earlyReturn/throttledSkip/nampower/fallback). - All GetUnitField health reads (GetUnitStats, heal-prediction, health-gradient color) -- collapsed to the UnitHealth/UnitHealthMax they already fell back to. - The heartbeat-polling backstop and its fallbackThrottle/lastEventUpdate deps. Frames now refresh on events only; eventless frames still use their own tick, and range/aggro still run in the eventless-actions block. commit63001b7b0cAuthor: Brues <5278969+brues-code@users.noreply.github.com> Date: Fri Jul 24 20:09:42 2026 -0500 Move nameplates onto ClassicAPI stable nameplate tokens ClassicAPI now assigns retail-exact, per-plate-stable "nameplateN" tokens and fires vanilla UNIT_* events for them, so the nameplate module can key off the token instead of Nampower/SuperWoW GUID primitives: - Events: UNIT_AURA_GUID/UNIT_FLAGS_GUID -> UNIT_AURA/UNIT_FLAGS, matched by the "nameplateN" token (guarded on the token prefix, since these also fire for target/party/raid). Registered unconditionally -- no GetUnitField gate. - Health: GetUnitField(guid, "health"/"maxHealth") -> UnitHealth/UnitHealthMax on the plate's cached token. Same UNIT_FIELD_HEALTH the server broadcasts (real HP on Turtle; the ~= 100 scaled-vs-real guard is unchanged). - Combat/target in GetCombatStateColor: GetUnitField("flags") + HasFlag -> UnitAffectingCombat(token); GetUnitField("target") + SuperWoW "<guid>target" -> the "nameplateNtarget" chain. Dropped the now-dead HasFlag, UNIT_FLAG_IN_COMBAT, and guidTargetTokenCache. - GetCastInfo now takes the token directly; every caller already holds it, so the UnitTokenFromGUID(guid) round-trip is gone (unused module-wide now). NAME_PLATE_UNIT_ADDED caches the token as plate.nameplate.unit alongside the GUID; REMOVED clears both. GUID is retained only as the stable cache key (debuffCache/threatMemory/combatColorCache). Only GetUnitField("npcFlags") remains -- it has no token/vanilla equivalent. commitce1c49fcbbAuthor: Brues <5278969+brues-code@users.noreply.github.com> Date: Fri Jul 24 18:20:06 2026 -0500 Make focus/focustarget event-driven via ClassicAPI unit events ClassicAPI now fires UNIT_* (health/mana/aura/...) with arg1 == "focus" and arg1 == "focustarget", observed per-unit like target/party/raid. Both frames already registered those events (focus/focustarget are in pfValidUnits) and their OnEvent already matches arg1 == label, so the 0.2s polling ticks were pure workarounds for the missing events. Drop both ticks; the frames now refresh on-event like target, with range/glow still on the shared 0.5s state pass and PLAYER_FOCUS_CHANGED still driving assign/clear.
1687 lines
62 KiB
Lua
1687 lines
62 KiB
Lua
-- load pfUI environment
|
|
setfenv(1, pfUI:GetEnvironment())
|
|
|
|
--[[ libdebuff - GetUnitField Edition ]]--
|
|
-- A pfUI library that detects and saves all ongoing debuffs of players, NPCs and enemies.
|
|
--
|
|
-- MAJOR REWRITE: Now uses GetUnitField for slot mapping instead of manual shifting.
|
|
-- Key insight: GetUnitField returns STABLE aura slots (33-48) that DON'T shift when
|
|
-- debuffs expire. Only the display slots (UnitDebuff returns 1,2,3...) are compacted.
|
|
--
|
|
-- This eliminates ~400 lines of error-prone shift logic while maintaining full
|
|
-- multi-caster tracking support.
|
|
--
|
|
-- The public per-aura readers (UnitDebuff, UnitOwnDebuff) were retired in favor
|
|
-- 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 (libpredict HoT tracking) and the
|
|
-- libdebuff_*_hooks broadcast surface (subscribers in actionbar / swingtimer
|
|
-- react to SPELL_GO and SPELL_FAILED).
|
|
|
|
-- return instantly when another libdebuff is already active
|
|
if pfUI.api.libdebuff then return end
|
|
|
|
-- fix a typo (missing $) in ruRU capture index
|
|
if GetLocale() == "ruRU" then
|
|
SPELLREFLECTSELFOTHER = gsub(SPELLREFLECTSELFOTHER, "%%2s", "%%2%$s")
|
|
end
|
|
|
|
local libdebuff = CreateFrame("Frame", "pfdebuffsScanner", UIParent)
|
|
local _, class = UnitClass("player")
|
|
local lastspell
|
|
|
|
-- Nampower Support
|
|
local hasNampower = false
|
|
|
|
-- Set hasNampower immediately for functionality
|
|
if GetNampowerVersion then
|
|
local major, minor, patch = GetNampowerVersion()
|
|
patch = patch or 0
|
|
-- Minimum required version: 3.0.0 (UnitGUID support)
|
|
if major > 3 or (major == 3 and minor > 0) or (major == 3 and minor == 0 and patch >= 0) then
|
|
hasNampower = true
|
|
end
|
|
end
|
|
|
|
EventUtil.ContinueOnPlayerLogin(function()
|
|
RunNextFrame(function()
|
|
|
|
if GetNampowerVersion then
|
|
local major, minor, patch = GetNampowerVersion()
|
|
patch = patch or 0
|
|
local versionString = major .. "." .. minor .. "." .. patch
|
|
|
|
if major > 3 or (major == 3 and minor > 0) or (major == 3 and minor == 0 and patch >= 0) then
|
|
if SetCVar and GetCVar then
|
|
local cvarsToEnable = {
|
|
"NP_EnableSpellStartEvents",
|
|
"NP_EnableSpellGoEvents",
|
|
"NP_EnableAuraCastEvents",
|
|
"NP_EnableAutoAttackEvents",
|
|
"NP_EnableSpellHealEvents",
|
|
}
|
|
local enabledCount = 0
|
|
local alreadyEnabledCount = 0
|
|
local failedCount = 0
|
|
|
|
for _, cvar in ipairs(cvarsToEnable) do
|
|
local success, currentValue = pcall(GetCVar, cvar)
|
|
if success and currentValue then
|
|
if currentValue == "1" then
|
|
alreadyEnabledCount = alreadyEnabledCount + 1
|
|
else
|
|
local setSuccess = pcall(SetCVar, cvar, "1")
|
|
if setSuccess then enabledCount = enabledCount + 1
|
|
else failedCount = failedCount + 1 end
|
|
end
|
|
else
|
|
failedCount = failedCount + 1
|
|
end
|
|
end
|
|
|
|
if enabledCount > 0 then
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cff33ff99[libdebuff]|r Enabled " .. enabledCount .. " Nampower CVars")
|
|
end
|
|
if failedCount > 0 then
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cffffcc00[libdebuff]|r Warning: Could not check/set " .. failedCount .. " CVars")
|
|
end
|
|
end
|
|
|
|
else
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[libdebuff] Debuff tracking disabled! Please update Nampower to v3.0.0 or higher.|r")
|
|
StaticPopup_Show("LIBDEBUFF_NAMPOWER_UPDATE", versionString)
|
|
end
|
|
else
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[libdebuff] Nampower not found! Debuff tracking disabled.|r")
|
|
StaticPopup_Show("LIBDEBUFF_NAMPOWER_MISSING")
|
|
end
|
|
end)
|
|
end)
|
|
|
|
-- ============================================================================
|
|
-- DATA STRUCTURES (Simplified - no more manual slot tracking!)
|
|
-- ============================================================================
|
|
|
|
-- ownDebuffs: [targetGUID][spellName] = {startTime, duration, texture, rank}
|
|
-- Timer data for OUR debuffs only
|
|
pfUI.libdebuff_own = pfUI.libdebuff_own or {}
|
|
local ownDebuffs = pfUI.libdebuff_own
|
|
|
|
-- allAuraCasts: [targetGUID][spellName][casterGuid] = {startTime, duration, rank}
|
|
-- Timer data for ALL debuffs (multi-caster support)
|
|
pfUI.libdebuff_all_auras = pfUI.libdebuff_all_auras or {}
|
|
local allAuraCasts = pfUI.libdebuff_all_auras
|
|
|
|
-- slotOwnership: [targetGUID][auraSlot] = {casterGuid, spellName, spellId}
|
|
-- Maps REAL aura slots (33-48) to caster info - NO SHIFTING NEEDED!
|
|
pfUI.libdebuff_slot_ownership = pfUI.libdebuff_slot_ownership or {}
|
|
local slotOwnership = pfUI.libdebuff_slot_ownership
|
|
|
|
-- displayToAura: [targetGUID][displaySlot] = auraSlot
|
|
-- Maps DISPLAY slots (1-16) to REAL aura slots (33-48) for DEBUFF_REMOVED correlation
|
|
pfUI.libdebuff_display_to_aura = pfUI.libdebuff_display_to_aura or {}
|
|
local displayToAura = pfUI.libdebuff_display_to_aura
|
|
|
|
-- pendingCasts: [targetGUID][spellName] = {casterGuid, rank, time}
|
|
-- Temporary storage from SPELL_GO to correlate with DEBUFF_ADDED
|
|
pfUI.libdebuff_pending = pfUI.libdebuff_pending or {}
|
|
local pendingCasts = pfUI.libdebuff_pending
|
|
|
|
-- Spell Icon Cache: [spellId] = texture
|
|
pfUI.libdebuff_icon_cache = pfUI.libdebuff_icon_cache or {}
|
|
local iconCache = pfUI.libdebuff_icon_cache
|
|
|
|
-- Cast Tracking: [casterGuid] = {spellID, spellName, icon, startTime, duration, endTime}
|
|
-- Shared with nameplates for cast-bar display
|
|
|
|
-- Cleveroids API: [targetGUID][spellID] = {start, duration, caster, stacks}
|
|
pfUI.libdebuff_objects_guid = pfUI.libdebuff_objects_guid or {}
|
|
local objectsByGuid = pfUI.libdebuff_objects_guid
|
|
|
|
-- LEGACY: Keep these for backwards compatibility (external modules might check them)
|
|
pfUI.libdebuff_own_slots = pfUI.libdebuff_own_slots or {}
|
|
pfUI.libdebuff_all_slots = pfUI.libdebuff_all_slots or {}
|
|
|
|
-- Deduplication: Track recent AURA_CAST events to ignore duplicates
|
|
-- [targetGuid][spellName][casterGuid] = timestamp
|
|
pfUI.libdebuff_recent_casts = pfUI.libdebuff_recent_casts or {}
|
|
local recentCasts = pfUI.libdebuff_recent_casts
|
|
|
|
-- Callbacks fired after SPELL_GO_SELF is processed: fn(spellId, arg1, arg2, arg3, arg4, arg5, arg6, arg7)
|
|
pfUI.libdebuff_spell_go_hooks = pfUI.libdebuff_spell_go_hooks or {}
|
|
|
|
-- Callbacks fired after SPELL_GO_OTHER is processed: fn(spellId, casterGuid, targetGuid)
|
|
pfUI.libdebuff_spell_go_other_hooks = pfUI.libdebuff_spell_go_other_hooks or {}
|
|
|
|
-- Callbacks fired after SPELL_START_SELF is processed: fn(spellId, casterGuid, targetGuid, castTime)
|
|
pfUI.libdebuff_spell_start_self_hooks = pfUI.libdebuff_spell_start_self_hooks or {}
|
|
|
|
-- Callbacks fired after SPELL_START_OTHER is processed: fn(spellId, casterGuid, targetGuid, castTime)
|
|
pfUI.libdebuff_spell_start_other_hooks = pfUI.libdebuff_spell_start_other_hooks or {}
|
|
|
|
-- Callbacks fired after SPELL_FAILED_OTHER is processed: fn(casterGuid, spellId)
|
|
pfUI.libdebuff_spell_failed_other_hooks = pfUI.libdebuff_spell_failed_other_hooks or {}
|
|
|
|
-- Callbacks fired after AURA_CAST_ON_SELF is processed: fn(spellId, casterGuid, targetGuid)
|
|
pfUI.libdebuff_aura_cast_on_self_hooks = pfUI.libdebuff_aura_cast_on_self_hooks or {}
|
|
|
|
-- Callbacks fired after AURA_CAST_ON_OTHER is processed: fn(spellId, casterGuid, targetGuid)
|
|
pfUI.libdebuff_aura_cast_on_other_hooks = pfUI.libdebuff_aura_cast_on_other_hooks or {}
|
|
|
|
-- Callbacks fired after DEBUFF_ADDED_OTHER is processed: fn(guid, luaSlot, spellId, stackCount)
|
|
pfUI.libdebuff_debuff_added_other_hooks = pfUI.libdebuff_debuff_added_other_hooks or {}
|
|
|
|
-- Callbacks fired after DEBUFF_REMOVED_OTHER is processed: fn(guid, luaSlot, spellId, stackCount)
|
|
pfUI.libdebuff_debuff_removed_other_hooks = pfUI.libdebuff_debuff_removed_other_hooks or {}
|
|
|
|
-- Callbacks fired after UNIT_HEALTH is processed: fn(unitToken)
|
|
pfUI.libdebuff_unit_health_hooks = pfUI.libdebuff_unit_health_hooks or {}
|
|
|
|
-- Callbacks fired after PLAYER_TARGET_CHANGED is processed: fn()
|
|
pfUI.libdebuff_player_target_changed_hooks = pfUI.libdebuff_player_target_changed_hooks or {}
|
|
|
|
-- Callbacks fired after UNIT_DIED is processed: fn(guid)
|
|
pfUI.libdebuff_unit_died_hooks = pfUI.libdebuff_unit_died_hooks or {}
|
|
|
|
-- Callbacks fired after SPELL_CAST_EVENT is processed: fn(success, spellId, castType, targetGuid)
|
|
pfUI.libdebuff_spell_cast_hooks = pfUI.libdebuff_spell_cast_hooks or {}
|
|
|
|
-- Callbacks fired when a cast is identified as a downrank of an already active debuff.
|
|
-- fn(spellName, castRank, activeRank, targetGuid, casterGuid)
|
|
-- External addons can use this to avoid re-implementing downrank detection themselves.
|
|
pfUI.libdebuff_downrank_blocked_hooks = pfUI.libdebuff_downrank_blocked_hooks or {}
|
|
local AURA_CAST_DEDUPE_WINDOW = 0.1 -- Ignore duplicates within 100ms
|
|
|
|
-- Pending cast info for libpredict (heal prediction target tracking)
|
|
-- SPELL_CAST_EVENT fires with targetGuid BEFORE SPELLCAST_START,
|
|
-- which allows libpredict to know the correct target for queued casts.
|
|
-- Fields: { spellId, spellName, targetGuid, time }
|
|
pfUI.libpredict_pending_cast = pfUI.libpredict_pending_cast or {}
|
|
|
|
-- ============================================================================
|
|
-- STATIC POPUP DIALOGS
|
|
-- ============================================================================
|
|
|
|
local function CopyNampowerLink()
|
|
pfUI.chat.urlcopy.CopyText("https://github.com/brues-code/nampower")
|
|
end
|
|
|
|
StaticPopupDialogs["LIBDEBUFF_NAMPOWER_UPDATE"] = {
|
|
text = "|cffff0000!!!WARNING!!!|r\n\nNampower Update Required!\n\nYour current version: %s\nRequired version: 3.0.0+\n\nPlease update Nampower to continue using pfUI!",
|
|
button1 = "Show Download",
|
|
button2 = "Dismiss",
|
|
timeout = 0,
|
|
whileDead = 1,
|
|
hideOnEscape = 0,
|
|
preferredIndex = 3,
|
|
OnAccept = CopyNampowerLink,
|
|
}
|
|
|
|
StaticPopupDialogs["LIBDEBUFF_NAMPOWER_MISSING"] = {
|
|
text = "|cffff0000!!!WARNING!!!|r\n\nNampower Not Found!\n\nNampower 3.0.0+ is required for pfUI to function correctly.\n\nPlease install Nampower!",
|
|
button1 = "Show Download",
|
|
button2 = "Dismiss",
|
|
timeout = 0,
|
|
whileDead = 1,
|
|
hideOnEscape = 0,
|
|
preferredIndex = 3,
|
|
OnAccept = CopyNampowerLink,
|
|
}
|
|
|
|
-- ============================================================================
|
|
-- SPELL DATA TABLES
|
|
-- ============================================================================
|
|
|
|
-- Debuffs that only ONE player can have on target (overwrites other casters)
|
|
local selfOverwriteDebuffs = {
|
|
["Faerie Fire"] = true,
|
|
["Faerie Fire (Feral)"] = true,
|
|
["Demoralizing Shout"] = true,
|
|
["Demoralizing Roar"] = true,
|
|
["Hunter's Mark"] = true,
|
|
["Sunder Armor"] = true,
|
|
["Thunder Clap"] = true,
|
|
["Expose Armor"] = true,
|
|
["Curse of Weakness"] = true,
|
|
["Curse of Recklessness"] = true,
|
|
["Curse of the Elements"] = true,
|
|
["Curse of Shadow"] = true,
|
|
["Curse of Tongues"] = true,
|
|
["Curse of Exhaustion"] = true,
|
|
["Judgement of Wisdom"] = true,
|
|
["Judgement of Light"] = true,
|
|
["Judgement of the Crusader"] = true,
|
|
["Judgement of Justice"] = true,
|
|
["Shadow Weaving"] = true,
|
|
["Winter's Chill"] = true,
|
|
}
|
|
|
|
-- Debuff pairs that overwrite each other
|
|
local debuffOverwritePairs = {
|
|
["Faerie Fire"] = "Faerie Fire (Feral)",
|
|
["Faerie Fire (Feral)"] = "Faerie Fire",
|
|
["Demoralizing Shout"] = "Demoralizing Roar",
|
|
["Demoralizing Roar"] = "Demoralizing Shout",
|
|
}
|
|
|
|
-- ============================================================================
|
|
-- HELPER FUNCTIONS
|
|
-- ============================================================================
|
|
|
|
-- Debug Stats
|
|
pfUI.libdebuff_debugstats = pfUI.libdebuff_debugstats or {
|
|
enabled = false,
|
|
trackAllUnits = false,
|
|
aura_cast = 0,
|
|
debuff_added = 0,
|
|
debuff_removed = 0,
|
|
getunitfield_calls = 0,
|
|
}
|
|
local debugStats = pfUI.libdebuff_debugstats
|
|
|
|
local function DebugGuid(guid)
|
|
if not guid then return "nil" end
|
|
local str = tostring(guid)
|
|
if string.len(str) > 4 then
|
|
return string.sub(str, -4)
|
|
end
|
|
return str
|
|
end
|
|
|
|
local function IsCurrentTarget(guid)
|
|
if debugStats.trackAllUnits then return true end
|
|
if not guid or not UnitGUID then return false end
|
|
local targetGuid = UnitGUID("target")
|
|
return targetGuid == guid
|
|
end
|
|
|
|
local function GetDebugTimestamp()
|
|
return string.format("[%.3f]", GetTime())
|
|
end
|
|
|
|
-- Speichert die Ranks der zuletzt gecasteten Spells
|
|
pfUI.libdebuff_lastranks = pfUI.libdebuff_lastranks or {}
|
|
local lastCastRanks = pfUI.libdebuff_lastranks
|
|
|
|
-- Speichert Spells die gefailed sind
|
|
pfUI.libdebuff_lastfailed = pfUI.libdebuff_lastfailed or {}
|
|
local lastFailedSpells = pfUI.libdebuff_lastfailed
|
|
|
|
-- Get spell icon texture (with caching)
|
|
function libdebuff:GetSpellIcon(spellId)
|
|
if not spellId or type(spellId) ~= "number" or spellId <= 0 then
|
|
return "Interface\\Icons\\INV_Misc_QuestionMark"
|
|
end
|
|
|
|
if iconCache[spellId] then
|
|
return iconCache[spellId]
|
|
end
|
|
|
|
local texture = C_Spell.GetSpellTexture(spellId) or "Interface\\Icons\\INV_Misc_QuestionMark"
|
|
iconCache[spellId] = texture
|
|
return texture
|
|
end
|
|
|
|
pfUI.libdebuff_GetSpellIcon = function(spellId)
|
|
return libdebuff:GetSpellIcon(spellId)
|
|
end
|
|
|
|
function libdebuff:DidSpellFail(spell)
|
|
if not spell then return false end
|
|
local data = lastFailedSpells[spell]
|
|
if data and (GetTime() - data.time) < 1 then
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- ============================================================================
|
|
-- CORE: GetUnitField-based Slot Mapping (THE KEY INNOVATION!)
|
|
-- ============================================================================
|
|
|
|
-- Dispel type mapping: SpellRec.dispel index -> Blizzard DebuffTypeColor key
|
|
local dispelTypeMap = {
|
|
[1] = "Magic",
|
|
[2] = "Curse",
|
|
[3] = "Disease",
|
|
[4] = "Poison",
|
|
}
|
|
|
|
-- Get current debuff state directly from WoW via GetUnitField
|
|
-- Returns: { [displaySlot] = {auraSlot, spellId, spellName, stacks, texture, dtype} }
|
|
local function GetDebuffSlotMap(guid)
|
|
if not guid or not GetUnitField then
|
|
return nil
|
|
end
|
|
|
|
local auras = GetUnitField(guid, "aura")
|
|
if not auras then return nil end
|
|
|
|
local auraApps = GetUnitField(guid, "auraApplications")
|
|
|
|
if debugStats.enabled then
|
|
debugStats.getunitfield_calls = debugStats.getunitfield_calls + 1
|
|
end
|
|
|
|
local map = {}
|
|
local displaySlot = 0
|
|
|
|
-- Debuff aura slots are 33-48
|
|
for auraSlot = 33, 48 do
|
|
local spellId = auras[auraSlot]
|
|
if spellId and spellId > 0 then
|
|
displaySlot = displaySlot + 1
|
|
local spellName = C_Spell.GetSpellName(spellId)
|
|
local texture = libdebuff:GetSpellIcon(spellId)
|
|
local stacks = (auraApps and auraApps[auraSlot] or 0) + 1
|
|
local dtype = nil
|
|
if GetSpellRecField then
|
|
local dispelId = GetSpellRecField(spellId, "dispel")
|
|
if dispelId and dispelId > 0 then
|
|
dtype = dispelTypeMap[dispelId]
|
|
end
|
|
end
|
|
map[displaySlot] = {
|
|
auraSlot = auraSlot,
|
|
spellId = spellId,
|
|
spellName = spellName or "Unknown",
|
|
stacks = stacks,
|
|
texture = texture,
|
|
dtype = dtype
|
|
}
|
|
end
|
|
end
|
|
|
|
return map
|
|
end
|
|
|
|
-- Get caster info for a specific aura slot
|
|
local function GetSlotCaster(guid, auraSlot, spellName)
|
|
-- First check our ownership tracking
|
|
if slotOwnership[guid] and slotOwnership[guid][auraSlot] then
|
|
local ownership = slotOwnership[guid][auraSlot]
|
|
-- Verify spell name matches (slot might have been reused)
|
|
if ownership.spellName == spellName then
|
|
return ownership.casterGuid, ownership.isOurs
|
|
end
|
|
end
|
|
|
|
-- Fallback: Check ownDebuffs
|
|
local myGuid = GetPlayerGuid()
|
|
if ownDebuffs[guid] and ownDebuffs[guid][spellName] then
|
|
return myGuid, true
|
|
end
|
|
|
|
-- Fallback: Check allAuraCasts for any caster
|
|
if allAuraCasts[guid] and allAuraCasts[guid][spellName] then
|
|
for casterGuid, data in pairs(allAuraCasts[guid][spellName]) do
|
|
local timeleft = (data.startTime + data.duration) - GetTime()
|
|
if timeleft > 0 then
|
|
return casterGuid, (casterGuid == myGuid)
|
|
end
|
|
end
|
|
end
|
|
|
|
return nil, false
|
|
end
|
|
|
|
-- ============================================================================
|
|
-- CLEANUP FUNCTIONS
|
|
-- ============================================================================
|
|
|
|
local lastRangeCheck = 0
|
|
|
|
-- Recycled buffers for cleanup (avoids table creation per call)
|
|
local _cleanupBuf1 = {}
|
|
local _cleanupBuf2 = {}
|
|
|
|
local function CleanupUnit(guid)
|
|
if not guid then return false end
|
|
|
|
local cleaned = false
|
|
|
|
if ownDebuffs[guid] then
|
|
ownDebuffs[guid] = nil
|
|
cleaned = true
|
|
end
|
|
|
|
if slotOwnership[guid] then
|
|
slotOwnership[guid] = nil
|
|
cleaned = true
|
|
end
|
|
|
|
if allAuraCasts[guid] then
|
|
allAuraCasts[guid] = nil
|
|
cleaned = true
|
|
end
|
|
|
|
if objectsByGuid[guid] then
|
|
objectsByGuid[guid] = nil
|
|
cleaned = true
|
|
end
|
|
|
|
if pendingCasts[guid] then
|
|
pendingCasts[guid] = nil
|
|
cleaned = true
|
|
end
|
|
|
|
if debugStats.enabled and cleaned and IsCurrentTarget(guid) then
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cffff0000[CLEANUP]|r GUID %s", DebugGuid(guid)))
|
|
end
|
|
|
|
return cleaned
|
|
end
|
|
|
|
local function CleanupExpiredTimers(guid)
|
|
local now = GetTime()
|
|
|
|
-- Cleanup ownDebuffs
|
|
if ownDebuffs[guid] then
|
|
local n = 0
|
|
for spellName, data in pairs(ownDebuffs[guid]) do
|
|
local timeleft = (data.startTime + data.duration) - now
|
|
if timeleft < -2 then -- Grace period
|
|
n = n + 1
|
|
_cleanupBuf1[n] = spellName
|
|
end
|
|
end
|
|
for i = 1, n do
|
|
ownDebuffs[guid][_cleanupBuf1[i]] = nil
|
|
_cleanupBuf1[i] = nil
|
|
end
|
|
end
|
|
|
|
-- Cleanup allAuraCasts
|
|
if allAuraCasts[guid] then
|
|
for spellName, casterTable in pairs(allAuraCasts[guid]) do
|
|
local n2 = 0
|
|
for casterGuid, data in pairs(casterTable) do
|
|
local timeleft = (data.startTime + data.duration) - now
|
|
if timeleft < -2 then
|
|
n2 = n2 + 1
|
|
_cleanupBuf2[n2] = casterGuid
|
|
end
|
|
end
|
|
for i = 1, n2 do
|
|
allAuraCasts[guid][spellName][_cleanupBuf2[i]] = nil
|
|
_cleanupBuf2[i] = nil
|
|
end
|
|
-- Remove empty spell tables
|
|
local hasCasters = false
|
|
for _ in pairs(allAuraCasts[guid][spellName]) do
|
|
hasCasters = true
|
|
break
|
|
end
|
|
if not hasCasters then
|
|
allAuraCasts[guid][spellName] = nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function CleanupOutOfRangeUnits()
|
|
local now = GetTime()
|
|
-- Cleanup expired pending entries (every 1s)
|
|
if not pfUI.libdebuff_last_pending_cleanup then pfUI.libdebuff_last_pending_cleanup = 0 end
|
|
if now - pfUI.libdebuff_last_pending_cleanup >= 1 then
|
|
pfUI.libdebuff_last_pending_cleanup = now
|
|
for guid, spells in pairs(ownDebuffs) do
|
|
for spellName, data in pairs(spells) do
|
|
if data.pending then
|
|
local timeleft = (data.startTime + data.duration) - now
|
|
if timeleft < -2 then
|
|
spells[spellName] = nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if now - lastRangeCheck < 10 then return end
|
|
lastRangeCheck = now
|
|
|
|
local allGuids = {}
|
|
for guid in pairs(ownDebuffs) do allGuids[guid] = true end
|
|
for guid in pairs(slotOwnership) do allGuids[guid] = true end
|
|
for guid in pairs(allAuraCasts) do allGuids[guid] = true end
|
|
for guid in pairs(objectsByGuid) do allGuids[guid] = true end
|
|
for guid in pairs(pendingCasts) do allGuids[guid] = true end
|
|
|
|
for guid in pairs(allGuids) do
|
|
local exists = UnitExists and UnitExists(guid)
|
|
local isDead = UnitIsDead and UnitIsDead(guid)
|
|
|
|
if not exists or isDead then
|
|
CleanupUnit(guid)
|
|
end
|
|
end
|
|
|
|
-- Cleanup old lastCastRanks
|
|
for spell, data in pairs(lastCastRanks) do
|
|
if now - data.time > 3 then
|
|
lastCastRanks[spell] = nil
|
|
end
|
|
end
|
|
|
|
-- Cleanup old lastFailedSpells
|
|
for spell, data in pairs(lastFailedSpells) do
|
|
if now - data.time > 2 then
|
|
lastFailedSpells[spell] = nil
|
|
end
|
|
end
|
|
|
|
-- Cleanup old pendingCasts
|
|
for guid, spells in pairs(pendingCasts) do
|
|
for spell, data in pairs(spells) do
|
|
if now - data.time > 1 then
|
|
pendingCasts[guid][spell] = nil
|
|
end
|
|
end
|
|
local isEmpty = true
|
|
for _ in pairs(pendingCasts[guid]) do
|
|
isEmpty = false
|
|
break
|
|
end
|
|
if isEmpty then
|
|
pendingCasts[guid] = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ============================================================================
|
|
-- DURATION FUNCTIONS
|
|
-- ============================================================================
|
|
|
|
function libdebuff:GetDuration(effect, rank)
|
|
if L["debuffs"][effect] then
|
|
local rank = rank and tonumber((string.gsub(rank, RANK, ""))) or 0
|
|
local rank = L["debuffs"][effect][rank] and rank or libdebuff:GetMaxRank(effect)
|
|
local duration = L["debuffs"][effect][rank]
|
|
|
|
if effect == L["dyndebuffs"]["Demoralizing Shout"] then
|
|
local _,_,_,_,count = GetTalentInfo(2,1)
|
|
if count and count > 0 then duration = duration + ( duration / 100 * (count*10)) end
|
|
elseif effect == L["dyndebuffs"]["Shadow Word: Pain"] then
|
|
local _,_,_,_,count = GetTalentInfo(3,4)
|
|
if count and count > 0 then duration = duration + count * 3 end
|
|
elseif effect == L["dyndebuffs"]["Frostbolt"] then
|
|
local _,_,_,_,count = GetTalentInfo(3,7)
|
|
if count and count > 0 then duration = duration + count end
|
|
elseif effect == L["dyndebuffs"]["Gouge"] then
|
|
local _,_,_,_,count = GetTalentInfo(3,3)
|
|
if count and count > 0 then duration = duration + (count*.5) end
|
|
end
|
|
return duration
|
|
else
|
|
return 0
|
|
end
|
|
end
|
|
|
|
function libdebuff:GetMaxRank(effect)
|
|
local max = 0
|
|
for id in pairs(L["debuffs"][effect]) do
|
|
if id > max then max = id end
|
|
end
|
|
return max
|
|
end
|
|
|
|
function libdebuff:UpdateDuration(unit, unitlevel, effect, duration)
|
|
if not unit or not effect or not duration then return end
|
|
unitlevel = unitlevel or 0
|
|
|
|
if libdebuff.objects[unit] and libdebuff.objects[unit][unitlevel] and libdebuff.objects[unit][unitlevel][effect] then
|
|
libdebuff.objects[unit][unitlevel][effect].duration = duration
|
|
end
|
|
end
|
|
|
|
function libdebuff:UpdateUnits()
|
|
if not pfUI.uf or not pfUI.uf.target then return end
|
|
pfUI.uf:RefreshUnit(pfUI.uf.target, "aura")
|
|
end
|
|
|
|
-- ============================================================================
|
|
-- LEGACY API (for turtle-wow.lua compatibility)
|
|
-- ============================================================================
|
|
|
|
libdebuff.pending = {}
|
|
libdebuff.objects = {}
|
|
|
|
function libdebuff:AddPending(unit, unitlevel, effect, duration, caster, rank)
|
|
if not unit or duration <= 0 then return end
|
|
if not L["debuffs"][effect] then return end
|
|
if libdebuff.pending[3] then return end
|
|
|
|
libdebuff.pending[1] = unit
|
|
libdebuff.pending[2] = unitlevel or 0
|
|
libdebuff.pending[3] = effect
|
|
libdebuff.pending[4] = duration
|
|
libdebuff.pending[5] = caster
|
|
libdebuff.pending[6] = rank
|
|
|
|
QueueFunction(libdebuff.PersistPending)
|
|
end
|
|
|
|
function libdebuff:RemovePending()
|
|
libdebuff.pending[1] = nil
|
|
libdebuff.pending[2] = nil
|
|
libdebuff.pending[3] = nil
|
|
libdebuff.pending[4] = nil
|
|
libdebuff.pending[5] = nil
|
|
libdebuff.pending[6] = nil
|
|
end
|
|
|
|
function libdebuff:PersistPending(effect)
|
|
if not libdebuff.pending[3] then return end
|
|
|
|
if libdebuff.pending[3] == effect or ( effect == nil and libdebuff.pending[3] ) then
|
|
local p1, p2, p3, p4, p5, p6 = libdebuff.pending[1], libdebuff.pending[2], libdebuff.pending[3], libdebuff.pending[4], libdebuff.pending[5], libdebuff.pending[6]
|
|
libdebuff.AddEffect(libdebuff, p1, p2, p3, p4, p5, p6)
|
|
end
|
|
|
|
libdebuff:RemovePending()
|
|
end
|
|
|
|
function libdebuff:AddEffect(unit, unitlevel, effect, duration, caster, rank)
|
|
if not rank and caster == "player" and effect then
|
|
if libdebuff.pending[3] == effect and libdebuff.pending[6] then
|
|
rank = libdebuff.pending[6]
|
|
elseif lastCastRanks[effect] and (GetTime() - lastCastRanks[effect].time) < 2 then
|
|
rank = lastCastRanks[effect].rank
|
|
end
|
|
end
|
|
|
|
if not unit then return end
|
|
unitlevel = unitlevel or 0
|
|
|
|
-- Create tables if needed
|
|
libdebuff.objects[unit] = libdebuff.objects[unit] or {}
|
|
libdebuff.objects[unit][unitlevel] = libdebuff.objects[unit][unitlevel] or {}
|
|
|
|
-- Get duration from spell database if not provided
|
|
if not duration or duration == 0 then
|
|
duration = libdebuff:GetDuration(effect, rank)
|
|
end
|
|
|
|
-- Store/update effect
|
|
local now = GetTime()
|
|
local existing = libdebuff.objects[unit][unitlevel][effect]
|
|
|
|
if existing then
|
|
existing.start = now
|
|
existing.duration = duration
|
|
existing.caster = caster
|
|
existing.rank = rank
|
|
else
|
|
libdebuff.objects[unit][unitlevel][effect] = {
|
|
start = now,
|
|
duration = duration,
|
|
caster = caster,
|
|
rank = rank
|
|
}
|
|
end
|
|
|
|
lastspell = libdebuff.objects[unit][unitlevel][effect]
|
|
end
|
|
|
|
-- ============================================================================
|
|
-- API: GetBestAuraCast (for libpredict HoT tracking)
|
|
-- ============================================================================
|
|
|
|
function libdebuff:GetBestAuraCast(guid, spellName)
|
|
if not guid or not spellName then return nil end
|
|
|
|
-- Check ownDebuffs first (for our casts)
|
|
if ownDebuffs[guid] and ownDebuffs[guid][spellName] then
|
|
local data = ownDebuffs[guid][spellName]
|
|
local timeleft = (data.startTime + data.duration) - GetTime()
|
|
if timeleft > 0 then
|
|
return data.startTime, data.duration, timeleft, data.rank, GetPlayerGuid()
|
|
end
|
|
end
|
|
|
|
-- Check allAuraCasts (for any caster)
|
|
if allAuraCasts[guid] and allAuraCasts[guid][spellName] then
|
|
local bestData = nil
|
|
local bestCaster = nil
|
|
local bestTimeleft = 0
|
|
|
|
for casterGuid, data in pairs(allAuraCasts[guid][spellName]) do
|
|
local timeleft = (data.startTime + data.duration) - GetTime()
|
|
if timeleft > bestTimeleft then
|
|
bestTimeleft = timeleft
|
|
bestData = data
|
|
bestCaster = casterGuid
|
|
end
|
|
end
|
|
|
|
if bestData and bestTimeleft > 0 then
|
|
return bestData.startTime, bestData.duration, bestTimeleft, bestData.rank, bestCaster
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
-- ============================================================================
|
|
-- NAMPOWER EVENT HANDLING
|
|
-- ============================================================================
|
|
|
|
if hasNampower then
|
|
-- Carnage Talent Rank
|
|
local carnageRank = 0
|
|
local function UpdateCarnageRank()
|
|
if class ~= "DRUID" then return end
|
|
local _, _, _, _, rank = GetTalentInfo(2, 17)
|
|
carnageRank = rank or 0
|
|
end
|
|
|
|
-- Persistent Carnage check frame (reused instead of CreateFrame per Bite)
|
|
local carnageState = nil -- {targetGuid, checkTime}
|
|
local carnageCheckFrame = CreateFrame("Frame")
|
|
carnageCheckFrame:Hide()
|
|
carnageCheckFrame:SetScript("OnUpdate", function()
|
|
if not carnageState then
|
|
this:Hide()
|
|
return
|
|
end
|
|
if GetTime() < carnageState.checkTime then return end
|
|
|
|
-- Check if we gained a combo point (indicates Carnage proc)
|
|
local cp = GetComboPoints() or 0
|
|
|
|
if cp > 0 then
|
|
-- Carnage triggered! Refresh Rip & Rake
|
|
local guid = carnageState.targetGuid
|
|
local refreshTime = GetTime()
|
|
local myGuid = GetPlayerGuid()
|
|
|
|
-- Refresh in ownDebuffs - only if timer still active
|
|
if ownDebuffs[guid] then
|
|
if ownDebuffs[guid]["Rip"] then
|
|
local timeleft = (ownDebuffs[guid]["Rip"].startTime + ownDebuffs[guid]["Rip"].duration) - refreshTime
|
|
if timeleft > 0 then
|
|
ownDebuffs[guid]["Rip"].startTime = refreshTime
|
|
if debugStats.enabled then
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff[CARNAGE]|r Rip refreshed (CP detected)")
|
|
end
|
|
end
|
|
end
|
|
if ownDebuffs[guid]["Rake"] then
|
|
local timeleft = (ownDebuffs[guid]["Rake"].startTime + ownDebuffs[guid]["Rake"].duration) - refreshTime
|
|
if timeleft > 0 then
|
|
ownDebuffs[guid]["Rake"].startTime = refreshTime
|
|
if debugStats.enabled then
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff[CARNAGE]|r Rake refreshed (CP detected)")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Refresh in allAuraCasts - only if timer still active
|
|
if allAuraCasts[guid] then
|
|
if allAuraCasts[guid]["Rip"] and allAuraCasts[guid]["Rip"][myGuid] then
|
|
local d = allAuraCasts[guid]["Rip"][myGuid]
|
|
if (d.startTime + d.duration) > refreshTime then
|
|
d.startTime = refreshTime
|
|
end
|
|
end
|
|
if allAuraCasts[guid]["Rake"] and allAuraCasts[guid]["Rake"][myGuid] then
|
|
local d = allAuraCasts[guid]["Rake"][myGuid]
|
|
if (d.startTime + d.duration) > refreshTime then
|
|
d.startTime = refreshTime
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Trigger UI updates
|
|
if pfTarget and UnitGUID("target") then
|
|
local currentTargetGuid = UnitGUID("target")
|
|
if currentTargetGuid == guid then
|
|
pfTarget.update_aura = true
|
|
end
|
|
end
|
|
end
|
|
|
|
carnageState = nil
|
|
this:Hide()
|
|
end)
|
|
|
|
local frame = CreateFrame("Frame")
|
|
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
|
|
frame:RegisterEvent("PLAYER_TALENT_UPDATE")
|
|
frame:RegisterEvent("PLAYER_LOGOUT")
|
|
frame:RegisterEvent("SPELLCAST_CHANNEL_STOP")
|
|
frame:RegisterEvent("SPELL_START_SELF")
|
|
frame:RegisterEvent("SPELL_START_OTHER")
|
|
frame:RegisterEvent("SPELL_GO_SELF")
|
|
frame:RegisterEvent("SPELL_GO_OTHER")
|
|
frame:RegisterEvent("SPELL_FAILED_OTHER")
|
|
frame:RegisterEvent("UNIT_DIED")
|
|
frame:RegisterEvent("SPELL_CAST_EVENT")
|
|
frame:RegisterEvent("AURA_CAST_ON_SELF")
|
|
frame:RegisterEvent("AURA_CAST_ON_OTHER")
|
|
frame:RegisterEvent("DEBUFF_ADDED_OTHER")
|
|
frame:RegisterEvent("DEBUFF_REMOVED_OTHER")
|
|
frame:RegisterEvent("PLAYER_TARGET_CHANGED")
|
|
frame:RegisterEvent("UNIT_HEALTH")
|
|
|
|
frame:SetScript("OnEvent", function()
|
|
if event == "PLAYER_LOGOUT" then
|
|
this:UnregisterAllEvents()
|
|
this:SetScript("OnEvent", nil)
|
|
return
|
|
|
|
elseif event == "SPELLCAST_CHANNEL_STOP" then
|
|
-- 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 = 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]
|
|
local remaining = (data.startTime + data.duration) - GetTime()
|
|
if remaining > 0 then
|
|
ownDebuffs[targetGuid][spellName] = nil
|
|
end
|
|
end
|
|
end
|
|
elseif event == "PLAYER_ENTERING_WORLD" or event == "PLAYER_TALENT_UPDATE" then
|
|
UpdateCarnageRank()
|
|
elseif event == "UNIT_HEALTH" then
|
|
local guid = arg1
|
|
if guid and UnitIsDead and UnitIsDead(guid) then
|
|
CleanupUnit(guid)
|
|
end
|
|
if pfUI.libdebuff_unit_health_hooks then
|
|
for _, fn in pairs(pfUI.libdebuff_unit_health_hooks) do
|
|
fn(arg1)
|
|
end
|
|
end
|
|
|
|
elseif event == "SPELL_START_SELF" or event == "SPELL_START_OTHER" then
|
|
local spellId = arg2
|
|
local casterGuid = arg3
|
|
-- 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
|
|
if not casterGuid or not spellId then return end
|
|
|
|
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)
|
|
end
|
|
elseif event == "SPELL_START_OTHER" and pfUI.libdebuff_spell_start_other_hooks then
|
|
for _, fn in pairs(pfUI.libdebuff_spell_start_other_hooks) do
|
|
fn(spellId, casterGuid, arg4, castTime)
|
|
end
|
|
end
|
|
|
|
elseif event == "SPELL_GO_SELF" or event == "SPELL_GO_OTHER" then
|
|
local itemId = arg1
|
|
local spellId = arg2
|
|
local casterGuid = arg3
|
|
local targetGuid = arg4
|
|
local numHit = arg6 or 0
|
|
local numMissed = arg7 or 0
|
|
|
|
-- 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
|
|
for _, fn in pairs(pfUI.libdebuff_spell_go_hooks) do
|
|
fn(spellId, arg1, arg2, arg3, arg4, arg5, arg6, arg7)
|
|
end
|
|
end
|
|
|
|
if numMissed > 0 or numHit == 0 then return end
|
|
|
|
local spellName = C_Spell.GetSpellName(spellId)
|
|
if not spellName then return end
|
|
local spellRankString = C_Spell.GetSpellSubtext(spellId)
|
|
|
|
local castRank = 0
|
|
if spellRankString and spellRankString ~= "" then
|
|
castRank = tonumber((string.gsub(spellRankString, "Rank ", ""))) or 0
|
|
end
|
|
|
|
-- Store in pendingCasts for DEBUFF_ADDED correlation.
|
|
-- If this cast is a downrank of an already active debuff, fire the downrank blocked hook
|
|
-- so external addons (e.g. SuperCleveRoidMacros) don't need to re-implement this check.
|
|
if targetGuid then
|
|
pendingCasts[targetGuid] = pendingCasts[targetGuid] or {}
|
|
local isDownrankBlocked = false
|
|
if castRank > 0 then
|
|
local existingOwn = ownDebuffs[targetGuid] and ownDebuffs[targetGuid][spellName]
|
|
if existingOwn and existingOwn.rank and existingOwn.rank > castRank then
|
|
local existingTimeleft = (existingOwn.startTime + existingOwn.duration) - GetTime()
|
|
if existingTimeleft > 0 then
|
|
isDownrankBlocked = true
|
|
-- Fire hook so external addons know this cast was downrank-blocked
|
|
if pfUI.libdebuff_downrank_blocked_hooks then
|
|
for _, fn in pairs(pfUI.libdebuff_downrank_blocked_hooks) do
|
|
fn(spellName, castRank, existingOwn.rank, targetGuid, casterGuid)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
-- Always write pendingCasts — let consumers use the hook to handle downrank themselves
|
|
pendingCasts[targetGuid][spellName] = {
|
|
casterGuid = casterGuid,
|
|
rank = castRank,
|
|
time = GetTime(),
|
|
downrankBlocked = isDownrankBlocked
|
|
}
|
|
end
|
|
|
|
-- selfdebuff mode: write ownDebuffs immediately on confirmed hit
|
|
-- so buffwatch shows our debuffs even when over the 16 debuff cap
|
|
if event == "SPELL_GO_SELF" and targetGuid and not isNullTarget then
|
|
local selfdebuffMode = pfUI_config and pfUI_config.buffbar and
|
|
pfUI_config.buffbar.tdebuff and pfUI_config.buffbar.tdebuff.selfdebuff == "1"
|
|
if selfdebuffMode then
|
|
local myGuid2 = GetPlayerGuid()
|
|
if casterGuid == myGuid2 then
|
|
local duration = libdebuff:GetDuration(spellName, castRank) or 0
|
|
if duration > 0 then
|
|
local texture = libdebuff:GetSpellIcon(spellId)
|
|
ownDebuffs[targetGuid] = ownDebuffs[targetGuid] or {}
|
|
-- Downrank protection
|
|
local existing = ownDebuffs[targetGuid][spellName]
|
|
local blocked = false
|
|
if existing and existing.rank and castRank > 0 and existing.rank > castRank then
|
|
local existingTimeleft = (existing.startTime + existing.duration) - GetTime()
|
|
if existingTimeleft > 0 then blocked = true end
|
|
end
|
|
if not blocked then
|
|
ownDebuffs[targetGuid][spellName] = {
|
|
startTime = GetTime(),
|
|
duration = duration,
|
|
texture = texture,
|
|
rank = castRank,
|
|
spellId = spellId,
|
|
stacks = 1,
|
|
pending = true,
|
|
}
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Store rank for our casts
|
|
local myGuid = GetPlayerGuid()
|
|
if casterGuid == myGuid then
|
|
lastCastRanks[spellName] = {
|
|
rank = castRank,
|
|
time = GetTime()
|
|
}
|
|
end
|
|
|
|
-- CARNAGE TALENT: Ferocious Bite refreshes Rip & Rake
|
|
-- Check for combo point gain after Bite (indicates Carnage proc)
|
|
-- Carnage gives +1 CP immediately after Bite if it procs
|
|
if class == "DRUID" and carnageRank >= 1 and spellName == "Ferocious Bite" and casterGuid == myGuid then
|
|
if targetGuid and numHit > 0 then
|
|
-- Schedule delayed check (50ms to allow CP to register)
|
|
carnageState = {
|
|
targetGuid = targetGuid,
|
|
checkTime = GetTime() + 0.05
|
|
}
|
|
carnageCheckFrame:Show()
|
|
end
|
|
end
|
|
|
|
-- Fire registered SPELL_GO_OTHER hooks
|
|
if event == "SPELL_GO_OTHER" and pfUI.libdebuff_spell_go_other_hooks then
|
|
for _, fn in pairs(pfUI.libdebuff_spell_go_other_hooks) do
|
|
fn(spellId, casterGuid, targetGuid)
|
|
end
|
|
end
|
|
|
|
elseif event == "UNIT_DIED" then
|
|
if pfUI.libdebuff_unit_died_hooks then
|
|
for _, fn in pairs(pfUI.libdebuff_unit_died_hooks) do
|
|
fn(arg1)
|
|
end
|
|
end
|
|
|
|
elseif event == "SPELL_FAILED_OTHER" then
|
|
local casterGuid = arg1
|
|
if pfUI.libdebuff_spell_failed_other_hooks then
|
|
for _, fn in pairs(pfUI.libdebuff_spell_failed_other_hooks) do
|
|
fn(casterGuid, arg2)
|
|
end
|
|
end
|
|
|
|
elseif event == "SPELL_CAST_EVENT" then
|
|
-- Capture combo points BEFORE they're consumed
|
|
-- This event fires when YOU cast a spell (before server processes it)
|
|
local success = arg1
|
|
local spellId = arg2
|
|
local castType = arg3
|
|
local targetGuid = arg4
|
|
|
|
if success ~= 1 or not spellId then return end
|
|
|
|
local spellName = C_Spell.GetSpellName(spellId)
|
|
|
|
-- Store pending cast info for libpredict (heal prediction target tracking)
|
|
-- This allows libpredict to resolve the correct target for Nampower queued casts,
|
|
-- where CastSpellByName hook fires while current_cast is set and spell_queue
|
|
-- cannot be updated. SPELL_CAST_EVENT fires right before SPELLCAST_START.
|
|
if spellName and targetGuid and targetGuid ~= "" and targetGuid ~= "0x0000000000000000" then
|
|
pfUI.libpredict_pending_cast.spellId = spellId
|
|
pfUI.libpredict_pending_cast.spellName = spellName
|
|
pfUI.libpredict_pending_cast.targetGuid = targetGuid
|
|
pfUI.libpredict_pending_cast.time = GetTime()
|
|
else
|
|
-- No explicit target - clear pending so libpredict falls back to spell_queue
|
|
pfUI.libpredict_pending_cast.spellId = nil
|
|
pfUI.libpredict_pending_cast.spellName = nil
|
|
pfUI.libpredict_pending_cast.targetGuid = nil
|
|
pfUI.libpredict_pending_cast.time = nil
|
|
end
|
|
|
|
-- Fire registered SPELL_CAST_EVENT hooks
|
|
if pfUI.libdebuff_spell_cast_hooks then
|
|
for _, fn in pairs(pfUI.libdebuff_spell_cast_hooks) do
|
|
fn(success, spellId, castType, targetGuid)
|
|
end
|
|
end
|
|
|
|
elseif event == "AURA_CAST_ON_SELF" or event == "AURA_CAST_ON_OTHER" then
|
|
local spellId = arg1
|
|
local casterGuid = arg2
|
|
local targetGuid = arg3
|
|
local effect = arg4
|
|
local effectAuraName = arg5
|
|
local effectAmplitude = arg6
|
|
local effectMiscValue = arg7
|
|
local durationMs = arg8
|
|
local auraCapStatus = arg9
|
|
|
|
if not spellId then return end
|
|
if not targetGuid or targetGuid == "" or targetGuid == "0x0000000000000000" then return end
|
|
|
|
local spellName = C_Spell.GetSpellName(spellId)
|
|
if not spellName then return end
|
|
|
|
-- Deduplicate: Ignore if we processed this exact cast recently (within 100ms)
|
|
-- Nampower fires multiple AURA_CAST events for multi-effect spells (e.g. Faerie Fire has 3 effects)
|
|
recentCasts[targetGuid] = recentCasts[targetGuid] or {}
|
|
recentCasts[targetGuid][spellName] = recentCasts[targetGuid][spellName] or {}
|
|
|
|
local now = GetTime()
|
|
local lastCastTime = recentCasts[targetGuid][spellName][casterGuid]
|
|
|
|
if lastCastTime and (now - lastCastTime) < AURA_CAST_DEDUPE_WINDOW then
|
|
return -- Duplicate event, ignore
|
|
end
|
|
|
|
recentCasts[targetGuid][spellName][casterGuid] = now
|
|
|
|
-- Rank aus spellId ermitteln
|
|
local rankNum = 0
|
|
local rankString = C_Spell.GetSpellSubtext(spellId)
|
|
if rankString and rankString ~= "" then
|
|
rankNum = tonumber((string.gsub(rankString, "Rank ", ""))) or 0
|
|
end
|
|
|
|
local duration = durationMs and (durationMs / 1000) or 0
|
|
local startTime = GetTime()
|
|
local myGuid = GetPlayerGuid()
|
|
local isOurs = (myGuid and casterGuid == myGuid)
|
|
|
|
if debugStats.enabled and isOurs then
|
|
debugStats.aura_cast = debugStats.aura_cast + 1
|
|
end
|
|
|
|
-- Duration comes from nampower's AURA_CAST event. ClassicAPI's
|
|
-- C_UnitAuras now resolves combo-scaled durations server-side, so
|
|
-- libdebuff only needs the database fallback when AURA_CAST reports 0.
|
|
if duration == 0 then
|
|
duration = libdebuff:GetDuration(spellName, rankNum) or 0
|
|
end
|
|
|
|
-- Store in allAuraCasts
|
|
if targetGuid and targetGuid ~= "" and targetGuid ~= "0x0000000000000000" then
|
|
allAuraCasts[targetGuid] = allAuraCasts[targetGuid] or {}
|
|
allAuraCasts[targetGuid][spellName] = allAuraCasts[targetGuid][spellName] or {}
|
|
|
|
-- Downrank Protection: Check BEFORE clearing old casters!
|
|
-- For selfOverwrite debuffs, check ALL existing casters
|
|
if selfOverwriteDebuffs[spellName] then
|
|
for otherCaster, existingData in pairs(allAuraCasts[targetGuid][spellName]) do
|
|
if existingData.rank and rankNum and rankNum > 0 then
|
|
local existingTimeleft = (existingData.startTime + existingData.duration) - GetTime()
|
|
if existingTimeleft > 0 and rankNum < existingData.rank then
|
|
-- Lower rank cannot overwrite higher rank - block the update
|
|
if debugStats.enabled then
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cffff0000[DOWNRANK BLOCKED]|r %s: Rank %d from %s cannot overwrite Rank %d from %s (%.1fs left)",
|
|
spellName, rankNum, DebugGuid(casterGuid), existingData.rank, DebugGuid(otherCaster), existingTimeleft))
|
|
end
|
|
return
|
|
end
|
|
end
|
|
end
|
|
else
|
|
-- For non-selfOverwrite: Check only same caster
|
|
local existingData = allAuraCasts[targetGuid][spellName][casterGuid]
|
|
if existingData and existingData.rank and rankNum and rankNum > 0 then
|
|
local existingTimeleft = (existingData.startTime + existingData.duration) - GetTime()
|
|
if existingTimeleft > 0 and rankNum < existingData.rank then
|
|
if debugStats.enabled and isOurs then
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cffff0000[DOWNRANK BLOCKED]|r %s: Rank %d cannot overwrite Rank %d (%.1fs left)",
|
|
spellName, rankNum, existingData.rank, existingTimeleft))
|
|
end
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Handle self-overwrite debuffs (clear other casters)
|
|
if selfOverwriteDebuffs[spellName] then
|
|
local n = 0
|
|
for otherCaster in pairs(allAuraCasts[targetGuid][spellName]) do
|
|
if otherCaster ~= casterGuid then
|
|
n = n + 1
|
|
_cleanupBuf1[n] = otherCaster
|
|
end
|
|
end
|
|
for i = 1, n do
|
|
allAuraCasts[targetGuid][spellName][_cleanupBuf1[i]] = nil
|
|
_cleanupBuf1[i] = nil
|
|
end
|
|
|
|
-- Clear from ownDebuffs if we're being overwritten
|
|
if not isOurs and ownDebuffs[targetGuid] and ownDebuffs[targetGuid][spellName] then
|
|
ownDebuffs[targetGuid][spellName] = nil
|
|
end
|
|
end
|
|
|
|
-- Handle variant pairs (Faerie Fire <-> Faerie Fire (Feral))
|
|
if debuffOverwritePairs[spellName] then
|
|
local otherVariant = debuffOverwritePairs[spellName]
|
|
if allAuraCasts[targetGuid][otherVariant] and allAuraCasts[targetGuid][otherVariant][casterGuid] then
|
|
allAuraCasts[targetGuid][otherVariant][casterGuid] = nil
|
|
end
|
|
end
|
|
|
|
-- Store timer data
|
|
allAuraCasts[targetGuid][spellName][casterGuid] = {
|
|
startTime = startTime,
|
|
duration = duration,
|
|
rank = rankNum
|
|
}
|
|
|
|
-- UPDATE slotOwnership for selfOverwrite refreshes
|
|
-- (DEBUFF_ADDED doesn't fire on refresh, so we must update here!)
|
|
if selfOverwriteDebuffs[spellName] and slotOwnership[targetGuid] then
|
|
for auraSlot, ownership in pairs(slotOwnership[targetGuid]) do
|
|
if ownership.spellName == spellName then
|
|
-- Update the casterGuid and isOurs for this slot
|
|
ownership.casterGuid = casterGuid
|
|
ownership.isOurs = isOurs
|
|
|
|
if debugStats.enabled and IsCurrentTarget(targetGuid) then
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[SLOT UPDATED]|r aura=%d %s newCaster=%s isOurs=%s",
|
|
auraSlot, spellName, DebugGuid(casterGuid), tostring(isOurs)))
|
|
end
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
if debugStats.enabled and IsCurrentTarget(targetGuid) then
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format("%s |cff00ffff[AURA_CAST]|r %s target=%s caster=%s isOurs=%s dur=%.1fs",
|
|
GetDebugTimestamp(), spellName, DebugGuid(targetGuid), DebugGuid(casterGuid), tostring(isOurs), duration))
|
|
end
|
|
|
|
end
|
|
|
|
-- Notify unitframes of debuff updates (UNIT_AURA doesn't fire on refreshes!)
|
|
-- Check player
|
|
if IsPlayerGuid(targetGuid) and pfPlayer then
|
|
pfPlayer.update_aura = true
|
|
end
|
|
|
|
-- Check target
|
|
if UnitExists("target") then
|
|
local tarUnitGUID = UnitGUID("target")
|
|
if tarUnitGUID == targetGuid and pfTarget then
|
|
pfTarget.update_aura = true
|
|
end
|
|
end
|
|
|
|
-- Only track in ownDebuffs if it's OUR debuff
|
|
if not isOurs then return end
|
|
if targetGuid == myGuid then return end -- Skip self-buffs
|
|
if not targetGuid or targetGuid == "" or targetGuid == "0x0000000000000000" then return end
|
|
|
|
-- Get texture
|
|
local texture = libdebuff:GetSpellIcon(spellId)
|
|
|
|
-- Write ownDebuffs if:
|
|
-- (a) Entry already exists (refresh of active debuff), OR
|
|
-- (b) pendingCasts has an entry for this spell (SPELL_GO confirmed a hit is in-flight,
|
|
-- not a miss). This allows the downrank hook in SPELL_GO to fire correctly on first cast.
|
|
-- Without (b), ownDebuffs is empty until DEBUFF_ADDED (~300ms later), meaning the downrank
|
|
-- check in SPELL_GO has no data to work with on the first cast.
|
|
local entryExists = ownDebuffs[targetGuid] and ownDebuffs[targetGuid][spellName]
|
|
local pendingConfirm = pendingCasts[targetGuid] and pendingCasts[targetGuid][spellName]
|
|
if not entryExists and not pendingConfirm then return end
|
|
|
|
ownDebuffs[targetGuid] = ownDebuffs[targetGuid] or {}
|
|
local data = ownDebuffs[targetGuid][spellName]
|
|
-- For first-cast path (pendingConfirm but no existing entry): create a new entry
|
|
if not data then
|
|
if not pendingConfirm then return end -- race condition: cleared by DEBUFF_REMOVED
|
|
data = {}
|
|
ownDebuffs[targetGuid][spellName] = data
|
|
end
|
|
|
|
-- Downrank Protection: Check if existing debuff is still active and has higher rank
|
|
if data.startTime and data.duration and data.rank and rankNum > 0 then
|
|
local existingTimeleft = (data.startTime + data.duration) - GetTime()
|
|
if existingTimeleft > 0 then
|
|
-- Existing debuff is still active
|
|
if rankNum < data.rank then
|
|
-- Lower rank cannot overwrite higher rank - block the update
|
|
if debugStats.enabled then
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cffff0000[DOWNRANK BLOCKED]|r %s: Rank %d cannot overwrite Rank %d (%.1fs left)",
|
|
spellName, rankNum, data.rank, existingTimeleft))
|
|
end
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
data.startTime = startTime
|
|
data.duration = duration
|
|
data.texture = texture
|
|
data.rank = rankNum
|
|
data.spellId = spellId
|
|
|
|
-- Handle variant pairs for ownDebuffs
|
|
if debuffOverwritePairs[spellName] then
|
|
local otherVariant = debuffOverwritePairs[spellName]
|
|
if ownDebuffs[targetGuid][otherVariant] then
|
|
ownDebuffs[targetGuid][otherVariant] = nil
|
|
end
|
|
end
|
|
|
|
-- Store for Cleveroids API
|
|
objectsByGuid[targetGuid] = objectsByGuid[targetGuid] or {}
|
|
objectsByGuid[targetGuid][spellId] = {
|
|
start = startTime,
|
|
duration = duration,
|
|
caster = "player",
|
|
stacks = 1
|
|
}
|
|
if event == "AURA_CAST_ON_SELF" and pfUI.libdebuff_aura_cast_on_self_hooks then
|
|
for _, fn in pairs(pfUI.libdebuff_aura_cast_on_self_hooks) do
|
|
fn(spellId, casterGuid, targetGuid)
|
|
end
|
|
elseif event == "AURA_CAST_ON_OTHER" and pfUI.libdebuff_aura_cast_on_other_hooks then
|
|
for _, fn in pairs(pfUI.libdebuff_aura_cast_on_other_hooks) do
|
|
fn(spellId, casterGuid, targetGuid)
|
|
end
|
|
end
|
|
|
|
elseif event == "DEBUFF_ADDED_OTHER" then
|
|
local guid = arg1
|
|
local displaySlot = arg2 -- Display slot (1-16), compacted
|
|
local spellId = arg3
|
|
local stacks = arg4
|
|
local auraSlot_0based = arg6 -- Nampower 2.29+: raw slot 0-based (32-47)
|
|
|
|
-- Convert 0-based (Nampower event) to 1-based (Lua GetUnitField array)
|
|
local auraSlot = auraSlot_0based and (auraSlot_0based + 1) or nil
|
|
|
|
-- Invalidate slot map cache for this GUID
|
|
|
|
local spellName = C_Spell.GetSpellName(spellId)
|
|
if not spellName then return end
|
|
|
|
if debugStats.enabled then
|
|
debugStats.debuff_added = debugStats.debuff_added + 1
|
|
end
|
|
|
|
-- If unit is dead, cleanup and skip
|
|
if UnitIsDead and UnitIsDead(guid) then
|
|
CleanupUnit(guid)
|
|
return
|
|
end
|
|
|
|
-- Get auraSlot from event parameter (Nampower 2.29+)
|
|
-- Fallback to GetUnitField lookup if not available
|
|
if not auraSlot then
|
|
local slotMap = GetDebuffSlotMap(guid)
|
|
if slotMap and slotMap[displaySlot] then
|
|
auraSlot = slotMap[displaySlot].auraSlot
|
|
end
|
|
end
|
|
|
|
-- Fallback: Calculate aura slot if GetUnitField didn't work
|
|
-- (This assumes no gaps, which isn't always true, but better than nothing)
|
|
if not auraSlot then
|
|
auraSlot = 32 + displaySlot
|
|
end
|
|
|
|
-- Get caster from pendingCasts (SPELL_GO correlation)
|
|
local casterGuid = nil
|
|
if pendingCasts[guid] and pendingCasts[guid][spellName] then
|
|
local pending = pendingCasts[guid][spellName]
|
|
if GetTime() - pending.time < 0.5 then
|
|
casterGuid = pending.casterGuid
|
|
pendingCasts[guid][spellName] = nil
|
|
end
|
|
end
|
|
|
|
-- Fallback: Check allAuraCasts for most recent caster
|
|
if not casterGuid and allAuraCasts[guid] and allAuraCasts[guid][spellName] then
|
|
local mostRecent = nil
|
|
local mostRecentTime = 0
|
|
for casterId, data in pairs(allAuraCasts[guid][spellName]) do
|
|
if data.startTime > mostRecentTime then
|
|
mostRecentTime = data.startTime
|
|
mostRecent = casterId
|
|
end
|
|
end
|
|
if mostRecent then
|
|
casterGuid = mostRecent
|
|
end
|
|
end
|
|
|
|
local myGuid = GetPlayerGuid()
|
|
local isOurs = (myGuid and casterGuid == myGuid)
|
|
|
|
-- Fallback: Check ownDebuffs timing
|
|
if not isOurs and not casterGuid then
|
|
if ownDebuffs[guid] and ownDebuffs[guid][spellName] then
|
|
local age = GetTime() - ownDebuffs[guid][spellName].startTime
|
|
if age < 0.5 then
|
|
isOurs = true
|
|
casterGuid = myGuid
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Store slot ownership (KEY: auraSlot is STABLE, no shifting needed!)
|
|
slotOwnership[guid] = slotOwnership[guid] or {}
|
|
slotOwnership[guid][auraSlot] = {
|
|
casterGuid = casterGuid,
|
|
spellName = spellName,
|
|
spellId = spellId,
|
|
isOurs = isOurs
|
|
}
|
|
|
|
-- Store displaySlot → auraSlot mapping for DEBUFF_REMOVED
|
|
displayToAura[guid] = displayToAura[guid] or {}
|
|
displayToAura[guid][displaySlot] = auraSlot
|
|
|
|
if debugStats.enabled and IsCurrentTarget(guid) then
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format("%s |cff00ff00[DEBUFF_ADDED]|r display=%d aura=%d %s caster=%s isOurs=%s",
|
|
GetDebugTimestamp(), displaySlot, auraSlot, spellName, DebugGuid(casterGuid), tostring(isOurs)))
|
|
end
|
|
|
|
-- CRITICAL FIX: Update ownDebuffs here too for refresh timing!
|
|
-- This prevents the gap between DEBUFF_REMOVED and AURA_CAST where buffwatch shows nothing
|
|
if isOurs and casterGuid then
|
|
if myGuid and casterGuid == myGuid then
|
|
-- Check if we have timer data from allAuraCasts
|
|
if allAuraCasts[guid] and allAuraCasts[guid][spellName] and allAuraCasts[guid][spellName][casterGuid] then
|
|
local auraData = allAuraCasts[guid][spellName][casterGuid]
|
|
local texture = libdebuff:GetSpellIcon(spellId)
|
|
|
|
ownDebuffs[guid] = ownDebuffs[guid] or {}
|
|
ownDebuffs[guid][spellName] = {
|
|
startTime = auraData.startTime,
|
|
duration = auraData.duration,
|
|
texture = texture,
|
|
rank = auraData.rank or 0,
|
|
spellId = spellId
|
|
}
|
|
|
|
if debugStats.enabled and IsCurrentTarget(guid) then
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cffff00ff[OWNDEBUFF SYNC]|r %s from DEBUFF_ADDED", spellName))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Cleanup expired timers
|
|
CleanupExpiredTimers(guid)
|
|
|
|
if pfUI.libdebuff_debuff_added_other_hooks then
|
|
for _, fn in pairs(pfUI.libdebuff_debuff_added_other_hooks) do
|
|
fn(arg1, arg2, arg3, arg4)
|
|
end
|
|
end
|
|
|
|
elseif event == "DEBUFF_REMOVED_OTHER" then
|
|
local guid = arg1
|
|
local displaySlot = arg2 -- Display slot (1-16), compacted
|
|
local spellId = arg3
|
|
local auraSlot_0based = arg6 -- Nampower 2.29+: raw slot 0-based (32-47)
|
|
|
|
-- Convert 0-based (Nampower event) to 1-based (Lua GetUnitField array)
|
|
local auraSlot = auraSlot_0based and (auraSlot_0based + 1) or nil
|
|
|
|
-- Invalidate slot map cache for this GUID
|
|
|
|
local spellName = C_Spell.GetSpellName(spellId) or "?"
|
|
|
|
if debugStats.enabled then
|
|
debugStats.debuff_removed = debugStats.debuff_removed + 1
|
|
if IsCurrentTarget(guid) then
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format("%s |cffff9900[DEBUFF_REMOVED]|r display=%d aura=%d (0based=%d) %s",
|
|
GetDebugTimestamp(), displaySlot, auraSlot or -1, auraSlot_0based or -1, spellName))
|
|
end
|
|
end
|
|
|
|
-- If unit is dead, cleanup all
|
|
if UnitIsDead and UnitIsDead(guid) then
|
|
CleanupUnit(guid)
|
|
return
|
|
end
|
|
|
|
-- Get auraSlot from event parameter (Nampower 2.29+)
|
|
-- Fallback to displayToAura mapping if not available
|
|
local foundAuraSlot = auraSlot
|
|
if not foundAuraSlot and displayToAura[guid] and displayToAura[guid][displaySlot] then
|
|
foundAuraSlot = displayToAura[guid][displaySlot]
|
|
end
|
|
|
|
local wasOurs = false
|
|
local removedCasterGuid = nil
|
|
|
|
if foundAuraSlot then
|
|
-- Get ownership info for this specific slot
|
|
if slotOwnership[guid] and slotOwnership[guid][foundAuraSlot] then
|
|
local ownership = slotOwnership[guid][foundAuraSlot]
|
|
wasOurs = ownership.isOurs
|
|
removedCasterGuid = ownership.casterGuid
|
|
end
|
|
|
|
-- Clear both mappings (with nil-checks)
|
|
if slotOwnership[guid] then
|
|
slotOwnership[guid][foundAuraSlot] = nil
|
|
end
|
|
if displayToAura[guid] then
|
|
displayToAura[guid][displaySlot] = nil
|
|
end
|
|
|
|
if debugStats.enabled and IsCurrentTarget(guid) then
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format("%s |cffff9900[SLOT CLEARED]|r aura=%d [arg6] %s wasOurs=%s caster=%s",
|
|
GetDebugTimestamp(), foundAuraSlot, spellName, tostring(wasOurs), DebugGuid(removedCasterGuid)))
|
|
end
|
|
end
|
|
|
|
-- Remove from ownDebuffs if it was ours
|
|
if wasOurs and ownDebuffs[guid] and ownDebuffs[guid][spellName] then
|
|
local age = GetTime() - ownDebuffs[guid][spellName].startTime
|
|
-- Only delete if not recently renewed
|
|
if age > 1 then
|
|
ownDebuffs[guid][spellName] = nil
|
|
end
|
|
end
|
|
|
|
-- Remove from allAuraCasts
|
|
if removedCasterGuid and allAuraCasts[guid] and allAuraCasts[guid][spellName] then
|
|
if allAuraCasts[guid][spellName][removedCasterGuid] then
|
|
local auraData = allAuraCasts[guid][spellName][removedCasterGuid]
|
|
local age = GetTime() - auraData.startTime
|
|
-- Only delete if not recently refreshed
|
|
if age > 1 then
|
|
allAuraCasts[guid][spellName][removedCasterGuid] = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Cleanup expired timers
|
|
CleanupExpiredTimers(guid)
|
|
|
|
if pfUI.libdebuff_debuff_removed_other_hooks then
|
|
for _, fn in pairs(pfUI.libdebuff_debuff_removed_other_hooks) do
|
|
fn(arg1, arg2, arg3, arg4)
|
|
end
|
|
end
|
|
|
|
elseif event == "PLAYER_TARGET_CHANGED" then
|
|
if not UnitGUID then return end
|
|
local targetGuid = UnitGUID("target")
|
|
|
|
if targetGuid and targetGuid ~= "" then
|
|
-- Invalidate slot map cache on retarget
|
|
-- Prevents stale slot mappings after untarget/retarget cycles
|
|
-- Cleanup expired timers for new target
|
|
CleanupExpiredTimers(targetGuid)
|
|
end
|
|
if pfUI.libdebuff_player_target_changed_hooks then
|
|
for _, fn in pairs(pfUI.libdebuff_player_target_changed_hooks) do
|
|
fn()
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Periodic cleanup
|
|
CleanupOutOfRangeUnits()
|
|
end)
|
|
|
|
-- Cleveroids API
|
|
if CleveRoids then
|
|
CleveRoids.libdebuff = libdebuff
|
|
libdebuff.objects = objectsByGuid
|
|
end
|
|
end
|
|
|
|
-- add libdebuff to pfUI API
|
|
pfUI.api.libdebuff = libdebuff
|
|
|
|
-- Expose debugStats for external access
|
|
libdebuff.debugStats = debugStats
|
|
|
|
-- ============================================================================
|
|
-- DEBUG COMMANDS
|
|
-- ============================================================================
|
|
|
|
_G.SLASH_LIBDEBUGSTATS1 = "/libdebugstats"
|
|
_G.SlashCmdList["LIBDEBUGSTATS"] = function(msg)
|
|
msg = string.lower(msg or "")
|
|
|
|
if msg == "start" then
|
|
debugStats.enabled = true
|
|
debugStats.trackAllUnits = false
|
|
debugStats.aura_cast = 0
|
|
debugStats.debuff_added = 0
|
|
debugStats.debuff_removed = 0
|
|
debugStats.getunitfield_calls = 0
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[libdebuff]|r Debug tracking STARTED")
|
|
|
|
elseif msg == "stop" then
|
|
debugStats.enabled = false
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cffff9900[libdebuff]|r Debug tracking STOPPED")
|
|
|
|
elseif msg == "stats" then
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff=== LIBDEBUFF STATS (GetUnitField Edition) ===|r")
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format("AURA_CAST events: %d", debugStats.aura_cast))
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format("DEBUFF_ADDED events: %d", debugStats.debuff_added))
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format("DEBUFF_REMOVED events: %d", debugStats.debuff_removed))
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format("GetUnitField calls: %d", debugStats.getunitfield_calls))
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00No manual slot shifting needed!|r")
|
|
|
|
elseif msg == "target" then
|
|
if not UnitGUID("target") then
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[libdebuff]|r No target!")
|
|
return
|
|
end
|
|
|
|
local guid = UnitGUID("target")
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff=== TARGET DEBUFF STATE ===|r")
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format("GUID: %s", tostring(guid)))
|
|
|
|
-- Show GetUnitField slot map
|
|
local slotMap = GetDebuffSlotMap(guid)
|
|
if slotMap then
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00GetUnitField Slots:|r")
|
|
for displaySlot, data in pairs(slotMap) do
|
|
local casterGuid, isOurs = GetSlotCaster(guid, data.auraSlot, data.spellName)
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format(" Display %d (aura %d): %s [caster=%s, ours=%s]",
|
|
displaySlot, data.auraSlot, data.spellName, DebugGuid(casterGuid), tostring(isOurs)))
|
|
end
|
|
else
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cffff9900No debuffs via GetUnitField|r")
|
|
end
|
|
|
|
-- Show ownDebuffs
|
|
if ownDebuffs[guid] then
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00ownDebuffs:|r")
|
|
for spell, data in pairs(ownDebuffs[guid]) do
|
|
local timeleft = (data.startTime + data.duration) - GetTime()
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format(" %s: dur=%.1f left=%.1f", spell, data.duration, timeleft))
|
|
end
|
|
end
|
|
|
|
else
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff[libdebuff] GetUnitField Edition - Commands:|r")
|
|
DEFAULT_CHAT_FRAME:AddMessage(" /libdebugstats start - Start debug tracking")
|
|
DEFAULT_CHAT_FRAME:AddMessage(" /libdebugstats stop - Stop debug tracking")
|
|
DEFAULT_CHAT_FRAME:AddMessage(" /libdebugstats stats - Show statistics")
|
|
DEFAULT_CHAT_FRAME:AddMessage(" /libdebugstats target - Show target debuff state")
|
|
end
|
|
end
|
|
|
|
_G.SLASH_MEMCHECK1 = "/memcheck"
|
|
_G.SlashCmdList["MEMCHECK"] = function()
|
|
local function countTable(t)
|
|
local count = 0
|
|
if not t then return 0 end
|
|
for _ in pairs(t) do count = count + 1 end
|
|
return count
|
|
end
|
|
|
|
local function countNestedEntries(t)
|
|
local total = 0
|
|
if not t then return 0 end
|
|
for _, nested in pairs(t) do
|
|
if type(nested) == "table" then
|
|
total = total + countTable(nested)
|
|
end
|
|
end
|
|
return total
|
|
end
|
|
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff========== LIBDEBUFF MEMORY (GetUnitField Edition) ==========|r")
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00Primary Tables:|r"))
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format(" ownDebuffs: %d GUIDs, %d debuffs", countTable(ownDebuffs), countNestedEntries(ownDebuffs)))
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format(" slotOwnership: %d GUIDs, %d slots", countTable(slotOwnership), countNestedEntries(slotOwnership)))
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format(" allAuraCasts: %d GUIDs", countTable(allAuraCasts)))
|
|
DEFAULT_CHAT_FRAME:AddMessage(string.format(" pendingCasts: %d GUIDs", countTable(pendingCasts)))
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00No ownSlots/allSlots (eliminated by GetUnitField approach!)|r")
|
|
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff============================================================|r")
|
|
end
|