11 Commits

Author SHA1 Message Date
Brues de7dacf788 xpbar: route rep tracking through ClassicAPI faction APIs
Drop CHAT_MSG_COMBAT_FACTION_CHANGE + SanitizePattern(FACTION_STANDING_
INCREASED) string parsing in favor of FACTION_STANDING_CHANGED, which
ships (factionID, newStanding, repGained) directly — locale-independent,
no chat-string scrape. Track factionID instead of faction name.

Collapse the two for i=1, 99 GetFactionInfo loops (tooltip + bar fill)
into a single GetRepDisplay helper: GetFactionInfoByID for the remembered
faction, C_Reputation.GetWatchedFactionData for the watched one.

Watched-faction-changed detection now compares factionIDs.
2026-06-27 01:09:15 -05:00
Brues e0bcbbf2dc castbar: Quartz-style tradeskill merge with per-craft spark
Hook DoTradeSkill to capture the requested count, then on the first
SPELLCAST_START of an isTradeskill cast stretch endTime to span all
crafts so the player bar fills continuously across the chain. Mid-chain
SPELLCAST_START / SPELL_START_SELF events refresh the "(N)" remaining
label and reset a per-craft spark that crosses the bar once per craft.
SPELL_GO_SELF counts completions; SPELLCAST_STOP no-ops while merged.

Gated by a new C.castbar.player.mergetradeskill knob (default on).
2026-06-26 18:43:31 -05:00
Brues 65bbea6fa9 bump min again 2026-06-25 23:46:34 -05:00
Brues 822c873132 castbar: event-driven rewrite on ClassicAPI C_Spell + nampower events
Drive the cast bar from cast lifecycle events instead of polling C_Spell
every frame. OnUpdate now only animates a stamped start/end and fades out;
all state transitions come from events:

- player: vanilla SPELLCAST_START / _STOP / _FAILED / _INTERRUPTED /
  _CHANNEL_START / _CHANNEL_STOP, plus nampower SPELL_START_SELF (the only
  signal for a chained same-spell recast, which never runs the client cast
  path) and SPELL_DELAYED_SELF for pushback (applied from its delayMs arg).
- non-player (target/focus): nampower SPELL_START_OTHER / SPELL_FAILED_OTHER
  + PLAYER_TARGET_CHANGED / PLAYER_FOCUS_CHANGED.

Data comes from ClassicAPI's C_Spell.UnitCastingInfo / UnitChannelInfo
(player exact; other units from the SMSG_SPELL_START cache). SPELL_START_*
re-polls are deferred one frame so ClassicAPI's packet co-hook has stamped
before the read. SPELLCAST_CHANNEL_STOP only clears when a channel is shown,
so a lagged channel-stop doesn't wipe a following cast's bar.

Remove the now-unused CASTBAR_EVENT_* constants from compat/vanilla.lua.
2026-06-25 22:38:53 -05:00
Brues 96fd486187 bump min 2026-06-25 01:49:17 -05:00
Brues b79132b9f4 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.
2026-06-25 01:18:22 -05:00
Brues d62a1222df feigndeath: use UnitIsFeignDeath + GetUnitField, drop tooltip cache
ClassicAPI's UnitIsFeignDeath reads UNIT_FIELD_FLAGS bit 29 directly —
the authoritative server flag, no detection guesswork needed. Combined
with Nampower's GetUnitField("health"), we read live HP off the
descriptor instead of caching the moment-of-death healthbar value via
libtipscan.

Drops the name-keyed cache, the UNIT_HEALTH / PLAYER_TARGET_CHANGED
event handlers, and the tooltip scanner. Also fixes the staleness bug
where a feigning hunter taking further damage kept showing the cached
snapshot from when feign first triggered — live read updates with
every UnitHealth call now.
2026-06-24 22:49:43 -05:00
Brues be6ae25315 libdebuff/nameplates: drop dead per-aura readers and debuff cache
With every external caller of libdebuff:UnitDebuff / :UnitOwnDebuff
now on C_UnitAuras, the two public per-aura readers and the
nameplate-side cache they were feeding have no consumers.

- libs/libdebuff.lua: removes libdebuff:UnitDebuff (~120 lines),
  libdebuff:UnitOwnDebuff (~75 lines), the _ownDebuffSortFunc helper,
  and the local cache table. The slotOwnership / ownDebuffs /
  allAuraCasts / pendingCasts bookkeeping stays — GetBestAuraCast
  (libpredict) and GetEnhancedDebuffs (CleveRoids) still read it, and
  the event handlers maintain it. GetSlotCaster / GetDebuffSlotMap
  stay too; the DEBUFF_ADDED_OTHER handler and the debug printer use
  them. File goes 2010 → 1870 lines.
- modules/nameplates.lua: deletes PlateCacheDebuffs (was already
  rewritten on C_UnitAuras and unused once the display loop bypassed
  the cache), PlateUnitDebuff, the cachedVerify scaffolding, and the
  nameplate.UnitDebuff / nameplate.CacheDebuffs registrations.
- api/config.lua + modules/gui.lua: drops the now-defunct
  "guessdebuffs" knob — its only effect was gating the dead cache.
2026-06-24 22:41:36 -05:00
Brues d6951b386e unitframes/buffwatch/libpredict: route aura reads through C_UnitAuras
ClassicAPI's recent C_UnitAuras additions (sourceUnit / sourceGUID /
non-player expirationTime / the PLAYER filter token) finally cover
everything libdebuff:UnitDebuff and :UnitOwnDebuff were doing — caster
attribution, accurate timing for non-player units, and the own-debuffs
filter. Migrating the remaining callers off the libdebuff readers.

- api/unitframes.lua: collapses the debuff render path's three-branch
  if/else into a single C_UnitAuras.GetAuraDataByIndex with a HARMFUL
  or HARMFUL|PLAYER filter selected by the selfdebuff config; tooltip
  slot-finders match by sourceGUID instead of libdebuff's caster flag;
  custom-debuff indicator scan unifies the same way.
- modules/buffwatch.lua: GetBuffData drops the libdebuff fallback;
  tooltip slot-finder mirrors the unitframes pattern.
- libs/libpredict.lua: drops the orphaned UnitHasBuff slot-loop (no
  callers left).
- api/api.lua: pfUI.api.UnitHasBuff tightens from a HELPFUL iteration
  to a single GetAuraDataBySpellName lookup.

Each site that builds a cooldown ring from expirationTime carries the
talent-extension guard — when expirationTime exceeds the dbc base
duration (e.g. Shadow Affinity → SW:P), clamp start to now and use the
remaining time as the effective duration so CooldownFrame_SetTimer
doesn't get a future start it treats as "not yet begun".
2026-06-24 22:41:13 -05:00
Brues fd21b24a74 utilize C_UnitAuras for nameplate auras 2026-06-24 19:14:36 -05:00
Brues 90ac4f9de0 auto generate changelogs 2026-06-22 19:50:44 -05:00
20 changed files with 629 additions and 1660 deletions
-1
View File
@@ -1 +0,0 @@
manual-changelog: CHANGELOG.md
-11
View File
@@ -1,11 +0,0 @@
- Hard dependency on [ClassicAPI](https://github.com/brues-code/ClassicAPI) — modern `C_*` namespaces and engine polyfills (focus, nameplates, GUIDs).
- TBC/Wrath/expansion plumbing removed; vanilla 1.12 + Turtle WoW only.
- Nameplate overhaul — GUID-keyed caches, per-tick allocation cuts, name-collision filtering, configurable name text position.
- Unit auras rebuilt on `C_UnitAuras` — replaces the old tooltip-scraping aura tracker with structured engine data, so buff/debuff durations, stack counts, and source attribution are accurate without polling.
- Equipment manager — new backport of Blizzard's gear-set UI, integrated into the character pane. Save, swap, and edit sets without a third-party addon.
- Bag sorting — new built-in feature (previously pfUI only deferred to third-party sorters).
- `/focus` and `/clearfocus` moved to ClassicAPI; `/focusname` retained in pfUI.
- questitem refactored onto `C_QuestLog`.
- `updatenotify` isolated to its own `pfUI-brues` addon-message prefix so we don't share traffic with upstream pfUI installs.
See full commit history at https://github.com/brues-code/pfUI/commits/master for details.
+1 -4
View File
@@ -193,10 +193,7 @@ end
-- name [string] The localized name of the buff.
-- return: [bool] true if unit has buff otherwise "nil"
function pfUI.api.UnitHasBuff(unit, name)
for _, aura in ipairs(C_UnitAuras.GetUnitAuras(unit, "HELPFUL")) do
if aura.name == name then return true end
end
return nil
return C_UnitAuras.GetAuraDataBySpellName(unit, name, "HELPFUL") ~= nil or nil
end
-- [ GetUnbuffedRoster ]
+2 -1
View File
@@ -714,6 +714,7 @@ function pfUI:LoadConfig()
pfUI:UpdateConfig("castbar", "player", "txtleftoffy", "0")
pfUI:UpdateConfig("castbar", "player", "showlag", "0")
pfUI:UpdateConfig("castbar", "player", "showrank", "0")
pfUI:UpdateConfig("castbar", "player", "mergetradeskill", "1")
pfUI:UpdateConfig("castbar", "player", "txtrightoffx", "0")
pfUI:UpdateConfig("castbar", "player", "txtrightoffy", "0")
pfUI:UpdateConfig("castbar", "target", "hide_pfui", "0")
@@ -817,7 +818,7 @@ function pfUI:LoadConfig()
pfUI:UpdateConfig("nameplates", nil, "selfdebuff", "0")
pfUI:UpdateConfig("nameplates", nil, "showdebuffs_hostile", "1")
pfUI:UpdateConfig("nameplates", nil, "showdebuffs_friendly", "0")
pfUI:UpdateConfig("nameplates", nil, "guessdebuffs", "1")
pfUI:UpdateConfig("nameplates", nil, "owndebuffs", "0")
pfUI:UpdateConfig("nameplates", nil, "clickthrough", "0")
pfUI:UpdateConfig("nameplates", nil, "rightclick", "1")
pfUI:UpdateConfig("nameplates", nil, "clickthreshold", "0.5")
+27 -47
View File
@@ -87,13 +87,16 @@ local function DebuffOnEnter()
if this:GetParent().label ~= "player" then
local parent = this:GetParent()
-- For "only own debuffs" mode: find the REAL slot by matching spell name AND caster
if parent.config and parent.config.selfdebuff == "1" and libdebuff then
local ownDebuffName = libdebuff:UnitOwnDebuff(unitstr, this.id)
if ownDebuffName then
-- selfdebuff filters the displayed list to player-cast harmful auras, but
-- SetUnitAura's index has to be into the engine's full HARMFUL list. Look
-- up the displayed aura via the PLAYER filter, then scan engine slots for
-- one whose name + sourceGUID match.
if parent.config and parent.config.selfdebuff == "1" then
local ownAura = C_UnitAuras.GetAuraDataByIndex(unitstr, this.id, "HARMFUL|PLAYER")
if ownAura then
for gameSlot = 1, 16 do
local gameName, _, _, _, _, _, _, gameCaster = libdebuff:UnitDebuff(unitstr, gameSlot)
if gameName == ownDebuffName and gameCaster == "player" then
local check = C_UnitAuras.GetDebuffDataByIndex(unitstr, gameSlot)
if check and check.name == ownAura.name and check.sourceGUID == ownAura.sourceGUID then
GameTooltip:SetUnitAura(unitstr, gameSlot, "HARMFUL")
return
end
@@ -1971,16 +1974,15 @@ function pfUI.uf:RefreshUnit(unit, component)
invert_h * ((row+buffrow)*(multiply*default_border + unit.config.debuffsize + 1) + (multiply*default_border + 1)))
end
local aura
if unit.label ~= "player" and selfdebuff == "1" then
_, _, texture, stacks, dtype = libdebuff:UnitOwnDebuff(unitstr, i)
-- selfdebuff narrows to player-cast harmful auras via the PLAYER filter.
-- Player-frame debuffs aren't gated on it (it'd hide most party-applied
-- effects on you).
local filter = (unit.label ~= "player" and selfdebuff == "1") and "HARMFUL|PLAYER" or "HARMFUL"
local aura = C_UnitAuras.GetAuraDataByIndex(unitstr, i, filter)
if aura then
texture, stacks, dtype = aura.icon, aura.applications, aura.dispelName
else
aura = C_UnitAuras.GetDebuffDataByIndex(unitstr, i)
if aura then
texture, stacks, dtype = aura.icon, aura.applications, aura.dispelName
else
texture, stacks, dtype = nil, 0, nil
end
texture, stacks, dtype = nil, 0, nil
end
unit.debuffs[i].texture:SetTexture(texture)
@@ -1992,8 +1994,9 @@ function pfUI.uf:RefreshUnit(unit, component)
unit.debuffs[i]:Show()
if aura and aura.expirationTime > 0 then
-- Player path: real engine expirationTime. Cap start to now to keep pfUI's
-- cooldown text out of the 2^32-wraparound branch for talent-extended debuffs.
-- Cap start to now so talent-extended debuffs (expirationTime past
-- the dbc base duration) don't push start into the future and trip
-- CooldownFrame_SetTimer's 2^32-wraparound branch.
local now = GetTime()
local start = aura.expirationTime - aura.duration
local duration = aura.duration
@@ -2003,16 +2006,6 @@ function pfUI.uf:RefreshUnit(unit, component)
if duration > 0 then
CooldownFrame_SetTimer(unit.debuffs[i].cd, start, duration, 1)
end
elseif libdebuff and selfdebuff == "1" then
local _, _, _, _, _, duration, timeleft = libdebuff:UnitOwnDebuff(unitstr, i)
if duration and timeleft then
CooldownFrame_SetTimer(unit.debuffs[i].cd, GetTime() + timeleft - duration, duration, 1)
end
elseif libdebuff then
local _, _, _, _, _, duration, timeleft = libdebuff:UnitDebuff(unitstr, i)
if duration and timeleft then
CooldownFrame_SetTimer(unit.debuffs[i].cd, GetTime() + timeleft - duration, duration, 1)
end
end
if stacks > 1 then
@@ -2198,29 +2191,16 @@ function pfUI.uf:RefreshUnit(unit, component)
end
end
local debuffFilter = unit.config.selfdebuff == "1" and "HARMFUL|PLAYER" or "HARMFUL"
for i=1,16 do -- scan for custom debuffs
local aura = C_UnitAuras.GetDebuffDataByIndex(unitstr, i)
local aura = C_UnitAuras.GetAuraDataByIndex(unitstr, i, debuffFilter)
if aura then
local name = aura.name
local texture = aura.icon
local timeleft = aura.expirationTime > 0 and (aura.expirationTime - GetTime()) or nil
-- libdebuff provides caster correlation + reconstructed timer for non-player units
if libdebuff then
if unit.config.selfdebuff == "1" then
name, _, texture, _, _, _, timeleft = libdebuff:UnitOwnDebuff(unitstr, i)
else
name, _, texture, _, _, _, timeleft = libdebuff:UnitDebuff(unitstr, i)
end
end
if name then
for _, filter in pairs(unit.indicator_custom) do
if filter == string.lower(name) then
pfUI.uf:AddIcon(unit, pos, texture, timeleft, aura.applications)
pos = pos + 1
break
end
for _, filter in pairs(unit.indicator_custom) do
if filter == string.lower(aura.name) then
pfUI.uf:AddIcon(unit, pos, aura.icon, timeleft, aura.applications)
pos = pos + 1
break
end
end
end
-5
View File
@@ -2,11 +2,6 @@
setfenv(1, pfUI:GetEnvironment())
-- [[ Constants ]]--
CASTBAR_EVENT_CAST_DELAY = "SPELLCAST_DELAYED"
CASTBAR_EVENT_CHANNEL_DELAY = "SPELLCAST_CHANNEL_UPDATE"
CASTBAR_EVENT_CAST_START = "SPELLCAST_START"
CASTBAR_EVENT_CHANNEL_START = "SPELLCAST_CHANNEL_START"
EVENTS_MINIMAP_ZONE_UPDATE = {"PLAYER_ENTERING_WORLD", "MINIMAP_ZONE_CHANGED"}
MICRO_BUTTONS = {
-1
View File
@@ -1,7 +1,6 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/">
<Include file="..\libs\libtipscan.lua"/>
<Include file="..\libs\libspell.lua"/>
<Include file="..\libs\libcast.lua"/>
<Include file="..\libs\libdebuff.lua"/>
<Include file="..\libs\librange.lua"/>
<Include file="..\libs\libunitscan.lua"/>
-571
View File
@@ -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
+18 -301
View File
@@ -11,9 +11,12 @@ setfenv(1, pfUI:GetEnvironment())
-- This eliminates ~400 lines of error-prone shift logic while maintaining full
-- multi-caster tracking support.
--
-- libdebuff:UnitDebuff(unit, id)
-- Returns debuff informations on the given effect of the specified unit.
-- name, rank, texture, stacks, dtype, duration, timeleft, caster
-- 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 / GetEnhancedDebuffs and the
-- 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
@@ -142,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 {}
@@ -787,219 +788,6 @@ function libdebuff:AddEffect(unit, unitlevel, effect, duration, caster, rank)
lastspell = libdebuff.objects[unit][unitlevel][effect]
end
-- ============================================================================
-- MAIN API: UnitDebuff (GetUnitField-based)
-- ============================================================================
local cache = {}
function libdebuff:UnitDebuff(unit, displaySlot)
local unitname = UnitName(unit)
local unitlevel = UnitLevel(unit)
local duration, timeleft = nil, -1
local rank = nil
local caster = nil
local effect = nil
local texture = nil
local stacks = 0
local dtype = nil
-- Nampower: Use GetUnitField for ALL debuff data (no Blizzard UnitDebuff needed)
if hasNampower and UnitGUID then
local guid = UnitGUID(unit)
if not guid then
-- Safety fallback: no GUID available (should not happen with Nampower)
local aura = C_UnitAuras.GetDebuffDataByIndex(unit, displaySlot)
if aura then
return aura.name, rank, aura.icon, aura.applications, aura.dispelName, duration, timeleft, caster
end
return effect, rank, texture, stacks, dtype, duration, timeleft, caster
end
-- Get current slot map from GetUnitField (cached 50ms)
local slotMap = GetDebuffSlotMap(guid)
if not slotMap or not slotMap[displaySlot] then
return nil
end
local slotData = slotMap[displaySlot]
effect = slotData.spellName
texture = slotData.texture
stacks = slotData.stacks
dtype = slotData.dtype
local auraSlot = slotData.auraSlot
-- Get caster info for this slot
local slotCasterGuid, isOurs = GetSlotCaster(guid, auraSlot, effect)
if isOurs then
-- OUR debuff - get timer from ownDebuffs
if ownDebuffs[guid] and ownDebuffs[guid][effect] then
local data = ownDebuffs[guid][effect]
local remaining = (data.startTime + data.duration) - GetTime()
if remaining > 0 then
duration = data.duration
timeleft = remaining
caster = "player"
rank = data.rank
elseif remaining > -1 then
-- Grace period - show 0 timeleft
duration = data.duration
timeleft = 0
caster = "player"
rank = data.rank
end
end
else
-- OTHER player's debuff - get timer from allAuraCasts
if slotCasterGuid and allAuraCasts[guid] and allAuraCasts[guid][effect] then
local data = allAuraCasts[guid][effect][slotCasterGuid]
if data then
local remaining = (data.startTime + data.duration) - GetTime()
if remaining > 0 and data.duration > 0 then
duration = data.duration
timeleft = remaining
caster = "other"
rank = data.rank
end
end
end
-- Fallback: Search all casters if specific one not found
if not duration and allAuraCasts[guid] and allAuraCasts[guid][effect] then
for anyCasterGuid, data in pairs(allAuraCasts[guid][effect]) do
local remaining = (data.startTime + data.duration) - GetTime()
if remaining > 0 and data.duration > 0 then
duration = data.duration
timeleft = remaining
caster = "other"
rank = data.rank
break
end
end
end
end
return effect, rank, texture, stacks, dtype, duration, timeleft, caster
end
-- ============================================================================
-- FALLBACK: Legacy (non-Nampower) system
-- ============================================================================
local aura = C_UnitAuras.GetDebuffDataByIndex(unit, displaySlot)
if aura then
texture = aura.icon
stacks = aura.applications
dtype = aura.dispelName
effect = aura.name
end
if effect and libdebuff.objects[unitname] then
for level, effects in pairs(libdebuff.objects[unitname]) do
if effects[effect] and effects[effect].duration then
local timeleft = effects[effect].start and
effects[effect].start + effects[effect].duration - GetTime()
if timeleft and timeleft > 0 then
return effect, effects[effect].rank, texture, stacks, dtype,
effects[effect].duration, timeleft, effects[effect].caster
end
end
end
end
return effect, rank, texture, stacks, dtype, duration, timeleft, caster
end
-- ============================================================================
-- API: UnitOwnDebuff (only OUR debuffs)
-- ============================================================================
-- Pre-defined sort function for UnitOwnDebuff (avoids closure creation per call)
local _ownDebuffSortFunc = function(a, b)
if a.data.startTime == b.data.startTime then
return a.spellName < b.spellName
end
return a.data.startTime < b.data.startTime
end
function libdebuff:UnitOwnDebuff(unit, id)
if hasNampower and UnitGUID then
local guid = UnitGUID(unit)
if guid and ownDebuffs[guid] then
-- Build sorted list of our active debuffs
local sortedDebuffs = {}
local now = GetTime()
local toRemove = nil
for spellName, data in pairs(ownDebuffs[guid]) do
local timeleft = (data.startTime + data.duration) - now
if timeleft > 0 then
local count = table.getn(sortedDebuffs) + 1
sortedDebuffs[count] = {
spellName = spellName,
data = data,
timeleft = timeleft
}
elseif data.pending then
if timeleft < -2 then
toRemove = toRemove or {}
toRemove[spellName] = true
end
else
toRemove = toRemove or {}
toRemove[spellName] = true
end
end
if toRemove then
for spellName in pairs(toRemove) do
ownDebuffs[guid][spellName] = nil
end
end
-- Sort by startTime (oldest first = lowest display slot)
-- If startTime is equal (e.g. after Carnage refresh), use spellName for stable sorting
table.sort(sortedDebuffs, _ownDebuffSortFunc)
-- Return debuff at position 'id'
if sortedDebuffs[id] then
local entry = sortedDebuffs[id]
local texture = entry.data.texture or "Interface\\Icons\\INV_Misc_QuestionMark"
local displayTimeleft = entry.timeleft > 0 and entry.timeleft or 0
-- Get dtype from SpellRec DBC via stored spellId
local entryDtype = nil
if entry.data.spellId and GetSpellRecField then
local dispelId = GetSpellRecField(entry.data.spellId, "dispel")
if dispelId and dispelId > 0 then
entryDtype = dispelTypeMap[dispelId]
end
end
return entry.spellName, entry.data.rank, texture, 1, entryDtype, entry.data.duration, displayTimeleft, "player"
end
end
return nil
end
-- Fallback: Iterate through all debuffs and filter
for k in pairs(cache) do cache[k] = nil end
local count = 1
for i=1,16 do
local effect, rank, texture, stacks, dtype, duration, timeleft, caster = libdebuff:UnitDebuff(unit, i)
if effect and not cache[effect] and caster and caster == "player" then
cache[effect] = true
if count == id then
return effect, rank, texture, stacks, dtype, duration, timeleft, caster
else
count = count + 1
end
end
end
end
-- ============================================================================
-- API: GetBestAuraCast (for libpredict HoT tracking)
-- ============================================================================
@@ -1144,12 +932,8 @@ if hasNampower then
pfTarget.update_aura = true
end
end
if pfUI.nameplates and pfUI.nameplates.OnAuraUpdate then
pfUI.nameplates:OnAuraUpdate(guid)
end
end
carnageState = nil
this:Hide()
end)
@@ -1180,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
@@ -1218,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)
@@ -1278,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
@@ -1410,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)
@@ -1635,11 +1365,6 @@ if hasNampower then
end
-- Notify nameplates
if pfUI.nameplates and pfUI.nameplates.OnAuraUpdate then
pfUI.nameplates:OnAuraUpdate(targetGuid)
end
-- Notify unitframes of debuff updates (UNIT_AURA doesn't fire on refreshes!)
-- Check player
if UnitGUID("player") then
@@ -1857,11 +1582,7 @@ if hasNampower then
-- Cleanup expired timers
CleanupExpiredTimers(guid)
-- Notify nameplates
if pfUI.nameplates and pfUI.nameplates.OnAuraUpdate then
pfUI.nameplates:OnAuraUpdate(guid)
end
if pfUI.libdebuff_debuff_added_other_hooks then
for _, fn in pairs(pfUI.libdebuff_debuff_added_other_hooks) do
fn(arg1, arg2, arg3, arg4)
@@ -1950,11 +1671,7 @@ if hasNampower then
-- Cleanup expired timers
CleanupExpiredTimers(guid)
-- Notify nameplates
if pfUI.nameplates and pfUI.nameplates.OnAuraUpdate then
pfUI.nameplates:OnAuraUpdate(guid)
end
if pfUI.libdebuff_debuff_removed_other_hooks then
for _, fn in pairs(pfUI.libdebuff_debuff_removed_other_hooks) do
fn(arg1, arg2, arg3, arg4)
+26 -19
View File
@@ -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
@@ -944,16 +961,6 @@ local INSTANT_HOT_COOLDOWN = 1.0 -- 1 Sekunde Cooldown (GCD ist 1.5s)
-- Pending HoTs Queue - wird nach Delay verifiziert
local pendingHots = {}
-- Helper: check if buff is present on unit
local function UnitHasBuff(unit, buffName)
for i = 1, 32 do
local name = UnitBuff(unit, i)
if not name then break end
if name == buffName then return true end
end
return false
end
-- Gather Data by User Actions
hooksecurefunc("CastSpell", function(id, bookType)
if not libpredict.sender.enabled then return end
+1 -3
View File
@@ -151,9 +151,7 @@ pfUI:RegisterModule("afkcam", function ()
delay:SetScript("OnUpdate", function()
if ( this.tick or 0) > GetTime() then return else this.tick = GetTime() + 1 end
local name = UnitName("player")
local cast = pfGetCastInfo(name)
if not cast then cast = pfGetChannelInfo(name) end
local cast = C_Spell.UnitCastingInfo("player") or C_Spell.UnitChannelInfo("player")
if not this.delay then this.delay = 0 end
if cast then
+14 -19
View File
@@ -74,18 +74,11 @@ pfUI:RegisterModule("buffwatch", function ()
end
local function GetBuffData(unit, id, type, selfdebuff)
if unit == "player" then
local aura = C_UnitAuras.GetAuraDataByIndex("player", id, type)
if not aura then return end
local remaining = aura.expirationTime > 0 and (aura.expirationTime - GetTime()) or 0
return remaining, aura.icon, aura.name, aura.applications
elseif libdebuff and selfdebuff then
local name, _, texture, stacks, _, _, timeleft = libdebuff:UnitOwnDebuff(unit, id)
return timeleft, texture, name, stacks
elseif libdebuff then
local name, _, texture, stacks, _, _, timeleft = libdebuff:UnitDebuff(unit, id)
return timeleft, texture, name, stacks
end
local filter = (selfdebuff and type == "HARMFUL") and "HARMFUL|PLAYER" or type
local aura = C_UnitAuras.GetAuraDataByIndex(unit, id, filter)
if not aura then return end
local remaining = aura.expirationTime > 0 and (aura.expirationTime - GetTime()) or 0
return remaining, aura.icon, aura.name, aura.applications
end
local function StatusBarOnClick()
@@ -117,15 +110,17 @@ pfUI:RegisterModule("buffwatch", function ()
if this.unit == "player" then
GameTooltip:SetUnitAura("player", this.id, this.type)
elseif this.type == "HARMFUL" then
-- For "only own debuffs" mode: find the REAL slot by matching spell name AND caster
-- selfdebuff filters the displayed list to player-cast harmful auras, but
-- SetUnitAura's index has to be into the engine's full HARMFUL list. Look
-- up the displayed aura via the PLAYER filter, then scan engine slots for
-- one whose name + sourceGUID match.
local config = this.parent and this.parent.config
if config and config.selfdebuff == "1" and libdebuff then
local ownDebuffName = libdebuff:UnitOwnDebuff(this.unit, this.id)
if ownDebuffName then
-- Search through all game slots to find OUR debuff with matching name
if config and config.selfdebuff == "1" then
local ownAura = C_UnitAuras.GetAuraDataByIndex(this.unit, this.id, "HARMFUL|PLAYER")
if ownAura then
for gameSlot = 1, 16 do
local gameName, _, _, _, _, _, _, gameCaster = libdebuff:UnitDebuff(this.unit, gameSlot)
if gameName == ownDebuffName and gameCaster == "player" then
local check = C_UnitAuras.GetDebuffDataByIndex(this.unit, gameSlot)
if check and check.name == ownAura.name and check.sourceGUID == ownAura.sourceGUID then
GameTooltip:SetUnitAura(this.unit, gameSlot, "HARMFUL")
break
end
+343 -201
View File
@@ -16,6 +16,141 @@ pfUI:RegisterModule("castbar", function ()
end
end
-- Clear cast state on the bar. Shows the bar full for one frame, then
-- OnUpdate fades it out.
local function ClearBar(cb)
cb.startTime, cb.endTime, cb.isChannel = nil, nil, nil
cb.activeName, cb.spellID = nil, nil
cb.isTradeskill = nil
cb.tradeskillTotal, cb.tradeskillCompleted, cb.tradeskillSpellID = nil, nil, nil
cb.tradeskillSingleMs, cb.currentCraftStart = nil, nil
cb.lastMax = nil
cb.delay = 0
cb.bar:SetMinMaxValues(1, 100)
cb.bar:SetValue(100)
if cb.bar.spark then cb.bar.spark:Hide() end
cb.fadeout = 1
end
-- "Spellname (N)" label for an active tradeskill merge — N is the number
-- of crafts remaining. Falls back to the bare spell name when ≤1 left.
local function UpdateTradeskillLabel(cb)
if not cb.tradeskillTotal or not cb.activeName or not cb.showname then return end
local remaining = cb.tradeskillTotal - (cb.tradeskillCompleted or 0)
if remaining > 1 then
cb.bar.left:SetText(string.format("%s (%d)", cb.activeName, remaining))
else
cb.bar.left:SetText(cb.activeName)
end
end
-- Stretch the bar to span all queued crafts (Quartz-style merge): the bar
-- fills continuously across the chain instead of resetting per craft.
-- Also arms the per-craft spark — a thin marker that crosses the bar once
-- per craft (so it moves N× faster than the main fill on an N-stack).
local function EnterTradeskillMerge(cb, startMs, endMs, count)
cb.tradeskillTotal = count
cb.tradeskillCompleted = 0
cb.tradeskillSpellID = cb.spellID
local single = endMs - startMs
cb.tradeskillSingleMs = single
cb.currentCraftStart = startMs
local mergedEnd = startMs + single * count
cb.endTime = mergedEnd
local duration = (mergedEnd - startMs) / 1000
cb.bar:SetMinMaxValues(0, duration)
cb.lastMax = duration
if cb.bar.spark then cb.bar.spark:Show() end
UpdateTradeskillLabel(cb)
end
-- Mark the start of craft 2..N within an active merge (called from the
-- SPELLCAST_START / SPELL_START_SELF handlers). Resets the per-craft
-- spark to the left edge of the bar.
local function StartTradeskillCraft(cb)
cb.currentCraftStart = GetTime() * 1000
UpdateTradeskillLabel(cb)
end
-- Stamp the bar with cast data and render text/icon/lag once. OnUpdate
-- then animates the fill from this state without touching C_Spell.
local function StampBar(cb, name, tex, startMs, endMs, spellID, isChannel, delayMs, isTradeskill)
cb.startTime = startMs
cb.endTime = endMs
cb.isChannel = isChannel
cb.spellID = spellID
cb.activeName = name
cb.isTradeskill = isTradeskill
cb.delay = (delayMs or 0) / 1000
cb:SetAlpha(1)
cb.fadeout = nil
cb.bar:SetStatusBarColor(strsplit(",", C.appearance.castbar[isChannel and "channelcolor" or "castbarcolor"]))
local rank = ""
if spellID and GetSpellRecField then
rank = GetSpellRecField(spellID, "rank") or ""
end
local spellname = (cb.showname and name) and (name .. " ") or ""
local rankstr = (cb.showrank and rank ~= "") and string.format("|cffaaffcc[%s]|r", rank) or ""
cb.bar.left:SetText(spellname .. rankstr)
if tex and cb.showicon then
local size = cb:GetHeight()
cb.icon:Show()
cb.icon:SetHeight(size)
cb.icon:SetWidth(size)
cb.icon.texture:SetTexture(tex)
cb.bar:SetPoint("TOPLEFT", cb.icon, "TOPRIGHT", cb.spacing, 0)
else
cb.bar:SetPoint("TOPLEFT", cb, 0, 0)
cb.icon:Hide()
end
local duration = (endMs - startMs) / 1000
if cb.showlag then
local _, _, lag = GetNetStats()
cb.bar.lag:SetWidth(math.min(cb:GetWidth(), cb:GetWidth() / duration * (lag/1000)))
cb.bar.lag:Show()
else
cb.bar.lag:Hide()
end
cb.bar:SetMinMaxValues(0, duration)
cb.lastMax = duration
end
-- One-shot poll: read C_Spell for the bar's unit, stamp or clear. Called
-- from event handlers (cast start, target/focus change), never per-frame.
local function RefreshBar(cb)
local query = cb.unitstr ~= "" and cb.unitstr or cb.unitname
if not query or (cb.unitstr ~= "" and not UnitExists(cb.unitstr)) then
ClearBar(cb)
return
end
local name, _, tex, startMs, endMs, isTradeskill, _, _, spellID, _, delayMs = C_Spell.UnitCastingInfo(query)
local isChan
if not name then
name, _, tex, startMs, endMs, _, _, spellID = C_Spell.UnitChannelInfo(query)
isChan = true
end
-- Synthetic fallback for abilities the engine treats as instant-cast but
-- that have a meaningful wait window (e.g. Turtle WoW Steady Shot on the
-- ranged-swing queue). Per-unit table populated by module-side hooks.
if not name and pfUI.synthetic_casts and pfUI.synthetic_casts[query] then
local s = pfUI.synthetic_casts[query]
if s.endMs > GetTime() * 1000 then
name, tex, startMs, endMs, spellID = s.name, s.icon, s.startMs, s.endMs, s.spellID
isChan = nil
end
end
if name and startMs and endMs then
StampBar(cb, name, tex, startMs, endMs, spellID, isChan, delayMs, isTradeskill)
else
ClearBar(cb)
end
end
local function CreateCastbar(name, parent, unitstr, unitname)
local cb = CreateFrame("Frame", name, parent or UIParent)
@@ -76,238 +211,234 @@ pfUI:RegisterModule("castbar", function ()
cb.bar.lag:SetPoint("BOTTOMRIGHT", cb.bar, "BOTTOMRIGHT", 0, 0)
cb.bar.lag:SetTexture(1,.2,.2,.2)
-- OnUpdate script with throttle for performance optimization
-- Per-craft progress spark for tradeskill merge — a thin vertical line
-- that crosses the bar once per craft in the chain (so on a 5-stack it
-- moves 5x faster than the main fill). Position is updated by OnUpdate
-- while a merge is active; hidden otherwise.
cb.bar.spark = cb.bar:CreateTexture(nil, "OVERLAY")
cb.bar.spark:SetTexture(1, 1, 1, 0.8)
cb.bar.spark:SetWidth(2)
cb.bar.spark:SetPoint("TOP", cb.bar, "TOPLEFT", 0, 0)
cb.bar.spark:SetPoint("BOTTOM", cb.bar, "BOTTOMLEFT", 0, 0)
cb.bar.spark:Hide()
-- OnUpdate animates the bar fill and fades it out on completion. All
-- state transitions (cast start/stop/interrupt, channel start/stop,
-- pushback) come from the event handler below — we never poll C_Spell
-- here.
cb:SetScript("OnUpdate", function()
-- Throttle for performance
if (this.tick or 0) > GetTime() then return end
this.tick = GetTime() + 0.020 -- ~50 FPS for smooth castbar
this.tick = GetTime() + 0.020 -- ~50 FPS
if this.drag and this.drag:IsShown() then
this:SetAlpha(1)
return
end
if not UnitExists(this.unitstr) then
this:SetAlpha(0)
end
if this.fadeout and this:GetAlpha() > 0 then
if this:GetAlpha() == 0 then
this.fadeout = nil
end
this:SetAlpha(this:GetAlpha()-0.05)
this:SetAlpha(this:GetAlpha() - 0.05)
if this:GetAlpha() <= 0 then this.fadeout = nil end
return
end
local channel = nil
local query = this.unitstr ~= "" and this.unitstr or this.unitname
if not query then return end
if not this.endTime then return end
-- Check if we have a GUID-based focus (Turtle WoW native GUID)
local focusGuid = nil
if this.unitstr and string.find(this.unitstr, "^0x") then
focusGuid = this.unitstr
elseif this.unitstr and this.unitstr == "player" and UnitGUID then
focusGuid = UnitGUID("player")
elseif this.unitstr and this.unitstr ~= "player" then
local guid = UnitGUID(this.unitstr)
if guid then focusGuid = guid end
end
this.focusGuid = focusGuid
-- Try libdebuff_casts first for GUID-based units (works with Turtle GUID + Nampower events)
local cast, nameSubtext, text, texture, startTime, endTime
local castBlocked = false
if focusGuid and pfUI.libdebuff_casts and pfUI.libdebuff_casts[focusGuid] then
local castData = pfUI.libdebuff_casts[focusGuid]
if castData.event == "CAST" or castData.event == "FAIL" then
castBlocked = true
pfUI.libdebuff_casts[focusGuid] = nil
elseif (castData.event == "START" or castData.event == "CHANNEL") and castData.endTime and castData.endTime > GetTime() then
cast = castData.spellName
texture = castData.icon
startTime = castData.startTime * 1000
endTime = castData.endTime * 1000
-- Try to get rank from spell DB via spellID (nameSubtext is not stored in libdebuff_casts)
if castData.spellID and GetSpellRecField then
nameSubtext = GetSpellRecField(castData.spellID, "rank") or ""
else
nameSubtext = ""
end
if castData.event == "CHANNEL" then
channel = cast
end
end
-- Non-player bars: if the unit disappears (target died / detarget),
-- drop the bar immediately.
if this.unitstr ~= "" and this.unitstr ~= "player" and not UnitExists(this.unitstr) then
ClearBar(this)
return
end
local useLibcastForPlayer = this.unitstr == "player"
-- For player: use player name to query libcast.db directly
if not cast and useLibcastForPlayer then
query = UnitName("player")
local now = GetTime()
local endSec = this.endTime / 1000
if now >= endSec then
ClearBar(this)
return
end
-- Fallback: pfGetCastInfo only when no focusGuid (Nampower not available for this unit).
-- If we have a focusGuid, libdebuff is authoritative - don't fall back to libcast
-- even if no cast is active (prevents false positives e.g. spellbook clicks on CD spells).
if not cast and not castBlocked and not focusGuid and pfGetCastInfo then
cast, nameSubtext, text, texture, startTime, endTime, isTradeSkill = pfGetCastInfo(query)
local startSec = this.startTime / 1000
local max = endSec - startSec
local cur = this.isChannel and (endSec - now) or (now - startSec)
if cur > max then cur = max end
if cur < 0 then cur = 0 end
this.bar:SetValue(cur)
-- Per-craft spark for a tradeskill merge: position by (now - current
-- craft's start) / single-craft duration, clamped to [0, 1]. Snaps back
-- to the left edge each time StartTradeskillCraft fires for craft N+1.
if this.tradeskillTotal and this.tradeskillSingleMs and this.currentCraftStart then
local craftElapsed = now * 1000 - this.currentCraftStart
local p = craftElapsed / this.tradeskillSingleMs
if p < 0 then p = 0 elseif p > 1 then p = 1 end
local x = p * this.bar:GetWidth() - 1
this.bar.spark:ClearAllPoints()
this.bar.spark:SetPoint("TOP", this.bar, "TOPLEFT", x, 0)
this.bar.spark:SetPoint("BOTTOM", this.bar, "BOTTOMLEFT", x, 0)
end
if not cast and not castBlocked and not focusGuid and pfGetChannelInfo then
channel, nameSubtext, text, texture, startTime, endTime, isTradeSkill = pfGetChannelInfo(this.unitstr or this.unitname)
cast = channel
end
if cast then
local duration = endTime - startTime
local max = duration / 1000
local cur = GetTime() - startTime / 1000
this:SetAlpha(1)
local spellname = this.showname and cast and cast .. " " or ""
local rank = this.showrank and nameSubtext and nameSubtext ~= "" and string.format("|cffaaffcc[%s]|r", nameSubtext) or ""
if this.endTime ~= endTime then
this.bar:SetStatusBarColor(strsplit(",", C.appearance.castbar[(channel and "channelcolor" or "castbarcolor")]))
this.bar.left:SetText(spellname .. rank)
this.fadeout = nil
this.endTime = endTime
-- set texture
if texture and this.showicon then
local size = this:GetHeight()
this.icon:Show()
this.icon:SetHeight(size)
this.icon:SetWidth(size)
-- Override with item icon from libdebuff_casts or persistent item icon cache
local useTexture = texture
local useItemName = nil
if pfUI.libdebuff_casts or pfUI.libdebuff_item_icons then
local castGuid = nil
if this.unitstr and UnitExists then
local guid = UnitGUID(this.unitstr)
castGuid = guid
end
if castGuid then
-- First check active cast data
if pfUI.libdebuff_casts and pfUI.libdebuff_casts[castGuid] and pfUI.libdebuff_casts[castGuid].itemID then
useTexture = pfUI.libdebuff_casts[castGuid].icon or texture
-- Fallback to persistent item icon cache
elseif pfUI.libdebuff_item_icons and pfUI.libdebuff_item_icons[castGuid] then
useTexture = pfUI.libdebuff_item_icons[castGuid].icon or texture
useItemName = pfUI.libdebuff_item_icons[castGuid].name
end
end
end
this.icon.texture:SetTexture(useTexture)
this.bar:SetPoint("TOPLEFT", this.icon, "TOPRIGHT", this.spacing, 0)
-- Override spell name with item name for item-triggered casts
if useItemName and this.showname then
this.bar.left:SetText(useItemName .. " " .. rank)
end
else
this.bar:SetPoint("TOPLEFT", this, 0, 0)
this.icon:Hide()
end
if this.showlag then
local _, _, lag = GetNetStats()
local width = this:GetWidth() / (duration/1000) * (lag/1000)
this.bar.lag:SetWidth(math.min(this:GetWidth(), width))
else
this.bar.lag:Hide()
end
if this.showtimer then
if (this.delay or 0) > 0 then
local prefix = "|cffffaaaa" .. (this.isChannel and "-" or "+") .. FormatCastbarTime(this.delay) .. " |r "
this.bar.right:SetText(prefix .. FormatCastbarTime(cur) .. " / " .. FormatCastbarTime(max))
else
this.bar.right:SetText(FormatCastbarTime(cur) .. " / " .. FormatCastbarTime(max))
end
local newMax = duration / 1000
if this.lastMax ~= newMax then
this.bar:SetMinMaxValues(0, newMax)
this.lastMax = newMax
end
if channel then
cur = max + startTime/1000 - GetTime()
end
cur = cur > max and max or cur
cur = cur < 0 and 0 or cur
this.bar:SetValue(cur)
if this.showtimer then
if this.delay and this.delay > 0 then
local delay = "|cffffaaaa" .. (channel and "-" or "+") .. FormatCastbarTime(this.delay) .. " |r "
this.bar.right:SetText(delay .. FormatCastbarTime(cur) .. " / " .. FormatCastbarTime(max))
else
this.bar.right:SetText(FormatCastbarTime(cur) .. " / " .. FormatCastbarTime(max))
end
end
this.fadeout = nil
else
this.bar:SetMinMaxValues(1,100)
this.bar:SetValue(100)
this.lastMax = nil
this.fadeout = 1
this.delay = 0
this.itemIconApplied = nil
end
end)
-- register for spell delay
-- Prefer Nampower's SPELL_DELAYED_SELF (gives casterGuid + delayMs directly).
-- Fall back to vanilla SPELLCAST_DELAYED if Nampower is not available.
local playerarg = nil
local function ApplyPushback(delayMs)
if not delayMs or delayMs <= 0 or not this.endTime then return end
this.delay = (this.delay or 0) + delayMs / 1000
this.endTime = this.endTime + delayMs
local focusGuid = this.focusGuid
if focusGuid and pfUI.libdebuff_casts and pfUI.libdebuff_casts[focusGuid] then
pfUI.libdebuff_casts[focusGuid].endTime = this.endTime / 1000
-- Cast lifecycle events. Player bars react to vanilla SPELLCAST_*; non-
-- player bars also react to Nampower SPELL_*_OTHER (gated by the
-- NP_EnableSpell{Start,Go}Events CVars, enabled by libdebuff) plus the
-- retarget event. Player events also feed non-player bars for the
-- target=self case.
cb:RegisterEvent("SPELLCAST_START")
cb:RegisterEvent("SPELLCAST_STOP")
cb:RegisterEvent("SPELLCAST_FAILED")
cb:RegisterEvent("SPELLCAST_INTERRUPTED")
cb:RegisterEvent("SPELLCAST_CHANNEL_START")
cb:RegisterEvent("SPELLCAST_CHANNEL_STOP")
cb:RegisterEvent("SPELLCAST_CHANNEL_UPDATE")
cb:RegisterEvent("SPELL_DELAYED_SELF")
-- Chained same-spell recasts never run the client cast path (the 1.12
-- engine short-circuits at spellID == current-cast), so vanilla
-- SPELLCAST_START never fires for them. nampower's SPELL_START_SELF
-- (server-driven) is the only signal that shows them.
cb:RegisterEvent("SPELL_START_SELF")
if unitstr == "player" then
cb:RegisterEvent("SPELL_GO_SELF")
end
if unitstr ~= "player" and unitstr ~= "" then
cb:RegisterEvent("SPELL_START_OTHER")
cb:RegisterEvent("SPELL_FAILED_OTHER")
if unitstr == "target" then
cb:RegisterEvent("PLAYER_TARGET_CHANGED")
elseif unitstr == "focus" then
cb:RegisterEvent("PLAYER_FOCUS_CHANGED")
end
end
cb:RegisterEvent("SPELL_DELAYED_SELF")
cb:RegisterEvent(CASTBAR_EVENT_CAST_DELAY)
cb:RegisterEvent(CASTBAR_EVENT_CHANNEL_DELAY)
cb:RegisterEvent(CASTBAR_EVENT_CAST_START)
cb:RegisterEvent(CASTBAR_EVENT_CHANNEL_START)
cb:SetScript("OnEvent", function()
if this.unitstr and not UnitIsUnit(this.unitstr, "player") then return end
local unit = this.unitstr
if event == "SPELL_DELAYED_SELF" then
-- arg1=casterGuid, arg2=delayMs (Nampower, most accurate)
ApplyPushback(arg2)
elseif event == CASTBAR_EVENT_CAST_DELAY then
-- SPELLCAST_DELAYED fallback intentionally removed - addon requires Nampower.
-- Cast pushback is handled by SPELL_DELAYED_SELF above.
if event == "PLAYER_TARGET_CHANGED" or event == "PLAYER_FOCUS_CHANGED" then
RefreshBar(this)
return
end
elseif event == CASTBAR_EVENT_CHANNEL_DELAY then
-- SPELLCAST_CHANNEL_UPDATE fires when a channel is pushed back by damage.
-- arg1 = new remaining time in ms. Channel ends sooner = newEndTime < this.endTime.
if not this.endTime or not arg1 then return end
local newEndTime = GetTime() * 1000 + arg1
local diff = this.endTime - newEndTime -- positive = time lost to pushback
if diff > 50 then
this.delay = (this.delay or 0) + diff / 1000
this.endTime = newEndTime
local focusGuid = this.focusGuid
if focusGuid and pfUI.libdebuff_casts and pfUI.libdebuff_casts[focusGuid] then
pfUI.libdebuff_casts[focusGuid].endTime = newEndTime / 1000
if event == "SPELL_START_OTHER" then
-- arg3=casterGuid. Defer one frame so ClassicAPI's UnitChannelInfo
-- can see the engine's +0x228 broadcast for remote-unit channels
-- (the cohook+packet handler runs in the same frame; the broadcast
-- propagates after).
if arg3 == UnitGUID(unit) then
local target = this
RunNextFrame(function() RefreshBar(target) end)
end
return
end
if event == "SPELL_FAILED_OTHER" then
if arg1 == UnitGUID(unit) then ClearBar(this) end
return
end
-- Vanilla SPELLCAST_* + SPELL_DELAYED_SELF fire only for the local
-- player. Non-player bars handle them only when their unit currently
-- resolves to the player (target=self / focus=self).
if not UnitIsUnit(unit, 'player') then return end
if event == "SPELLCAST_START" or event == "SPELLCAST_CHANNEL_START" then
if this.tradeskillTotal then
-- Mid-chain craft N+1 of N. Keep the merged bar; resync the spark
-- to the new craft's start and refresh the "(N)" count label.
StartTradeskillCraft(this)
else
RefreshBar(this)
if this.isTradeskill and (this.pendingTradeskillCount or 0) > 1
and C.castbar.player.mergetradeskill == "1" then
EnterTradeskillMerge(this, this.startTime, this.endTime, this.pendingTradeskillCount)
end
this.pendingTradeskillCount = nil
end
elseif event == "SPELL_START_SELF" then
-- Catches chained same-spell recasts (no SPELLCAST_START fires) —
-- including tradeskill chaining where craft 2..N reuse one spell.
-- Defer one frame so ClassicAPI's SMSG_SPELL_START co-hook has
-- stamped g_cast before we poll, regardless of co-hook order.
if this.tradeskillTotal then
StartTradeskillCraft(this)
else
local target = this
RunNextFrame(function()
-- Re-check: SPELL_START_SELF (nampower co-hook) fires before vanilla
-- SPELLCAST_START on the same packet, so SPELLCAST_START may have
-- entered merge in this same frame. Don't restamp over it.
if target.tradeskillTotal then
StartTradeskillCraft(target)
return
end
RefreshBar(target)
if target.unitstr == "player" and target.isTradeskill
and (target.pendingTradeskillCount or 0) > 1
and C.castbar.player.mergetradeskill == "1" then
EnterTradeskillMerge(target, target.startTime, target.endTime, target.pendingTradeskillCount)
end
target.pendingTradeskillCount = nil
end)
end
elseif event == "SPELLCAST_CHANNEL_STOP" then
-- A channel's stop can arrive after a following cast already claimed
-- the bar (channel->cast transition); only clear if a channel is
-- actually being shown, so it doesn't wipe an active cast bar.
if this.isChannel then ClearBar(this) end
elseif event == "SPELLCAST_STOP" then
-- During a tradeskill chain, SPELL_GO_SELF already counted this craft
-- and either cleared the bar (chain done) or kept it running. Only a
-- non-merge cast clears here.
if not this.tradeskillTotal then ClearBar(this) end
elseif event == "SPELLCAST_FAILED" or event == "SPELLCAST_INTERRUPTED" then
ClearBar(this)
elseif event == "SPELL_GO_SELF" then
-- arg2 = spellId. In a tradeskill merge, count each successful craft
-- and clear when the chain is done.
if this.tradeskillTotal and arg2 == this.tradeskillSpellID then
this.tradeskillCompleted = (this.tradeskillCompleted or 0) + 1
if this.tradeskillCompleted >= this.tradeskillTotal then
ClearBar(this)
else
UpdateTradeskillLabel(this)
end
end
elseif event == CASTBAR_EVENT_CAST_START or event == CASTBAR_EVENT_CHANNEL_START then
playerarg = true
this.delay = 0
elseif event == "SPELL_DELAYED_SELF" then
-- Cast pushback. nampower's event carries the delay (arg2); apply it
-- locally rather than re-polling, so the bar doesn't depend on
-- ClassicAPI's SMSG_SPELL_DELAYED co-hook having bumped g_cast before
-- this fires (co-hook order vs nampower is not guaranteed).
if not this.endTime or not arg2 then return end
local delayMs = tonumber(arg2) or 0
if delayMs > 0 then
this.delay = (this.delay or 0) + delayMs / 1000
this.endTime = this.endTime + delayMs
local newDuration = (this.endTime - this.startTime) / 1000
this.bar:SetMinMaxValues(0, newDuration)
this.lastMax = newDuration
end
elseif event == "SPELLCAST_CHANNEL_UPDATE" then
-- Channel pushback. ClassicAPI doesn't track channel delay in
-- g_channel, so we adjust endTime + delay locally and resize the
-- bar so OnUpdate animates against the new total.
if not this.endTime or not arg1 then return end
local newEndMs = GetTime() * 1000 + arg1
local diff = this.endTime - newEndMs
if diff > 50 then
this.delay = (this.delay or 0) + diff / 1000
this.endTime = newEndMs
local newDuration = (this.endTime - this.startTime) / 1000
this.bar:SetMinMaxValues(0, newDuration)
this.lastMax = newDuration
end
end
end)
@@ -350,6 +481,17 @@ pfUI:RegisterModule("castbar", function ()
end
UpdateMovable(pfUI.castbar.player)
-- Tradeskill merge: hook DoTradeSkill so the player castbar knows the
-- requested count before the first SPELLCAST_START fires. Always-on hook
-- (the config knob is read at event time so toggling takes effect on the
-- next craft without a /reload). DoTradeSkill is synchronous; the server
-- roundtrip to SPELLCAST_START gives us plenty of time after this hook.
hooksecurefunc("DoTradeSkill", function(index, num)
if pfUI.castbar.player then
pfUI.castbar.player.pendingTradeskillCount = tonumber(num) or 1
end
end)
end
-- [[ pfTargetCastbar ]] --
+5 -24
View File
@@ -1,29 +1,10 @@
pfUI:RegisterModule("feigndeath", function ()
local cache = { }
local scanner = libtipscan:GetScanner("feigndeath")
local healthbar = scanner:GetChildren()
local cache_update = CreateFrame("Frame")
cache_update:RegisterEvent("UNIT_HEALTH")
cache_update:RegisterEvent("PLAYER_TARGET_CHANGED")
cache_update:SetScript("OnEvent", function()
if event == "PLAYER_TARGET_CHANGED" and UnitIsDead("target") then
scanner:SetUnit("target")
cache[UnitName("target")] = healthbar:GetValue()
elseif event == "UNIT_HEALTH" and UnitIsDead(arg1) and UnitName(arg1) then
scanner:SetUnit(arg1)
cache[UnitName(arg1)] = healthbar:GetValue()
elseif event == "UNIT_HEALTH" and UnitName(arg1) then
cache[UnitName(arg1)] = nil
end
end)
local oldUnitHealth = UnitHealth
function UnitHealth(arg)
if UnitIsDead(arg) and cache[UnitName(arg)] then
return cache[UnitName(arg)]
else
return oldUnitHealth(arg)
function UnitHealth(unit)
if UnitIsFeignDeath(unit) then
local hp = GetUnitField(unit, "health")
if hp and hp > 0 then return hp end
end
return oldUnitHealth(unit)
end
end)
+2 -1
View File
@@ -2776,6 +2776,7 @@ pfUI:RegisterModule("gui", function ()
CreateConfig(nil, T["Left Text Y Offset"], C.castbar.player, "txtleftoffy")
CreateConfig(nil, T["Show Lag"], C.castbar.player, "showlag", "checkbox")
CreateConfig(nil, T["Show Rank"], C.castbar.player, "showrank", "checkbox")
CreateConfig(nil, T["Merge Tradeskill Casts"], C.castbar.player, "mergetradeskill", "checkbox")
CreateConfig(nil, T["Right Text X Offset"], C.castbar.player, "txtrightoffx")
CreateConfig(nil, T["Right Text Y Offset"], C.castbar.player, "txtrightoffy")
@@ -2895,10 +2896,10 @@ pfUI:RegisterModule("gui", function ()
CreateConfig(U["nameplates"], T["Enable Debuffs"], C.nameplates, "showdebuffs", "checkbox")
CreateConfig(U["nameplates"], T["Show Debuffs on Hostile"], C.nameplates, "showdebuffs_hostile", "checkbox")
CreateConfig(U["nameplates"], T["Show Debuffs on Friendly"], C.nameplates, "showdebuffs_friendly", "checkbox")
CreateConfig(U["nameplates"], T["Only Show Your Debuffs"], C.nameplates, "owndebuffs", "checkbox")
CreateConfig(U["nameplates"], T["Debuff Position"], C.nameplates.debuffs, "position", "dropdown", pfUI.gui.dropdowns.debuffposition)
CreateConfig(U["nameplates"], T["Debuff Icon Offset"], C.nameplates, "debuffoffset")
CreateConfig(U["nameplates"], T["Debuff Icon Size"], C.nameplates, "debuffsize")
CreateConfig(U["nameplates"], T["Estimate Debuffs"], C.nameplates, "guessdebuffs", "checkbox")
CreateConfig(U["nameplates"], T["Show Debuff Stacks"], C.nameplates.debuffs, "showstacks", "checkbox")
CreateConfig(U["nameplates"], T["Enable Debuff Timers"], C.nameplates, "debufftimers", "checkbox")
CreateConfig(U["nameplates"], T["Show Timer Text"], C.nameplates, "debufftext", "checkbox")
+95 -266
View File
@@ -3,8 +3,6 @@ pfUI:RegisterModule("nameplates", function ()
pcall(SetCVar, "ShowVKeyCastbar", 0)
-- Local function references for performance
local pfGetCastInfo = pfGetCastInfo -- provided by libcast for vanilla
local pfGetChannelInfo = pfGetChannelInfo -- provided by libcast for vanilla
local GetTime = GetTime
local UnitExists = UnitExists
local UnitName = UnitName
@@ -65,9 +63,30 @@ pfUI:RegisterModule("nameplates", function ()
local raidGuidCache = {} -- guid -> name (rebuilt on RAID_ROSTER_UPDATE/PARTY_MEMBERS_CHANGED)
-- Helper function to safely access libdebuff cast data
-- Resolve a plate GUID to its cast/channel info via C_Spell. Returns a
-- compact struct (spellName / icon / startTime / endTime / duration /
-- isChannel) or nil when the unit isn't casting / the GUID can't map to a
-- live token.
local function GetCastInfo(guid)
return pfUI.libdebuff_casts and pfUI.libdebuff_casts[guid]
if not guid then return nil end
local unit = UnitTokenFromGUID(guid)
if not unit then return nil end
local name, _, texture, startMs, endMs, _, _, _, spellID = C_Spell.UnitCastingInfo(unit)
local isChannel
if not name then
name, _, texture, startMs, endMs, _, _, spellID = C_Spell.UnitChannelInfo(unit)
isChannel = true
end
if not name or not startMs or not endMs then return nil end
return {
spellName = name,
spellID = spellID,
icon = texture,
startTime = startMs / 1000,
endTime = endMs / 1000,
duration = (endMs - startMs) / 1000,
isChannel = isChannel,
}
end
local guidTargetTokenCache = {} -- guid -> "<guid>target" interned string
@@ -78,36 +97,6 @@ pfUI:RegisterModule("nameplates", function ()
local threatMemory = {} -- guid -> true if mob had player targeted
local debuffSeen = {} -- reusable table for debuff tracking (avoid GC churn)
-- PERF: Module-level IterDebuffs callback to avoid closure allocation per call
local _iterDebuffCount = 0
local function iterDebuffCallback(auraSlot, spellId, effect, texture, stacks, dtype, duration, timeleft)
if not texture or string.find(texture, "QuestionMark") then return end
_iterDebuffCount = _iterDebuffCount + 1
if _iterDebuffCount > 16 then return end
local b = debuffDisplayBuf[_iterDebuffCount]
b.effect, b.texture, b.stacks, b.dtype, b.duration, b.timeleft = effect, texture, stacks, dtype, duration, timeleft
end
-- PERF: Module-level IterDebuffs callback for PlateCacheDebuffs — same
-- rationale; PlateCacheDebuffs runs per visible plate per throttled tick.
local _pcdSelf, _pcdNow, _pcdId
local function iterPlateCacheDebuffsCallback(auraSlot, spellId, effect, texture, stacks, dtype, duration, timeleft)
if not effect or not texture then return end
_pcdId = _pcdId + 1
if _pcdId > 16 then return end
local stop = (timeleft and timeleft > 0) and (_pcdNow + timeleft) or nil
local start = stop and (stop - (duration or 0)) or _pcdNow
local cache = _pcdSelf.debuffcache[_pcdId] or {}
cache.effect = effect
cache.texture = texture
cache.stacks = stacks
cache.duration = duration or 0
cache.start = start
cache.stop = stop
cache.empty = nil
_pcdSelf.debuffcache[_pcdId] = cache
end
-- PERF: visiblePlateCount maintained event-driven (NAME_PLATE_UNIT_ADDED/_REMOVED)
local visiblePlateCount = 0
@@ -134,6 +123,7 @@ pfUI:RegisterModule("nameplates", function ()
cfg.showdebuffs = C.nameplates["showdebuffs"] == "1"
cfg.showdebuffs_hostile = C.nameplates["showdebuffs_hostile"] == "1"
cfg.showdebuffs_friendly = C.nameplates["showdebuffs_friendly"] == "1"
cfg.owndebuffs = C.nameplates["owndebuffs"] == "1"
cfg.targetzoom = C.nameplates.targetzoom == "1"
cfg.zoomval = (tonumber(C.nameplates.targetzoomval) or 0.4) + 1
cfg.width = tonumber(C.nameplates.width) or 120
@@ -274,15 +264,6 @@ pfUI:RegisterModule("nameplates", function ()
return
end
local function wipe(table)
if type(table) ~= "table" then
return
end
for k in pairs(table) do
table[k] = nil
end
end
local function DisableObject(object)
if not object then return end
if not object.GetObjectType then return end
@@ -400,60 +381,6 @@ pfUI:RegisterModule("nameplates", function ()
end
end
local function PlateCacheDebuffs(self, unitstr, verify)
if not self.debuffcache then self.debuffcache = {} end
if not libdebuff then return end
local now = GetTime()
-- Clear existing cache slots
for id = 1, 16 do
if self.debuffcache[id] then
self.debuffcache[id].empty = true
end
end
-- Use IterDebuffs if Nampower available, else fall back to slot loop
if unitstr and libdebuff.IterDebuffs and UnitGUID then
_pcdSelf, _pcdNow, _pcdId = self, now, 0
libdebuff:IterDebuffs(unitstr, iterPlateCacheDebuffsCallback)
else
for id = 1, 16 do
local effect, _, texture, stacks, _, duration, timeleft
effect, _, texture, stacks, _, duration, timeleft = libdebuff:UnitDebuff(unitstr, id)
if effect and timeleft and timeleft > 0 then
local start = now - ( (duration or 0) - ( timeleft or 0) )
local stop = now + timeleft
self.debuffcache[id] = self.debuffcache[id] or {}
self.debuffcache[id].effect = effect
self.debuffcache[id].texture = texture
self.debuffcache[id].stacks = stacks
self.debuffcache[id].duration = duration or 0
self.debuffcache[id].start = start
self.debuffcache[id].stop = stop
self.debuffcache[id].empty = nil
end
end
end
self.verify = verify
end
local function PlateUnitDebuff(self, id)
-- break on unknown data
if not self.debuffcache then return end
if not self.debuffcache[id] then return end
if not self.debuffcache[id].stop then return end
-- break on timeout debuffs
if self.debuffcache[id].empty then return end
if self.debuffcache[id].stop < GetTime() then return end
-- return cached debuff
local c = self.debuffcache[id]
return c.effect, c.rank, c.texture, c.stacks, c.dtype, c.duration, (c.stop - GetTime())
end
local function CreateDebuffIcon(plate, index)
plate.debuffs[index] = CreateFrame("Frame", plate.platename.."Debuff"..index, plate)
plate.debuffs[index]:Hide()
@@ -549,32 +476,9 @@ nameplates:RegisterEvent("NAME_PLATE_UNIT_ADDED")
nameplates:RegisterEvent("NAME_PLATE_UNIT_REMOVED")
if GetUnitField then
nameplates:RegisterEvent("UNIT_FLAGS_GUID")
nameplates:RegisterEvent("UNIT_AURA_GUID")
end
-- Cast tracking handled by libdebuff (SPELL_START/GO/FAILED events)
-- No local event registration needed
-- Callback from libdebuff when auras change (GUID-based, event-driven)
nameplates.OnAuraUpdate = function(self, guid)
if not guid then return end
-- GUID is actual GUID (0xF13000...) from Nampower events
local plate = C_NamePlate.GetNamePlateForGUID(guid)
if plate and plate.nameplate then
-- Mark nameplate for aura update in next OnUpdate cycle
plate.nameplate.auraUpdate = true
end
end
-- Hook into libdebuff timer signal (fires when slotTimers written or cleared)
pfUI.libdebuff_on_unit_updated = pfUI.libdebuff_on_unit_updated or {}
table.insert(pfUI.libdebuff_on_unit_updated, function(guid)
local plate = C_NamePlate.GetNamePlateForGUID(guid)
if plate and plate.nameplate then
plate.nameplate.auraUpdate = true
end
end)
nameplates:SetScript("OnEvent", function()
-- Stop event handling during logout to prevent crash 132
if event == "PLAYER_LOGOUT" then
@@ -665,10 +569,6 @@ end
if threatMemory[guid] then threatMemory[guid] = nil end
if guidTargetTokenCache[guid] then guidTargetTokenCache[guid] = nil end
if combatColorCache[guid] then combatColorCache[guid] = nil end
local castInfo = GetCastInfo(guid)
if castInfo and castInfo.endTime and castInfo.endTime < GetTime() then
if pfUI.libdebuff_casts then pfUI.libdebuff_casts[guid] = nil end
end
local plate = C_NamePlate.GetNamePlateForUnit(arg1)
if plate and plate.nameplate and plate.nameplate.cachedGuid == guid then
plate.nameplate.cachedGuid = nil
@@ -683,6 +583,17 @@ end
plate.nameplate.eventcache = true
end
elseif event == "UNIT_AURA_GUID" then
-- Nampower: fires when a unit's aura set changes (add/remove/modify).
-- arg1 = guid. Flag the matching plate so OnUpdate triggers a fresh
-- C_UnitAuras read on the next tick instead of waiting on the 0.5s
-- throttle — covers expirations, dispels, refreshes, and stack changes
-- in one event.
local plate = C_NamePlate.GetNamePlateForGUID(arg1)
if plate and plate.nameplate then
plate.nameplate.auraUpdate = true
end
elseif event == "PLAYER_TARGET_CHANGED" then
-- Flag target plate for update via GUID registry
local targetGuid = UnitGUID("target")
@@ -773,8 +684,6 @@ end
nameplate:EnableMouse(0)
nameplate.parent = parent
nameplate.cache = {}
nameplate.UnitDebuff = PlateUnitDebuff
nameplate.CacheDebuffs = PlateCacheDebuffs
nameplate.original = {}
-- create shortcuts for all known elements and disable them
@@ -1278,47 +1187,27 @@ end
local isFriendly = unittype == "FRIENDLY_PLAYER" or unittype == "FRIENDLY_NPC"
local showDebuffsForType = cfg.showdebuffs and (isFriendly and cfg.showdebuffs_friendly or (not isFriendly and cfg.showdebuffs_hostile))
if showDebuffsForType then
-- PERF: Cache verify string - only allocate new string when name/level actually changes
if name ~= plate.cachedVerifyName or level ~= plate.cachedVerifyLevel then
plate.cachedVerifyName = name
plate.cachedVerifyLevel = level
plate.cachedVerify = (name or "") .. ":" .. (level or "")
end
local verify = plate.cachedVerify
-- update cached debuffs
if C.nameplates["guessdebuffs"] == "1" and unitstr then
plate:CacheDebuffs(unitstr, verify)
end
-- update all debuff icons
-- Use IterDebuffs when Nampower available to avoid blind 16-slot loop
-- debuffDisplayBuf is a module-level reusable buffer (no GC churn)
-- Pull debuffs from C_UnitAuras (HARMFUL range). owndebuffs adds the
-- PLAYER filter token so only auras whose caster GUID matches the local
-- player come through. debuffDisplayBuf is a module-level reusable
-- buffer (no GC churn).
local debuffCount = 0
for i = 1, 16 do debuffDisplayBuf[i].effect = nil end -- clear previous
if unitstr and libdebuff and libdebuff.IterDebuffs and UnitGUID then
_iterDebuffCount = 0
libdebuff:IterDebuffs(unitstr, iterDebuffCallback)
debuffCount = _iterDebuffCount
elseif unitstr and libdebuff then
for i = 1, 16 do
local effect, rank, texture, stacks, dtype, duration, timeleft
effect, rank, texture, stacks, dtype, duration, timeleft = libdebuff:UnitDebuff(unitstr, i)
if effect then
debuffCount = debuffCount + 1
local b = debuffDisplayBuf[debuffCount]
b.effect, b.texture, b.stacks, b.dtype, b.duration, b.timeleft = effect, texture, stacks, dtype, duration, timeleft
end
end
elseif plate.verify == verify then
for i = 1, 16 do
local effect, rank, texture, stacks, dtype, duration, timeleft = plate:UnitDebuff(i)
if effect then
debuffCount = debuffCount + 1
local b = debuffDisplayBuf[debuffCount]
b.effect, b.texture, b.stacks, b.dtype, b.duration, b.timeleft = effect, texture, stacks, dtype, duration, timeleft
end
for i = 1, 16 do debuffDisplayBuf[i].effect = nil end
if unitstr then
local filter = cfg.owndebuffs and "HARMFUL|PLAYER" or "HARMFUL"
local auras = C_UnitAuras.GetUnitAuras(unitstr, filter)
local now = GetTime()
for _, aura in ipairs(auras) do
if debuffCount >= 16 then break end
debuffCount = debuffCount + 1
local timeleft = (aura.expirationTime and aura.expirationTime > 0) and (aura.expirationTime - now) or nil
local b = debuffDisplayBuf[debuffCount]
b.effect = aura.name
b.texture = aura.icon
b.stacks = aura.applications
b.dtype = aura.dispelName
b.duration = aura.duration
b.timeleft = timeleft
end
end
for i = 1, 16 do
@@ -1346,8 +1235,15 @@ end
end
if duration and timeleft and cfg.debufftimers then
-- C_UnitAuras returns the Spell.dbc base duration, which talents
-- can extend past — Shadow Affinity bumps SW:P from 18s to 24s,
-- so a fresh cast has timeleft > duration and `now + timeleft -
-- duration` lands in the future, which CooldownFrame_SetTimer
-- treats as "not yet started" (no swirl). Widen to whichever is
-- larger so start <= now.
local effDuration = duration > timeleft and duration or timeleft
plate.cdCache = plate.cdCache or {}
local newStart = GetTime() + timeleft - duration
local newStart = GetTime() + timeleft - effDuration
local slotCache = plate.cdCache[index]
local cachedStart = slotCache and slotCache.effect == effect and slotCache.start
local cd = plate.debuffs[index].cd
@@ -1361,7 +1257,7 @@ end
cd.cachedText = cfg.debufftext
cd.configCached = true
end
CooldownFrame_SetTimer(cd, newStart, duration, 1)
CooldownFrame_SetTimer(cd, newStart, effDuration, 1)
plate.cdCache[index] = plate.cdCache[index] or {}
plate.cdCache[index].effect = effect
plate.cdCache[index].start = newStart
@@ -1415,9 +1311,7 @@ end
local isCastingNonTarget = not target and nameplate.castbar and nameplate.castbar:IsShown()
if not isCastingNonTarget and not target and cfg.showcastbar and nameplate.cachedGuid then
local castInfo = GetCastInfo(nameplate.cachedGuid)
if castInfo and castInfo.spellID and castInfo.endTime and castInfo.endTime > now then
isCastingNonTarget = true
elseif pfGetCastInfo and nameplate.castUpdate then
if castInfo and castInfo.endTime > now then
isCastingNonTarget = true
end
end
@@ -1588,17 +1482,6 @@ end
update = true
end
-- PERF: scan for debuff timeouts using indexed access instead of pairs()
if nameplate.debuffcache then
for id = 1, 16 do
local data = nameplate.debuffcache[id]
if data and ( not data.stop or data.stop < now ) and not data.empty then
data.empty = true
update = true
end
end
end
-- use timer based updates
if not nameplate.tick or nameplate.tick < now then
update = true
@@ -1716,94 +1599,40 @@ end
-- Shared castbar update logic (used by both dedicated frame and central loop)
nameplates.UpdateCastbar = function(nameplate, now)
if not nameplate or not nameplate.castbar then return end
local unitstr = nameplate.cachedGuid
local castInfo = unitstr and GetCastInfo(unitstr)
local castInfo = GetCastInfo(nameplate.cachedGuid)
if not castInfo or castInfo.endTime < now then
nameplate.castbar.isShown = nil
nameplate.castbar.lastEndTime = nil
nameplate.castbar:Hide()
return
end
if castInfo and castInfo.spellID then
if castInfo.startTime + castInfo.duration < now then
wipe(castInfo)
nameplate.castbar:Hide()
elseif castInfo.event == "CAST" or castInfo.event == "FAIL" then
wipe(castInfo)
nameplate.castbar:Hide()
else
-- Only update min/max, color and icon once per cast (when endTime changes)
local isChannel = castInfo.event == "CHANNEL"
local duration = castInfo.endTime - castInfo.startTime
if nameplate.castbar.lastEndTime ~= castInfo.endTime then
nameplate.castbar.lastEndTime = castInfo.endTime
nameplate.castbar.lastTextTick = nil
-- Use relative 0..duration range (same as castbar.lua) to avoid
-- floating-point precision loss with large absolute timestamps
nameplate.castbar:SetMinMaxValues(0, duration)
nameplate.castbar:SetStatusBarColor(strsplit(",", C.appearance.castbar[(isChannel and "channelcolor" or "castbarcolor")]))
if castInfo.icon then
nameplate.castbar.icon.tex:SetTexture(castInfo.icon)
nameplate.castbar.icon.tex:SetTexCoord(.1,.9,.1,.9)
end
if cfg.spellname then
nameplate.castbar.spell:SetText(castInfo.spellName)
else
nameplate.castbar.spell:SetText("")
end
end
local barValue
if isChannel then
barValue = castInfo.endTime - now
else
barValue = now - castInfo.startTime
end
barValue = barValue < 0 and 0 or barValue
barValue = barValue > duration and duration or barValue
nameplate.castbar:SetValue(barValue)
SetCastbarText(nameplate.castbar, castInfo.endTime - now)
if not nameplate.castbar.isShown then nameplate.castbar.isShown = true; nameplate.castbar:Show() end
local isChannel = castInfo.isChannel
local duration = castInfo.duration
if nameplate.castbar.lastEndTime ~= castInfo.endTime then
nameplate.castbar.lastEndTime = castInfo.endTime
nameplate.castbar.lastTextTick = nil
-- Relative 0..duration range to avoid float precision loss with large
-- absolute timestamps.
nameplate.castbar:SetMinMaxValues(0, duration)
nameplate.castbar:SetStatusBarColor(strsplit(",", C.appearance.castbar[(isChannel and "channelcolor" or "castbarcolor")]))
if castInfo.icon then
nameplate.castbar.icon.tex:SetTexture(castInfo.icon)
nameplate.castbar.icon.tex:SetTexCoord(.1,.9,.1,.9)
end
else
-- libcast fallback (vanilla without Nampower)
if unitstr and pfGetCastInfo then
local cast, _, _, texture, startTime, endTime = pfGetCastInfo(unitstr)
local channel
if not cast then
channel, _, _, texture, startTime, endTime = pfGetChannelInfo(unitstr)
end
if cast or channel then
local effect = cast or channel
local duration = endTime - startTime
local max = duration / 1000
local cur = now - startTime / 1000
if channel then cur = max + startTime / 1000 - now end
if cur < 0 then cur = 0 end
if cur > max then cur = max end
if nameplate.castbar.lastEndTime ~= endTime then
nameplate.castbar.lastEndTime = endTime
nameplate.castbar.lastTextTick = nil
nameplate.castbar:SetMinMaxValues(0, max)
nameplate.castbar:SetStatusBarColor(strsplit(",", C.appearance.castbar[(channel and "channelcolor" or "castbarcolor")]))
if texture then
nameplate.castbar.icon.tex:SetTexture(texture)
nameplate.castbar.icon.tex:SetTexCoord(.1, .9, .1, .9)
end
if cfg.spellname then
nameplate.castbar.spell:SetText(effect)
else
nameplate.castbar.spell:SetText("")
end
end
nameplate.castbar:SetValue(cur)
SetCastbarText(nameplate.castbar, channel and cur or (max - cur))
if not nameplate.castbar.isShown then nameplate.castbar.isShown = true; nameplate.castbar:Show() end
else
nameplate.castbar.isShown = nil
nameplate.castbar.lastEndTime = nil
nameplate.castbar:Hide()
end
if cfg.spellname then
nameplate.castbar.spell:SetText(castInfo.spellName)
else
nameplate.castbar.isShown = nil
nameplate.castbar.lastEndTime = nil
nameplate.castbar:Hide()
nameplate.castbar.spell:SetText("")
end
end
local barValue = isChannel and (castInfo.endTime - now) or (now - castInfo.startTime)
if barValue < 0 then barValue = 0 end
if barValue > duration then barValue = duration end
nameplate.castbar:SetValue(barValue)
SetCastbarText(nameplate.castbar, castInfo.endTime - now)
if not nameplate.castbar.isShown then nameplate.castbar.isShown = true; nameplate.castbar:Show() end
end
-- Dedicated frame that updates ONLY the target plate castbar.
-98
View File
@@ -174,102 +174,4 @@ pfUI:RegisterModule("superwow", function ()
end
end
-- Enhance libcast with SuperWoW data for NPCs and other players
-- Player casts use SPELLCAST_* events for proper pushback handling
local supercast = CreateFrame("Frame")
local playerGuid = nil
supercast:RegisterEvent("PLAYER_ENTERING_WORLD")
supercast:RegisterEvent("UNIT_CASTEVENT")
supercast:RegisterEvent("PLAYER_LOGOUT")
supercast:SetScript("OnEvent", function()
-- Handle shutdown to prevent crash 132
if event == "PLAYER_LOGOUT" then
this:UnregisterAllEvents()
this:SetScript("OnEvent", nil)
return
end
if event == "PLAYER_ENTERING_WORLD" then
-- Cache player GUID
if UnitExists then
local guid = UnitGUID("player")
playerGuid = guid
end
return
end
local guid = arg1
local isPlayer = guid == playerGuid
-- For non-player units: disable combat parsing events (one-time init)
if not isPlayer and not supercast.init then
-- disable combat parsing events in superwow mode (for non-player units)
libcast:UnregisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE")
libcast:UnregisterEvent("CHAT_MSG_SPELL_HOSTILEPLAYER_DAMAGE")
libcast:UnregisterEvent("CHAT_MSG_SPELL_HOSTILEPLAYER_BUFF")
libcast:UnregisterEvent("CHAT_MSG_SPELL_FRIENDLYPLAYER_DAMAGE")
libcast:UnregisterEvent("CHAT_MSG_SPELL_FRIENDLYPLAYER_BUFF")
libcast:UnregisterEvent("CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_BUFFS")
libcast:UnregisterEvent("CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_BUFFS")
libcast:UnregisterEvent("CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE")
libcast:UnregisterEvent("CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_DAMAGE")
libcast:UnregisterEvent("CHAT_MSG_SPELL_PERIODIC_SELF_DAMAGE")
libcast:UnregisterEvent("CHAT_MSG_SPELL_PARTY_DAMAGE")
libcast:UnregisterEvent("CHAT_MSG_SPELL_PARTY_BUFF")
libcast:UnregisterEvent("CHAT_MSG_SPELL_PERIODIC_PARTY_DAMAGE")
libcast:UnregisterEvent("CHAT_MSG_SPELL_PERIODIC_PARTY_BUFFS")
libcast:UnregisterEvent("CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE")
libcast:UnregisterEvent("CHAT_MSG_SPELL_PERIODIC_CREATURE_BUFFS")
libcast:UnregisterEvent("CHAT_MSG_SPELL_CREATURE_VS_CREATURE_DAMAGE")
libcast:UnregisterEvent("CHAT_MSG_SPELL_CREATURE_VS_CREATURE_BUFF")
supercast.init = true
end
if arg3 == "START" or arg3 == "CAST" or arg3 == "CHANNEL" then
local target = arg2
local event_type = arg3
local spell_id = arg4
local timer = arg5
local spell = C_Spell.GetSpellName(spell_id) or UNKNOWN
local icon = C_Spell.GetSpellTexture(spell_id) or "Interface\\Icons\\INV_Misc_QuestionMark"
-- skip on buff procs during cast
if event_type == "CAST" then
if not libcast.db[guid] or libcast.db[guid].cast ~= spell then
-- ignore casts without 'START' event, while there is already another cast.
-- those events can be for example a frost shield proc while casting frostbolt.
-- we want to keep the cast itself, so we simply skip those.
return
end
end
-- For player: store in libcast.db[playerName] so pushback tracking works
-- For others: store by GUID
local dbKey = isPlayer and UnitName("player") or guid
-- add cast action to the database
if not libcast.db[dbKey] then libcast.db[dbKey] = {} end
libcast.db[dbKey].cast = spell
libcast.db[dbKey].rank = nil
libcast.db[dbKey].start = GetTime()
libcast.db[dbKey].casttime = timer or 0
libcast.db[dbKey].icon = icon
libcast.db[dbKey].channel = event_type == "CHANNEL" or false
elseif arg3 == "FAIL" then
-- For player: use playerName, for others: use GUID
local dbKey = isPlayer and UnitName("player") or guid
-- delete all cast entries
if libcast.db[dbKey] then
libcast.db[dbKey].cast = nil
libcast.db[dbKey].rank = nil
libcast.db[dbKey].start = nil
libcast.db[dbKey].casttime = nil
libcast.db[dbKey].icon = nil
libcast.db[dbKey].channel = nil
end
end
end)
end)
+32 -33
View File
@@ -312,42 +312,41 @@ pfUI:RegisterModule("turtle-wow", function ()
end
end)
-- add Trueshot recognition to to custom castbars.
if not libcast.customcast["steadyshot"] then
-- add trueshot to pfUI's custom casts
local player = UnitName("player")
-- Synthetic Steady Shot cast bar. The engine treats Steady Shot as
-- instant, but the arrow doesn't actually fire until the next ranged
-- swing (~1.4s baseline). Drive a synthetic entry off Nampower's
-- SPELL_QUEUE_EVENT so hunters see a tickdown matching the swing wait.
-- castbar.lua reads pfUI.synthetic_casts[unit] as a fallback when
-- C_Spell.UnitCastingInfo returns nil.
pfUI_locale["enUS"]["customcast"]["TRUESHOT"] = "Steady Shot"
pfUI_locale["zhCN"]["customcast"]["TRUESHOT"] = "稳固射击"
pfUI.synthetic_casts = pfUI.synthetic_casts or {}
-- add locales
pfUI_locale["enUS"]["customcast"]["TRUESHOT"] = "Steady Shot"
pfUI_locale["zhCN"]["customcast"]["TRUESHOT"] = "稳固射击"
local trueshot = L["customcast"]["TRUESHOT"] or ""
local steadyShotName = L["customcast"]["TRUESHOT"]
local STEADY_SHOT_ICON = "Interface\\Icons\\Ability_hunter_steadyshot"
local STEADY_SHOT_DURATION_MS = 1400 -- not haste-scaled; close enough for the visual cue
local ON_SWING_QUEUED, ON_SWING_QUEUE_POPPED = 0, 1
libcast.customcast[strlower(trueshot)] = function(begin, duration)
if begin then
-- cast time is 1sec, however it takes 1.4sec to fire in average
local duration = libcast.ApplyShotHaste(duration or 1400)
local _,_, lag = GetNetStats()
local start = GetTime() + lag/1000
-- add cast action to the database
libcast.db[player].cast = trueshot
libcast.db[player].rank = lastrank
libcast.db[player].start = start
libcast.db[player].casttime = duration
libcast.db[player].icon = "Interface\\Icons\\Ability_hunter_steadyshot"
libcast.db[player].channel = nil
else
-- remove cast action to the database
libcast.db[player].cast = nil
libcast.db[player].rank = nil
libcast.db[player].start = nil
libcast.db[player].casttime = nil
libcast.db[player].icon = nil
libcast.db[player].channel = nil
end
local steadyShot = CreateFrame("Frame")
steadyShot:RegisterEvent("SPELL_QUEUE_EVENT")
steadyShot:SetScript("OnEvent", function()
if event ~= "SPELL_QUEUE_EVENT" then return end
local eventCode, spellId = arg1, arg2
if eventCode ~= ON_SWING_QUEUED and eventCode ~= ON_SWING_QUEUE_POPPED then return end
if C_Spell.GetSpellName(spellId) ~= steadyShotName then return end
if eventCode == ON_SWING_QUEUED then
local now = GetTime() * 1000
pfUI.synthetic_casts["player"] = {
name = steadyShotName,
icon = STEADY_SHOT_ICON,
startMs = now,
endMs = now + STEADY_SHOT_DURATION_MS,
spellID = spellId,
}
else
pfUI.synthetic_casts["player"] = nil
end
end
end)
-- add skin to twow's talent inspect frame
if pfUI.skin["Inspect"] and pfUI_config["disabled"]["skin_Inspect"] ~= "1" then
+62 -53
View File
@@ -1,18 +1,31 @@
pfUI:RegisterModule("xpbar", function ()
local rawborder, default_border = GetBorderSize()
local parse_faction = SanitizePattern(FACTION_STANDING_INCREASED)
-- Rested-XP gain tracking constants
local REST_WINDOW = 300 -- seconds of sliding-window samples for rate calc
local REST_CAP_MUL = 1.5 -- target rested cap = UnitXPMax * 1.5
if IsTurtleWoW() then
REST_CAP_MUL = 1.13
end
-- Resolve the faction to display: the explicitly remembered one (last to
-- gain rep) if set, else the player's watched faction. Returns the same
-- 5-tuple GetFactionInfo's relevant slots produced.
local function GetRepDisplay(factionID)
if factionID then
local name, _, standingID, barMin, barMax, barValue = GetFactionInfoByID(factionID)
if name then return name, standingID, barMin, barMax, barValue end
end
local w = C_Reputation.GetWatchedFactionData()
if w then
return w.name, w.reaction, w.currentReactionThreshold, w.nextReactionThreshold, w.currentStanding
end
end
local data = CreateFrame("Frame", "pfExperienceBarData", UIParent)
data.rest_samples = {}
data:RegisterEvent("CHAT_MSG_COMBAT_FACTION_CHANGE")
data:RegisterEvent("FACTION_STANDING_CHANGED")
data:RegisterEvent("PLAYER_ENTERING_WORLD")
data:RegisterEvent("PLAYER_LEVEL_UP")
data:RegisterEvent("UPDATE_EXHAUSTION")
@@ -35,16 +48,17 @@ pfUI:RegisterModule("xpbar", function ()
while this.rest_samples[1] and (now - this.rest_samples[1][1]) > REST_WINDOW do
table.remove(this.rest_samples, 1)
end
elseif event == "CHAT_MSG_COMBAT_FACTION_CHANGE" then
local _,_, faction, amount = string.find(arg1, parse_faction)
this.faction = faction or this.faction
elseif event == "FACTION_STANDING_CHANGED" then
-- arg1=factionID, arg2=newStanding, arg3=repGained — no chat-string
-- parsing, no locale dependency.
this.factionID = arg1
elseif event == "UPDATE_FACTION" then
-- drop the auto-tracked faction when the user changes the watched one
local watched = C_Reputation.GetWatchedFactionData()
local watchedName = watched and watched.name or nil
if watchedName ~= this.watched then
this.watched = watchedName
this.faction = nil
local watchedID = watched and watched.factionID or nil
if watchedID ~= this.watchedID then
this.watchedID = watchedID
this.factionID = nil
end
end
end)
@@ -132,24 +146,21 @@ local function OnEnter(self)
table.insert(lines, { T["XP"], "|cffffffff" .. xp .. " / " .. xpmax .. " (" .. xp_perc .. "%)" })
table.insert(lines, { T["Remaining"], "|cffffffff" .. remaining .. " (" .. remaining_perc .. "%)" })
elseif mode == "REP" then
for i=1, 99 do
local name, description, standingID, barMin, barMax, barValue, atWarWith, canToggleAtWar, isHeader, isCollapsed, isWatched = GetFactionInfo(i)
if ( isWatched and not self.faction ) or ( self.faction and name == self.faction) then
barMax = barMax - barMin
barValue = barValue - barMin
local name, standingID, barMin, barMax, barValue = GetRepDisplay(self.factionID)
if name then
barMax = barMax - barMin
barValue = barValue - barMin
local color = FACTION_BAR_COLORS[standingID]
if color then
color = rgbhex(color.r + .3, color.g + .3, color.b + .3)
else
color = rgbhex(.5, .5, .5)
end
table.insert(lines, { "|cff555555" .. T["Reputation"], "" })
table.insert(lines, { color .. name .. " (" .. GetText("FACTION_STANDING_LABEL"..standingID, gender) .. ")"})
table.insert(lines, { barValue .. " / " .. barMax .. " (" .. round(barValue / barMax * 100) .. "%)" })
break
local color = FACTION_BAR_COLORS[standingID]
if color then
color = rgbhex(color.r + .3, color.g + .3, color.b + .3)
else
color = rgbhex(.5, .5, .5)
end
table.insert(lines, { "|cff555555" .. T["Reputation"], "" })
table.insert(lines, { color .. name .. " (" .. GetText("FACTION_STANDING_LABEL"..standingID, gender) .. ")"})
table.insert(lines, { barValue .. " / " .. barMax .. " (" .. round(barValue / barMax * 100) .. "%)" })
end
end
@@ -195,10 +206,10 @@ end
-- set either experience, reputation or flex-rep handler
local mode = self.display
if self.display == "XPFLEX" then
self.faction = data.faction or nil
self.factionID = data.factionID or nil
mode = UnitLevel("player") < MAX_PLAYER_LEVEL and "XP" or "REP"
elseif self.display == "FLEX" then
self.faction = data.faction or nil
self.factionID = data.factionID or nil
mode = "REP"
end
@@ -208,7 +219,7 @@ end
end
-- skip on events of no interest
if mode == "XP" and ( event == "CHAT_MSG_COMBAT_FACTION_CHANGE" or event == "UPDATE_FACTION" ) then return end
if mode == "XP" and ( event == "FACTION_STANDING_CHANGED" or event == "UPDATE_FACTION" ) then return end
if mode == "REP" and ( event == "PLAYER_XP_UPDATE" or event == "UPDATE_EXHAUSTION" ) then return end
if mode == "XP" then
@@ -254,33 +265,31 @@ end
elseif mode == "REP" then
self.restedbar:Hide()
for i=1, 99 do
local name, description, standingID, barMin, barMax, barValue, atWarWith, canToggleAtWar, isHeader, isCollapsed, isWatched = GetFactionInfo(i)
if ( isWatched and not self.faction ) or ( self.faction and name == self.faction) then
self.enabled = true
self:SetAlpha(1)
local name, standingID, barMin, barMax, barValue = GetRepDisplay(self.factionID)
if name then
self.enabled = true
self:SetAlpha(1)
barMax = barMax - barMin
barValue = barValue - barMin
barMax = barMax - barMin
barValue = barValue - barMin
self.bar:SetMinMaxValues(0, barMax)
self.bar:SetValue(barValue)
self.bar:SetMinMaxValues(0, barMax)
self.bar:SetValue(barValue)
local color = FACTION_BAR_COLORS[standingID]
if color then
self.bar:SetStatusBarColor((color.r + .5) * .5, (color.g + .5) * .5, (color.b + .5) * .5, 1)
else
self.bar:SetStatusBarColor(.5,.5,.5,1)
end
local text = "%s: %s%% (%s)"
local perc = round(barValue / barMax * 100)
local standing = GetText("FACTION_STANDING_LABEL"..standingID, gender)
self.bar.text:SetText(string.format(text, name, perc, standing))
self.tick = GetTime() + self.timeout
return
local color = FACTION_BAR_COLORS[standingID]
if color then
self.bar:SetStatusBarColor((color.r + .5) * .5, (color.g + .5) * .5, (color.b + .5) * .5, 1)
else
self.bar:SetStatusBarColor(.5,.5,.5,1)
end
local text = "%s: %s%% (%s)"
local perc = round(barValue / barMax * 100)
local standing = GetText("FACTION_STANDING_LABEL"..standingID, gender)
self.bar.text:SetText(string.format(text, name, perc, standing))
self.tick = GetTime() + self.timeout
return
end
end
@@ -361,7 +370,7 @@ end
-- auto hide
b:EnableMouse(true)
b:RegisterEvent("CHAT_MSG_COMBAT_FACTION_CHANGE")
b:RegisterEvent("FACTION_STANDING_CHANGED")
b:RegisterEvent("UNIT_PET")
b:RegisterEvent("UNIT_LEVEL")
b:RegisterEvent("UNIT_PET_EXPERIENCE")
+1 -1
View File
@@ -27,7 +27,7 @@ do
-- ClassicAPI dependency check.
-- pfUI relies pervasively on the modern C_* / SuperWoW / nameplate / focus
-- API surface that ClassicAPI polyfills, so presence is required.
local PFUI_CLASSIC_API_MIN = 10403 -- (X*10000 + Y*100 + Z)
local PFUI_CLASSIC_API_MIN = 10503 -- (X*10000 + Y*100 + Z)
local PFUI_CLASSIC_API_LATEST = PFUI_CLASSIC_API_MIN
local PFUI_CLASSIC_API_WEBSITE = "https://github.com/brues-code/ClassicAPI"
local PFUI_CLASSIC_API_LATEST_URL = PFUI_CLASSIC_API_WEBSITE .. "/releases/latest"